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;
97pub mod protocols;
98pub mod session;
99pub mod stream;
100pub mod types;
101
102// Crate-level error types
103pub use error::{Error, Result};
104
105// Backend trait and types
106pub use backend::{StreamBackend, WriteOutcome};
107
108// Discovery types
109pub use discovery::{
110 DacDiscovery, DiscoveredDevice, DiscoveredDeviceInfo, ExternalDevice, ExternalDiscoverer,
111};
112
113// Core types
114pub use types::{
115 // DAC types
116 caps_for_dac_type,
117 ChunkRequest,
118 // Streaming types
119 DacCapabilities,
120 DacConnectionState,
121 DacDevice,
122 DacInfo,
123 DacType,
124 EnabledDacTypes,
125 LaserPoint,
126 OutputModel,
127 RunExit,
128 StreamConfig,
129 StreamInstant,
130 StreamStats,
131 StreamStatus,
132 UnderrunPolicy,
133};
134
135// Stream and Dac types
136pub use session::{ReconnectingSession, SessionControl};
137pub use stream::{Dac, OwnedDac, Stream, StreamControl};
138
139// Frame adapters (converts point buffers to continuous streams)
140pub use frame_adapter::{Frame, FrameAdapter, SharedFrameAdapter};
141
142// Conditional exports based on features
143
144// Helios
145#[cfg(feature = "helios")]
146pub use backend::HeliosBackend;
147#[cfg(feature = "helios")]
148pub use protocols::helios;
149
150// Ether Dream
151#[cfg(feature = "ether-dream")]
152pub use backend::EtherDreamBackend;
153#[cfg(feature = "ether-dream")]
154pub use protocols::ether_dream;
155
156// IDN
157#[cfg(feature = "idn")]
158pub use backend::IdnBackend;
159#[cfg(feature = "idn")]
160pub use protocols::idn;
161
162// LaserCube WiFi
163#[cfg(feature = "lasercube-wifi")]
164pub use backend::LasercubeWifiBackend;
165#[cfg(feature = "lasercube-wifi")]
166pub use protocols::lasercube_wifi;
167
168// LaserCube USB
169#[cfg(feature = "lasercube-usb")]
170pub use backend::LasercubeUsbBackend;
171#[cfg(feature = "lasercube-usb")]
172pub use protocols::lasercube_usb;
173
174// Re-export rusb for consumers that need the Context type (for LaserCube USB)
175#[cfg(feature = "lasercube-usb")]
176pub use protocols::lasercube_usb::rusb;
177
178// =============================================================================
179// Device Discovery Functions
180// =============================================================================
181
182use backend::Result as BackendResult;
183
184/// List all available DACs.
185///
186/// Returns DAC info for each discovered DAC, including capabilities.
187pub fn list_devices() -> BackendResult<Vec<DacInfo>> {
188 list_devices_filtered(&EnabledDacTypes::all())
189}
190
191/// List available DACs filtered by DAC type.
192pub fn list_devices_filtered(enabled_types: &EnabledDacTypes) -> BackendResult<Vec<DacInfo>> {
193 let mut discovery = DacDiscovery::new(enabled_types.clone());
194 let devices = discovery
195 .scan()
196 .into_iter()
197 .map(|device| {
198 let info = device.info();
199 DacInfo {
200 id: info.stable_id(),
201 name: info.name(),
202 kind: device.dac_type(),
203 caps: caps_for_dac_type(&device.dac_type()),
204 }
205 })
206 .collect();
207
208 Ok(devices)
209}
210
211/// Open a DAC by ID.
212///
213/// The ID should match the `id` field returned by [`list_devices`].
214/// IDs are namespaced by protocol (e.g., `etherdream:aa:bb:cc:dd:ee:ff`,
215/// `idn:hostname.local`, `helios:serial`).
216pub fn open_device(id: &str) -> BackendResult<Dac> {
217 let mut discovery = DacDiscovery::new(EnabledDacTypes::all());
218 let discovered = discovery.scan();
219
220 let device = discovered
221 .into_iter()
222 .find(|d| d.info().stable_id() == id)
223 .ok_or_else(|| backend::Error::disconnected(format!("DAC not found: {}", id)))?;
224
225 let info = device.info();
226 let name = info.name();
227 let dac_type = device.dac_type();
228 let stream_backend = discovery.connect(device)?;
229
230 let dac_info = DacInfo {
231 id: id.to_string(),
232 name,
233 kind: dac_type,
234 caps: stream_backend.caps().clone(),
235 };
236
237 Ok(Dac::new(dac_info, stream_backend))
238}