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}