laser-dac 0.12.1

Unified laser DAC abstraction supporting multiple protocols
Documentation
//! Unified DAC backend abstraction for laser projectors.
//!
//! This crate provides a common interface for communicating with various
//! laser DAC (Digital-to-Analog Converter) hardware. Two API styles are
//! available:
//!
//! # Getting Started
//!
//! ## Frame Mode (recommended)
//!
//! Submit complete frames with automatic transition blanking:
//!
//! ```no_run
//! use laser_dac::{open_device, FrameSessionConfig, Frame, LaserPoint};
//!
//! let device = open_device("my-device").unwrap();
//! let config = FrameSessionConfig::new(30_000);
//! let (session, _info) = device.start_frame_session(config).unwrap();
//!
//! session.control().arm().unwrap();
//! session.send_frame(Frame::new(vec![
//!     LaserPoint::new(-0.5, 0.0, 65535, 0, 0, 65535),
//!     LaserPoint::new( 0.5, 0.0, 0, 65535, 0, 65535),
//! ]));
//! // Frame replays automatically. Submit new frames for animation.
//! ```
//!
//! For advanced frame-mode output-space processing on the final presented
//! sequence, use [`FrameSessionConfig::with_output_filter`]. The filter runs
//! after transition composition, blanking, and color delay, just before the
//! backend write.
//!
//! For downstream watchdogs, [`FrameSession::metrics`] exposes a small liveness
//! surface with connectivity, last-loop-activity, and last-write-success
//! timestamps. Watchdog policy remains application-owned.
//!
//! ## Callback Mode (advanced, FIFO backends only)
//!
//! Fill point buffers via zero-allocation callback for custom timing.
//! Not available for frame-swap backends (Helios) — use Frame Mode instead.
//!
//! ```no_run
//! use laser_dac::{open_device, StreamConfig, LaserPoint, ChunkRequest, ChunkResult};
//!
//! let device = open_device("my-device").unwrap();
//! let config = StreamConfig::new(30_000);
//! let (stream, _info) = device.start_stream(config).unwrap();
//!
//! stream.control().arm().unwrap();
//!
//! let exit = stream.run(
//!     |req: &ChunkRequest, buffer: &mut [LaserPoint]| {
//!         let n = req.target_points;
//!         for i in 0..n {
//!             buffer[i] = LaserPoint::blanked(0.0, 0.0);
//!         }
//!         ChunkResult::Filled(n)
//!     },
//!     |err| eprintln!("Stream error: {}", err),
//! );
//! ```
//!
//! # Supported DACs
//!
//! - **Helios** - USB laser DAC, Frame API only (feature: `helios`)
//! - **Ether Dream** - Network laser DAC (feature: `ether-dream`)
//! - **IDN** - ILDA Digital Network protocol (feature: `idn`)
//! - **LaserCube WiFi** - WiFi-connected laser DAC (feature: `lasercube-wifi`)
//! - **LaserCube USB** - USB laser DAC / LaserDock (feature: `lasercube-usb`)
//! - **Oscilloscope** - XY mode via stereo audio output (feature: `oscilloscope`)
//! - **AVB Audio Devices** - AVB audio output via the system audio host: CoreAudio (macOS), WASAPI or ASIO (Windows, ASIO opt-in via the `asio` feature), ALSA (Linux). Feature: `avb`.
//!
//! # Features
//!
//! - `all-dacs` (default): Enable all DAC protocols
//! - `usb-dacs`: Enable USB DACs (Helios, LaserCube USB)
//! - `network-dacs`: Enable network DACs (Ether Dream, IDN, LaserCube WiFi)
//! - `audio-dacs`: Enable audio DACs (Oscilloscope, AVB)
//! - `asio` (default): ASIO host on Windows for AVB output. Requires the
//!   Steinberg ASIO SDK plus `LIBCLANG_PATH` and `CPAL_ASIO_DIR` set at
//!   build time. Disable with `default-features = false` (and re-enable the
//!   features you want) to fall back to the default cpal host (WASAPI on
//!   Windows) and skip the SDK requirement.
//!
//! # Coordinate System
//!
//! All backends use normalized coordinates:
//! - X: -1.0 (left) to 1.0 (right)
//! - Y: -1.0 (bottom) to 1.0 (top)
//! - Colors: 0-65535 for R, G, B, and intensity
//!
//! Each backend handles conversion to its native format internally.

pub mod backend;
pub mod buffer_estimate;
pub mod config;
pub mod device;
pub mod discovery;
mod error;
#[cfg(any(feature = "idn", feature = "lasercube-wifi"))]
mod net_utils;
pub mod point;
pub mod presentation;
pub mod protocols;
#[cfg(feature = "receiver")]
pub mod receiver;
pub(crate) mod reconnect;
#[cfg(any(feature = "avb", feature = "oscilloscope"))]
pub(crate) mod resample;
pub mod stream;
pub mod types;

// Crate-level error types
pub use error::{Error, Result};

// Backend traits and types
pub use backend::{BackendKind, DacBackend, FifoBackend, FrameSwapBackend, WriteOutcome};

// Discovery types
pub use discovery::{
    downcast_connect_data, slugify_device_id, DacDiscovery, DiscoveredDevice, DiscoveredDeviceInfo,
    Discoverer,
};

// Point type
pub use point::LaserPoint;

// DAC identity, capabilities, discovery filtering
pub use device::{
    caps_for_dac_type, DacCapabilities, DacConnectionState, DacDevice, DacInfo, DacType,
    EnabledDacTypes, OutputModel,
};

// Configuration
pub use config::{IdlePolicy, ReconnectConfig, StreamConfig};

// Deprecated alias for backwards compatibility
#[allow(deprecated)]
pub use config::UnderrunPolicy;

// Stream and Dac types
pub use stream::{
    ChunkRequest, ChunkResult, Dac, RunExit, Stream, StreamControl, StreamInstant, StreamStats,
    StreamStatus,
};

// Presentation types (frame-first API)
pub use presentation::{
    default_transition, Frame, FrameSession, FrameSessionConfig, FrameSessionMetrics, OutputFilter,
    OutputFilterContext, OutputResetReason, PresentedSliceKind, TransitionFn, TransitionPlan,
};

// Conditional exports based on features

// Helios
#[cfg(feature = "helios")]
pub use backend::HeliosBackend;
#[cfg(feature = "helios")]
pub use protocols::helios;

// Ether Dream
#[cfg(feature = "ether-dream")]
pub use backend::EtherDreamBackend;
#[cfg(feature = "ether-dream")]
pub use protocols::ether_dream;

// IDN
#[cfg(feature = "idn")]
pub use backend::IdnBackend;
#[cfg(feature = "idn")]
pub use protocols::idn;

// LaserCube WiFi
#[cfg(feature = "lasercube-wifi")]
pub use backend::LasercubeWifiBackend;
#[cfg(feature = "lasercube-wifi")]
pub use protocols::lasercube_wifi;

// LaserCube USB
#[cfg(feature = "lasercube-usb")]
pub use backend::LasercubeUsbBackend;
#[cfg(feature = "lasercube-usb")]
pub use protocols::lasercube_usb;

// AVB
#[cfg(feature = "avb")]
pub use backend::AvbBackend;
#[cfg(feature = "avb")]
pub use protocols::avb;

// Re-export rusb for consumers that need the Context type (for LaserCube USB)
#[cfg(feature = "lasercube-usb")]
pub use protocols::lasercube_usb::rusb;

// =============================================================================
// Device Discovery Functions
// =============================================================================

use backend::Result as BackendResult;

/// List all available DACs.
///
/// Returns DAC info for each discovered DAC, including capabilities.
pub fn list_devices() -> BackendResult<Vec<DacInfo>> {
    list_devices_filtered(&EnabledDacTypes::all())
}

/// List available DACs filtered by DAC type.
pub fn list_devices_filtered(enabled_types: &EnabledDacTypes) -> BackendResult<Vec<DacInfo>> {
    let mut discovery = DacDiscovery::new(enabled_types.clone());
    let devices = discovery
        .scan()
        .into_iter()
        .map(|device| DacInfo {
            id: device.info().stable_id().to_string(),
            name: device.info().name().to_string(),
            kind: device.dac_type().clone(),
            caps: device.caps().clone(),
        })
        .collect();

    Ok(devices)
}

/// Open a DAC by ID.
///
/// The ID should match the `id` field returned by [`list_devices`].
/// IDs are namespaced by protocol (e.g., `etherdream:aa:bb:cc:dd:ee:ff`,
/// `idn:hostname.local`, `helios:serial`, `avb:device-slug:n`).
pub fn open_device(id: &str) -> BackendResult<Dac> {
    let mut discovery = DacDiscovery::new(EnabledDacTypes::all());
    let mut dac = discovery.open_by_id(id)?;
    dac.reconnect_target = Some(reconnect::ReconnectTarget {
        device_id: id.to_string(),
        discovery_factory: None,
    });
    Ok(dac)
}

/// Open a DAC by ID using a custom discovery factory.
///
/// Like [`open_device`], but uses the provided factory to create the
/// [`DacDiscovery`] instance. This is required for custom backends
/// registered via [`DacDiscovery::register`] — the default `open_device`
/// only finds built-in DAC types.
///
/// The factory is called once now for the initial open, and stored for
/// future reconnection attempts. It must be `Fn` (not `FnOnce`) because
/// reconnection may call it multiple times.
///
/// # Example
///
/// ```ignore
/// use laser_dac::{open_device_with, DacDiscovery, EnabledDacTypes, FrameSessionConfig, ReconnectConfig};
///
/// let dac = open_device_with("shownet:my-device", || {
///     let mut d = DacDiscovery::new(EnabledDacTypes::all());
///     d.register(Box::new(MyShowNetDiscoverer::new()));
///     d
/// })?;
///
/// let config = FrameSessionConfig::new(30_000)
///     .with_reconnect(ReconnectConfig::new());
/// let (session, _info) = dac.start_frame_session(config)?;
/// // Reconnection will also use the factory to find the custom backend
/// ```
pub fn open_device_with<F>(id: &str, factory: F) -> BackendResult<Dac>
where
    F: Fn() -> DacDiscovery + Send + 'static,
{
    let mut discovery = factory();
    let mut dac = discovery.open_by_id(id)?;
    dac.reconnect_target = Some(reconnect::ReconnectTarget {
        device_id: id.to_string(),
        discovery_factory: Some(Box::new(factory)),
    });
    Ok(dac)
}