use crate::errors::CameraError;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrabCameraConfig {
pub camera: CameraConfig,
pub quality: QualityConfig,
pub storage: StorageConfig,
pub advanced: AdvancedConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CameraConfig {
pub default_resolution: [u32; 2],
pub default_fps: u32,
pub auto_reconnect: bool,
pub reconnect_attempts: u32,
pub reconnect_delay_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QualityConfig {
pub auto_retry_enabled: bool,
pub max_retry_attempts: u32,
pub min_blur_threshold: f32,
pub min_exposure_score: f32,
pub min_overall_score: f32,
pub retry_delay_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub output_directory: String,
pub auto_organize_by_date: bool,
pub date_format: String,
pub default_format: String,
pub jpeg_quality: u8,
pub auto_delete_low_quality: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdvancedConfig {
pub focus_stacking_enabled: bool,
pub focus_stack_steps: u32,
pub hdr_enabled: bool,
pub hdr_brackets: u32,
}
impl Default for CrabCameraConfig {
fn default() -> Self {
Self {
camera: CameraConfig {
default_resolution: [1920, 1080],
default_fps: 30,
auto_reconnect: true,
reconnect_attempts: 3,
reconnect_delay_ms: 1000,
},
quality: QualityConfig {
auto_retry_enabled: true,
max_retry_attempts: 10,
min_blur_threshold: 0.7,
min_exposure_score: 0.6,
min_overall_score: 0.7,
retry_delay_ms: 100,
},
storage: StorageConfig {
output_directory: "./captures".to_string(),
auto_organize_by_date: true,
date_format: "YYYY-MM-DD".to_string(),
default_format: "jpeg".to_string(),
jpeg_quality: 95,
auto_delete_low_quality: false,
},
advanced: AdvancedConfig {
focus_stacking_enabled: false,
focus_stack_steps: 10,
hdr_enabled: false,
hdr_brackets: 3,
},
}
}
}
impl CrabCameraConfig {
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, CameraError> {
let path = path.as_ref();
if !path.exists() {
log::info!("Config file not found at {:?}, using defaults", path);
return Ok(Self::default());
}
let contents = fs::read_to_string(path).map_err(|e| {
CameraError::InitializationError(format!("Failed to read config file: {}", e))
})?;
let config: CrabCameraConfig = toml::from_str(&contents).map_err(|e| {
CameraError::InitializationError(format!("Failed to parse config file: {}", e))
})?;
log::info!("Loaded configuration from {:?}", path);
Ok(config)
}
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), CameraError> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
CameraError::InitializationError(format!(
"Failed to create config directory: {}",
e
))
})?;
}
let toml_string = toml::to_string_pretty(self).map_err(|e| {
CameraError::InitializationError(format!("Failed to serialize config: {}", e))
})?;
fs::write(path, toml_string).map_err(|e| {
CameraError::InitializationError(format!("Failed to write config file: {}", e))
})?;
log::info!("Saved configuration to {:?}", path);
Ok(())
}
pub fn default_path() -> PathBuf {
PathBuf::from("crabcamera.toml")
}
pub fn load_or_default() -> Self {
Self::load_from_file(Self::default_path()).unwrap_or_else(|e| {
log::warn!("Failed to load config, using defaults: {}", e);
Self::default()
})
}
pub fn validate(&self) -> Result<(), String> {
if self.camera.default_resolution[0] == 0 || self.camera.default_resolution[1] == 0 {
return Err("Invalid default resolution".to_string());
}
if self.camera.default_fps == 0 || self.camera.default_fps > 240 {
return Err("Invalid default FPS (must be 1-240)".to_string());
}
if !(0.0..=1.0).contains(&self.quality.min_blur_threshold) {
return Err("Blur threshold must be between 0.0 and 1.0".to_string());
}
if !(0.0..=1.0).contains(&self.quality.min_exposure_score) {
return Err("Exposure score must be between 0.0 and 1.0".to_string());
}
if !(0.0..=1.0).contains(&self.quality.min_overall_score) {
return Err("Overall score must be between 0.0 and 1.0".to_string());
}
if self.storage.jpeg_quality == 0 || self.storage.jpeg_quality > 100 {
return Err("JPEG quality must be between 1 and 100".to_string());
}
if self.advanced.focus_stack_steps == 0 || self.advanced.focus_stack_steps > 100 {
return Err("Focus stack steps must be between 1 and 100".to_string());
}
if self.advanced.hdr_brackets == 0 || self.advanced.hdr_brackets > 10 {
return Err("HDR brackets must be between 1 and 10".to_string());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CrabCameraConfig::default();
assert_eq!(config.camera.default_resolution, [1920, 1080]);
assert_eq!(config.camera.default_fps, 30);
assert!(config.quality.auto_retry_enabled);
}
#[test]
fn test_config_validation() {
let config = CrabCameraConfig::default();
assert!(config.validate().is_ok());
let mut bad_config = config.clone();
bad_config.camera.default_resolution = [0, 0];
assert!(bad_config.validate().is_err());
let mut bad_quality = CrabCameraConfig::default();
bad_quality.quality.min_blur_threshold = 1.5;
assert!(bad_quality.validate().is_err());
}
#[test]
fn test_config_save_and_load() {
let temp_dir = std::env::temp_dir();
let config_path = temp_dir.join("test_crabcamera.toml");
let _ = fs::remove_file(&config_path);
let config = CrabCameraConfig::default();
assert!(config.save_to_file(&config_path).is_ok());
let loaded = CrabCameraConfig::load_from_file(&config_path).unwrap();
assert_eq!(loaded.camera.default_fps, config.camera.default_fps);
assert_eq!(
loaded.quality.max_retry_attempts,
config.quality.max_retry_attempts
);
let _ = fs::remove_file(&config_path);
}
#[test]
fn test_config_toml_format() {
let config = CrabCameraConfig::default();
let toml_string = toml::to_string_pretty(&config).unwrap();
assert!(toml_string.contains("[camera]"));
assert!(toml_string.contains("[quality]"));
assert!(toml_string.contains("[storage]"));
assert!(toml_string.contains("[advanced]"));
assert!(toml_string.contains("default_resolution"));
assert!(toml_string.contains("auto_retry_enabled"));
}
#[test]
fn test_load_nonexistent_file() {
let result = CrabCameraConfig::load_from_file("nonexistent_file.toml");
assert!(result.is_ok()); assert_eq!(result.unwrap().camera.default_fps, 30);
}
}