Skip to main content

glint_mask_tools/config/
sensor_config.rs

1/// Configuration file management for sensor definitions.
2///
3/// This module provides functionality to load sensor configurations
4/// from TOML files, allowing users to define custom sensors without
5/// modifying the library code.
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10use crate::core::sensor::{Band, Sensor, SensorRegistry};
11use crate::error::{GlintError, Result};
12
13/// Configuration file format for sensor definitions
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SensorConfigFile {
16    /// Version of the configuration file format
17    pub version: String,
18    /// List of sensor definitions
19    pub sensors: Vec<SensorConfig>,
20}
21
22/// Sensor configuration as defined in a config file
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct SensorConfig {
25    /// Sensor ID
26    pub id: String,
27    /// Human-readable name
28    pub name: String,
29    /// Sensor description
30    pub description: Option<String>,
31    /// Bit depth
32    pub bit_depth: u8,
33    /// Loader type
34    pub loader_type: String,
35    /// Band definitions
36    pub bands: Vec<BandConfig>,
37    /// Loader-specific configuration
38    pub loader_config: Option<HashMap<String, String>>,
39}
40
41/// Band configuration as defined in a config file
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct BandConfig {
44    /// Band name
45    pub name: String,
46    /// Default threshold value
47    pub default_threshold: f64,
48    /// Optional wavelength in nanometers
49    pub wavelength: Option<f64>,
50    /// Optional description
51    pub description: Option<String>,
52}
53
54impl From<BandConfig> for Band {
55    fn from(config: BandConfig) -> Self {
56        Band {
57            name: config.name,
58            default_threshold: config.default_threshold,
59            wavelength: config.wavelength,
60            description: config.description,
61        }
62    }
63}
64
65impl From<Band> for BandConfig {
66    fn from(band: Band) -> Self {
67        BandConfig {
68            name: band.name,
69            default_threshold: band.default_threshold,
70            wavelength: band.wavelength,
71            description: band.description,
72        }
73    }
74}
75
76impl From<SensorConfig> for Sensor {
77    fn from(config: SensorConfig) -> Self {
78        let bands: Vec<Band> = config.bands.into_iter().map(Band::from).collect();
79        let mut sensor = Sensor::new(
80            config.id,
81            config.name,
82            bands,
83            config.bit_depth,
84            config.loader_type,
85        );
86
87        if let Some(description) = config.description {
88            sensor = sensor.with_description(description);
89        }
90
91        if let Some(loader_config) = config.loader_config {
92            for (key, value) in loader_config {
93                sensor = sensor.with_loader_config(key, value);
94            }
95        }
96
97        sensor
98    }
99}
100
101impl From<Sensor> for SensorConfig {
102    fn from(sensor: Sensor) -> Self {
103        let bands: Vec<BandConfig> = sensor.bands.into_iter().map(BandConfig::from).collect();
104        let loader_config = if sensor.loader_config.is_empty() {
105            None
106        } else {
107            Some(sensor.loader_config)
108        };
109
110        SensorConfig {
111            id: sensor.id,
112            name: sensor.name,
113            description: sensor.description,
114            bit_depth: sensor.bit_depth,
115            loader_type: sensor.loader_type,
116            bands,
117            loader_config,
118        }
119    }
120}
121
122/// Configuration manager for handling sensor configuration files
123#[derive(Debug)]
124pub struct ConfigManager {
125    /// Default configuration directory
126    config_dir: Option<PathBuf>,
127}
128
129impl ConfigManager {
130    /// Create a new configuration manager
131    pub fn new() -> Self {
132        Self { config_dir: None }
133    }
134
135    /// Create a configuration manager with a specific config directory
136    pub fn with_config_dir(config_dir: PathBuf) -> Self {
137        Self {
138            config_dir: Some(config_dir),
139        }
140    }
141
142    /// Get the default configuration directory
143    pub fn default_config_dir() -> Result<PathBuf> {
144        // Try to find a reasonable default config directory
145        if let Some(config_dir) = dirs::config_dir() {
146            Ok(config_dir.join("glint-mask-tools"))
147        } else {
148            // Fallback to current directory
149            Ok(PathBuf::from("."))
150        }
151    }
152
153    /// Get the configuration directory to use
154    pub fn get_config_dir(&self) -> Result<PathBuf> {
155        if let Some(ref dir) = self.config_dir {
156            Ok(dir.clone())
157        } else {
158            Self::default_config_dir()
159        }
160    }
161
162    /// Load sensor configurations from a TOML file
163    pub fn load_from_file(&self, path: &Path) -> Result<Vec<Sensor>> {
164        let content = std::fs::read_to_string(path).map_err(GlintError::Io)?;
165
166        let config_file: SensorConfigFile =
167            toml::from_str(&content).map_err(GlintError::Deserialization)?;
168
169        // Validate version (for future compatibility)
170        if config_file.version != "1.0" {
171            return Err(GlintError::config(format!(
172                "Unsupported configuration file version: {}",
173                config_file.version
174            )));
175        }
176
177        // Convert to sensors and validate
178        let mut sensors = Vec::new();
179        for sensor_config in config_file.sensors {
180            let sensor = Sensor::from(sensor_config);
181            sensor.validate()?;
182            sensors.push(sensor);
183        }
184
185        Ok(sensors)
186    }
187
188    /// Save sensor configurations to a TOML file
189    pub fn save_to_file(&self, sensors: &[Sensor], path: &Path) -> Result<()> {
190        let sensor_configs: Vec<SensorConfig> = sensors
191            .iter()
192            .map(|s| SensorConfig::from(s.clone()))
193            .collect();
194
195        let config_file = SensorConfigFile {
196            version: "1.0".to_string(),
197            sensors: sensor_configs,
198        };
199
200        let content = toml::to_string_pretty(&config_file).map_err(GlintError::Serialization)?;
201
202        // Ensure parent directory exists
203        if let Some(parent) = path.parent() {
204            std::fs::create_dir_all(parent).map_err(GlintError::Io)?;
205        }
206
207        std::fs::write(path, content).map_err(GlintError::Io)?;
208
209        Ok(())
210    }
211
212    /// Load sensors from the default configuration file
213    pub fn load_default_config(&self) -> Result<Vec<Sensor>> {
214        let config_dir = self.get_config_dir()?;
215        let config_path = config_dir.join("sensors.toml");
216
217        if config_path.exists() {
218            self.load_from_file(&config_path)
219        } else {
220            // Return empty vector if no config file exists
221            Ok(Vec::new())
222        }
223    }
224
225    /// Save sensors to the default configuration file
226    pub fn save_default_config(&self, sensors: &[Sensor]) -> Result<()> {
227        let config_dir = self.get_config_dir()?;
228        let config_path = config_dir.join("sensors.toml");
229        self.save_to_file(sensors, &config_path)
230    }
231
232    /// Create a sample configuration file with default sensors
233    pub fn create_sample_config(&self, path: &Path) -> Result<()> {
234        let registry = crate::core::sensor::SensorRegistry::with_defaults();
235        let default_sensors: Vec<_> = registry.sensors().into_iter().cloned().collect();
236        self.save_to_file(&default_sensors, path)
237    }
238
239    /// Load sensors into a registry from configuration files
240    pub fn load_into_registry(&self, registry: &mut SensorRegistry) -> Result<()> {
241        // Try to load from default config
242        match self.load_default_config() {
243            Ok(sensors) => {
244                for sensor in sensors {
245                    registry.register_sensor(sensor)?;
246                }
247            }
248            Err(GlintError::Io(_)) => {
249                // Config file doesn't exist, which is fine
250            }
251            Err(e) => return Err(e),
252        }
253
254        Ok(())
255    }
256}
257
258impl Default for ConfigManager {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::core::sensor::bands;
268    use tempfile::tempdir;
269
270    #[test]
271    fn test_sensor_config_conversion() {
272        let sensor = Sensor::new(
273            "test",
274            "Test Sensor",
275            vec![bands::red(), bands::green(), bands::blue()],
276            8,
277            "single_file",
278        )
279        .with_description("Test description");
280
281        // Convert to config and back
282        let config = SensorConfig::from(sensor.clone());
283        let converted_sensor = Sensor::from(config);
284
285        assert_eq!(sensor.id, converted_sensor.id);
286        assert_eq!(sensor.name, converted_sensor.name);
287        assert_eq!(sensor.description, converted_sensor.description);
288        assert_eq!(sensor.bit_depth, converted_sensor.bit_depth);
289        assert_eq!(sensor.loader_type, converted_sensor.loader_type);
290        assert_eq!(sensor.bands.len(), converted_sensor.bands.len());
291    }
292
293    #[test]
294    fn test_config_file_save_load() {
295        let temp_dir = tempdir().unwrap();
296        let config_path = temp_dir.path().join("sensors.toml");
297
298        let registry = crate::core::sensor::SensorRegistry::with_defaults();
299        let sensors = vec![
300            registry.get_sensor("rgb").unwrap().clone(),
301            registry.get_sensor("p4ms").unwrap().clone(),
302        ];
303
304        let manager = ConfigManager::new();
305
306        // Save configuration
307        manager.save_to_file(&sensors, &config_path).unwrap();
308        assert!(config_path.exists());
309
310        // Load configuration
311        let loaded_sensors = manager.load_from_file(&config_path).unwrap();
312        assert_eq!(loaded_sensors.len(), 2);
313
314        // Verify sensor properties
315        assert_eq!(loaded_sensors[0].id, "rgb");
316        assert_eq!(loaded_sensors[1].id, "p4ms");
317    }
318
319    #[test]
320    fn test_config_manager_with_directory() {
321        let temp_dir = tempdir().unwrap();
322        let manager = ConfigManager::with_config_dir(temp_dir.path().to_path_buf());
323
324        let registry = crate::core::sensor::SensorRegistry::with_defaults();
325        let sensors = vec![registry.get_sensor("rgb").unwrap().clone()];
326
327        // Save and load default config
328        manager.save_default_config(&sensors).unwrap();
329        let loaded_sensors = manager.load_default_config().unwrap();
330
331        assert_eq!(loaded_sensors.len(), 1);
332        assert_eq!(loaded_sensors[0].id, "rgb");
333    }
334
335    #[test]
336    fn test_sample_config_creation() {
337        let temp_dir = tempdir().unwrap();
338        let sample_path = temp_dir.path().join("sample_sensors.toml");
339
340        let manager = ConfigManager::new();
341        manager.create_sample_config(&sample_path).unwrap();
342
343        assert!(sample_path.exists());
344
345        // Verify we can load the sample config
346        let sensors = manager.load_from_file(&sample_path).unwrap();
347        assert_eq!(sensors.len(), 6); // Should have all default sensors
348    }
349
350    #[test]
351    fn test_registry_integration() {
352        let temp_dir = tempdir().unwrap();
353        let manager = ConfigManager::with_config_dir(temp_dir.path().to_path_buf());
354
355        // Save some sensors to config
356        let registry = crate::core::sensor::SensorRegistry::with_defaults();
357        let sensors = vec![
358            registry.get_sensor("rgb").unwrap().clone(),
359            registry.get_sensor("m3m").unwrap().clone(),
360        ];
361        manager.save_default_config(&sensors).unwrap();
362
363        // Load into registry
364        let mut registry = SensorRegistry::new();
365        manager.load_into_registry(&mut registry).unwrap();
366
367        assert!(registry.has_sensor("rgb"));
368        assert!(registry.has_sensor("m3m"));
369        assert!(!registry.has_sensor("p4ms")); // Not in our config
370    }
371}