spectro_rs/
lib.rs

1//! # spectro-rs
2//!
3//! A high-performance Rust driver for X-Rite ColorMunki spectrometers.
4//!
5//! This crate provides a safe, ergonomic interface for interacting with
6//! ColorMunki (Original and Design) devices, supporting reflective, emissive,
7//! and ambient measurement modes.
8//!
9//! ## Quick Start
10//!
11//! ```ignore
12//! use spectro_rs::{discover, MeasurementMode};
13//!
14//! fn main() -> spectro_rs::Result<()> {
15//!     // Find and connect to a device
16//!     let mut device = discover()?;
17//!     println!("Found: {:?}", device.info()?);
18//!
19//!     // Calibrate for reflective measurements
20//!     device.calibrate()?;
21//!
22//!     // Measure and get spectral data
23//!     let spectrum = device.measure(MeasurementMode::Reflective)?;
24//!     let xyz = spectrum.to_xyz();
25//!     println!("CIE XYZ: X={:.2}, Y={:.2}, Z={:.2}", xyz.x, xyz.y, xyz.z);
26//!
27//!     Ok(())
28//! }
29//! ```
30//!
31//! ## Architecture
32//!
33//! The crate is organized into several layers:
34//!
35//! - **Transport Layer** ([`transport`]): Abstracts low-level communication
36//!   (USB, Bluetooth, etc.). See [`transport::Transport`] trait.
37//!
38//! - **Device Layer** ([`device`]): Defines the unified [`device::Spectrometer`]
39//!   trait that all device implementations must follow.
40//!
41//! - **Device Implementations**: Concrete drivers like [`munki::Munki`] that
42//!   implement the [`device::Spectrometer`] trait.
43//!
44//! - **Colorimetry** ([`colorimetry`], [`spectrum`]): Color science utilities
45//!   for converting spectral data to various color spaces.
46
47use rusb::{Context, UsbContext};
48use thiserror::Error;
49
50// ============================================================================
51// Error Types
52// ============================================================================
53
54/// The error type for spectrometer operations.
55#[derive(Error, Debug)]
56pub enum SpectroError {
57    /// USB communication error.
58    #[error("USB Communication Error: {0}")]
59    Usb(#[from] rusb::Error),
60
61    /// Calibration-related error.
62    #[error("Calibration Error: {0}")]
63    Calibration(String),
64
65    /// General device error.
66    #[error("Device Error: {0}")]
67    Device(String),
68
69    /// Measurement mode mismatch.
70    #[error("Mode Mismatch: {0}")]
71    Mode(String),
72}
73
74/// A specialized [`Result`] type for spectrometer operations.
75pub type Result<T> = std::result::Result<T, SpectroError>;
76
77// ============================================================================
78// Constants
79// ============================================================================
80
81/// Standard wavelength bands (380nm - 780nm in 10nm steps).
82pub const WAVELENGTHS: [f32; 41] = [
83    380.0, 390.0, 400.0, 410.0, 420.0, 430.0, 440.0, 450.0, 460.0, 470.0, 480.0, 490.0, 500.0,
84    510.0, 520.0, 530.0, 540.0, 550.0, 560.0, 570.0, 580.0, 590.0, 600.0, 610.0, 620.0, 630.0,
85    640.0, 650.0, 660.0, 670.0, 680.0, 690.0, 700.0, 710.0, 720.0, 730.0, 740.0, 750.0, 760.0,
86    770.0, 780.0,
87];
88
89// ============================================================================
90// Public Modules
91// ============================================================================
92
93pub mod cam02;
94pub mod colorimetry;
95pub mod device;
96pub mod i18n;
97pub mod icc;
98pub mod munki;
99pub mod persistence;
100pub mod spectrum;
101pub mod sprague;
102pub mod tm30;
103pub mod tm30_data;
104pub mod tm30_data_cmf;
105pub mod transport;
106
107// ============================================================================
108// Re-exports for convenient API
109// ============================================================================
110
111pub use device::{BoxedSpectrometer, DeviceInfo, DevicePosition, DeviceStatus, Spectrometer};
112pub use spectrum::{MeasurementMode as SpectrumMeasurementMode, SpectralData};
113pub use transport::{Transport, UsbTransport};
114
115// ============================================================================
116// Types
117// ============================================================================
118
119/// Specifies the type of measurement to perform.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
121pub enum MeasurementMode {
122    /// Reflective measurement (paper, prints, materials).
123    /// Requires prior calibration with white tile.
124    Reflective,
125
126    /// Emissive measurement (displays, monitors).
127    /// Uses internal emissive matrix; no calibration required.
128    Emissive,
129
130    /// Ambient light measurement.
131    /// Requires the diffuser attachment to be in place.
132    Ambient,
133}
134
135/// Standard CIE Illuminants.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
137pub enum Illuminant {
138    D50,
139    D55,
140    D65,
141    D75,
142    A,
143    F2,
144    F7,
145    F11,
146}
147
148/// Standard CIE Observers.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
150pub enum Observer {
151    /// CIE 1931 2° Standard Observer (Small field of view)
152    CIE1931_2,
153    /// CIE 1964 10° Supplementary Standard Observer (Large field of view > 4°)
154    CIE1964_10,
155}
156
157// ============================================================================
158// Discovery API
159// ============================================================================
160
161/// ColorMunki USB Vendor IDs.
162const MUNKI_VIDS: [u16; 2] = [0x0765, 0x0971];
163/// ColorMunki USB Product ID.
164const MUNKI_PID: u16 = 0x2007;
165
166/// Discovers and connects to the first available spectrometer.
167///
168/// This function scans USB devices for supported spectrometers and returns
169/// a boxed [`Spectrometer`] trait object.
170///
171/// # Example
172///
173/// ```ignore
174/// use spectro_rs::{discover, MeasurementMode};
175///
176/// let mut device = discover()?;
177/// let spectrum = device.measure(MeasurementMode::Emissive)?;
178/// ```
179///
180/// # Errors
181///
182/// Returns an error if no supported device is found, or if the device
183/// cannot be opened/initialized.
184pub fn discover() -> Result<BoxedSpectrometer> {
185    let context = Context::new()?;
186    discover_with_context(&context)
187}
188
189/// Discovers a spectrometer using a provided USB context.
190///
191/// This is useful if you need more control over USB enumeration or want
192/// to reuse an existing context.
193pub fn discover_with_context<T: UsbContext + 'static>(
194    context: &T,
195) -> Result<Box<dyn Spectrometer + Send>> {
196    let devices = context.devices()?;
197
198    for device in devices.iter() {
199        let desc = device.device_descriptor()?;
200        let vid = desc.vendor_id();
201        let pid = desc.product_id();
202
203        if MUNKI_VIDS.contains(&vid) && pid == MUNKI_PID {
204            let handle = device.open()?;
205            handle.claim_interface(0)?;
206
207            let transport = transport::UsbTransport::new(handle);
208            let munki = munki::Munki::new(transport)?;
209
210            return Ok(Box::new(munki));
211        }
212    }
213
214    Err(SpectroError::Device(
215        "No supported spectrometer found. Ensure device is connected and drivers are installed."
216            .into(),
217    ))
218}
219
220/// Lists all detected spectrometer devices without connecting.
221///
222/// Returns a vector of (vendor_id, product_id, model_name) tuples.
223pub fn list_devices() -> Result<Vec<(u16, u16, &'static str)>> {
224    let context = Context::new()?;
225    let devices = context.devices()?;
226    let mut found = Vec::new();
227
228    for device in devices.iter() {
229        if let Ok(desc) = device.device_descriptor() {
230            let vid = desc.vendor_id();
231            let pid = desc.product_id();
232
233            if MUNKI_VIDS.contains(&vid) && pid == MUNKI_PID {
234                found.push((vid, pid, "ColorMunki"));
235            }
236            // Future: Add detection for i1Display Pro, Spyder, etc.
237        }
238    }
239
240    Ok(found)
241}