Skip to main content

laser_dac/
lib.rs

1//! Unified DAC backend abstraction for laser projectors.
2//!
3//! This crate provides a common interface for communicating with various
4//! laser DAC (Digital-to-Analog Converter) hardware using a streaming API
5//! that provides uniform pacing and backpressure across all device types.
6//!
7//! # Getting Started
8//!
9//! The streaming API provides two modes of operation:
10//!
11//! ## Blocking Mode
12//!
13//! Use `next_request()` to get what to produce, then `write()` to send points:
14//!
15//! ```no_run
16//! use laser_dac::{list_devices, open_device, StreamConfig, LaserPoint};
17//!
18//! // Discover devices
19//! let devices = list_devices().unwrap();
20//! println!("Found {} devices", devices.len());
21//!
22//! // Open and start streaming
23//! let device = open_device(&devices[0].id).unwrap();
24//! let config = StreamConfig::new(30_000); // 30k points per second
25//! let (mut stream, info) = device.start_stream(config).unwrap();
26//!
27//! // Arm the output (allow laser to fire)
28//! stream.control().arm().unwrap();
29//!
30//! // Streaming loop
31//! loop {
32//!     let req = stream.next_request().unwrap();
33//!
34//!     // Generate points for this chunk
35//!     let points: Vec<LaserPoint> = (0..req.n_points)
36//!         .map(|i| {
37//!             let t = (req.start.points() + i as u64) as f32 / req.pps as f32;
38//!             let angle = t * std::f32::consts::TAU;
39//!             LaserPoint::new(angle.cos(), angle.sin(), 65535, 0, 0, 65535)
40//!         })
41//!         .collect();
42//!
43//!     stream.write(&req, &points).unwrap();
44//! }
45//! ```
46//!
47//! ## Callback Mode
48//!
49//! Use `run()` with a producer closure for simpler code:
50//!
51//! ```no_run
52//! use laser_dac::{list_devices, open_device, StreamConfig, LaserPoint, ChunkRequest};
53//!
54//! let device = open_device("my-device").unwrap();
55//! let config = StreamConfig::new(30_000);
56//! let (stream, _info) = device.start_stream(config).unwrap();
57//!
58//! stream.control().arm().unwrap();
59//!
60//! let exit = stream.run(
61//!     |req: ChunkRequest| {
62//!         // Return Some(points) to continue, None to stop
63//!         let points = vec![LaserPoint::blanked(0.0, 0.0); req.n_points];
64//!         Some(points)
65//!     },
66//!     |err| eprintln!("Stream error: {}", err),
67//! );
68//! ```
69//!
70//! # Supported DACs
71//!
72//! - **Helios** - USB laser DAC (feature: `helios`)
73//! - **Ether Dream** - Network laser DAC (feature: `ether-dream`)
74//! - **IDN** - ILDA Digital Network protocol (feature: `idn`)
75//! - **LaserCube WiFi** - WiFi-connected laser DAC (feature: `lasercube-wifi`)
76//! - **LaserCube USB** - USB laser DAC / LaserDock (feature: `lasercube-usb`)
77//!
78//! # Features
79//!
80//! - `all-dacs` (default): Enable all DAC protocols
81//! - `usb-dacs`: Enable USB DACs (Helios, LaserCube USB)
82//! - `network-dacs`: Enable network DACs (Ether Dream, IDN, LaserCube WiFi)
83//!
84//! # Coordinate System
85//!
86//! All backends use normalized coordinates:
87//! - X: -1.0 (left) to 1.0 (right)
88//! - Y: -1.0 (bottom) to 1.0 (top)
89//! - Colors: 0-65535 for R, G, B, and intensity
90//!
91//! Each backend handles conversion to its native format internally.
92
93pub mod backend;
94pub mod discovery;
95mod error;
96mod frame_adapter;
97#[cfg(any(feature = "idn", feature = "lasercube-wifi"))]
98mod net_utils;
99pub mod protocols;
100pub mod session;
101pub mod stream;
102pub mod types;
103
104// Crate-level error types
105pub use error::{Error, Result};
106
107// Backend trait and types
108pub use backend::{StreamBackend, WriteOutcome};
109
110// Discovery types
111pub use discovery::{
112    DacDiscovery, DiscoveredDevice, DiscoveredDeviceInfo, ExternalDevice, ExternalDiscoverer,
113};
114
115// Core types
116pub use types::{
117    // DAC types
118    caps_for_dac_type,
119    ChunkRequest,
120    // Streaming types
121    DacCapabilities,
122    DacConnectionState,
123    DacDevice,
124    DacInfo,
125    DacType,
126    EnabledDacTypes,
127    LaserPoint,
128    OutputModel,
129    RunExit,
130    StreamConfig,
131    StreamInstant,
132    StreamStats,
133    StreamStatus,
134    UnderrunPolicy,
135};
136
137// Stream and Dac types
138pub use session::{ReconnectingSession, SessionControl};
139pub use stream::{Dac, OwnedDac, Stream, StreamControl};
140
141// Frame adapters (converts point buffers to continuous streams)
142pub use frame_adapter::{Frame, FrameAdapter, SharedFrameAdapter};
143
144// Conditional exports based on features
145
146// Helios
147#[cfg(feature = "helios")]
148pub use backend::HeliosBackend;
149#[cfg(feature = "helios")]
150pub use protocols::helios;
151
152// Ether Dream
153#[cfg(feature = "ether-dream")]
154pub use backend::EtherDreamBackend;
155#[cfg(feature = "ether-dream")]
156pub use protocols::ether_dream;
157
158// IDN
159#[cfg(feature = "idn")]
160pub use backend::IdnBackend;
161#[cfg(feature = "idn")]
162pub use protocols::idn;
163
164// LaserCube WiFi
165#[cfg(feature = "lasercube-wifi")]
166pub use backend::LasercubeWifiBackend;
167#[cfg(feature = "lasercube-wifi")]
168pub use protocols::lasercube_wifi;
169
170// LaserCube USB
171#[cfg(feature = "lasercube-usb")]
172pub use backend::LasercubeUsbBackend;
173#[cfg(feature = "lasercube-usb")]
174pub use protocols::lasercube_usb;
175
176// Re-export rusb for consumers that need the Context type (for LaserCube USB)
177#[cfg(feature = "lasercube-usb")]
178pub use protocols::lasercube_usb::rusb;
179
180// =============================================================================
181// Device Discovery Functions
182// =============================================================================
183
184use backend::Result as BackendResult;
185
186/// List all available DACs.
187///
188/// Returns DAC info for each discovered DAC, including capabilities.
189pub fn list_devices() -> BackendResult<Vec<DacInfo>> {
190    list_devices_filtered(&EnabledDacTypes::all())
191}
192
193/// List available DACs filtered by DAC type.
194pub fn list_devices_filtered(enabled_types: &EnabledDacTypes) -> BackendResult<Vec<DacInfo>> {
195    let mut discovery = DacDiscovery::new(enabled_types.clone());
196    let devices = discovery
197        .scan()
198        .into_iter()
199        .map(|device| {
200            let info = device.info();
201            DacInfo {
202                id: info.stable_id(),
203                name: info.name(),
204                kind: device.dac_type(),
205                caps: caps_for_dac_type(&device.dac_type()),
206            }
207        })
208        .collect();
209
210    Ok(devices)
211}
212
213/// Open a DAC by ID.
214///
215/// The ID should match the `id` field returned by [`list_devices`].
216/// IDs are namespaced by protocol (e.g., `etherdream:aa:bb:cc:dd:ee:ff`,
217/// `idn:hostname.local`, `helios:serial`).
218pub fn open_device(id: &str) -> BackendResult<Dac> {
219    let mut discovery = DacDiscovery::new(EnabledDacTypes::all());
220    let discovered = discovery.scan();
221
222    let device = discovered
223        .into_iter()
224        .find(|d| d.info().stable_id() == id)
225        .ok_or_else(|| backend::Error::disconnected(format!("DAC not found: {}", id)))?;
226
227    let info = device.info();
228    let name = info.name();
229    let dac_type = device.dac_type();
230    let stream_backend = discovery.connect(device)?;
231
232    let dac_info = DacInfo {
233        id: id.to_string(),
234        name,
235        kind: dac_type,
236        caps: stream_backend.caps().clone(),
237    };
238
239    Ok(Dac::new(dac_info, stream_backend))
240}