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}