use crate::{ENV_IOT_MODE, ENV_PUBLISH_INTERVAL};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PowerMode {
Full,
Balanced,
Low,
Critical,
}
impl Default for PowerMode {
fn default() -> Self {
Self::Balanced
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TransportConfig {
Memory,
Quic {
bind_addr: String,
port: u16,
},
Coap {
bind_addr: String,
port: u16,
},
Mesh {
mode: MeshMode,
},
#[cfg(feature = "webrtc")]
WebRtc {
stun_server: String,
turn_server: Option<String>,
signaling_port: u16,
},
#[cfg(feature = "ble")]
Ble {
device_name: String,
mesh_relay: bool,
tx_power: i8,
},
}
impl Default for TransportConfig {
fn default() -> Self {
Self::Coap {
bind_addr: "0.0.0.0".to_string(),
port: 5683,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum MeshMode {
WiFiDirect,
BluetoothLE,
LoRa,
Auto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GossipConfig {
pub loop_delay: Duration,
pub success_delay: Duration,
pub error_delay: Duration,
pub output_target_mbps: f64,
pub max_peers: usize,
}
impl Default for GossipConfig {
fn default() -> Self {
Self {
loop_delay: Duration::from_millis(1000),
success_delay: Duration::from_secs(60),
error_delay: Duration::from_secs(300),
output_target_mbps: 0.5,
max_peers: 8,
}
}
}
impl GossipConfig {
pub fn iot_mode() -> Self {
Self {
loop_delay: Duration::from_millis(100),
success_delay: Duration::from_secs(5),
error_delay: Duration::from_secs(30),
output_target_mbps: 5.0,
max_peers: 4,
}
}
pub fn low_power() -> Self {
Self {
loop_delay: Duration::from_millis(5000),
success_delay: Duration::from_secs(300),
error_delay: Duration::from_secs(600),
output_target_mbps: 0.1,
max_peers: 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StorageBackendType {
Sqlite,
Rocksdb,
Memory,
}
impl Default for StorageBackendType {
fn default() -> Self {
Self::Sqlite
}
}
impl std::fmt::Display for StorageBackendType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sqlite => write!(f, "sqlite"),
Self::Rocksdb => write!(f, "rocksdb"),
Self::Memory => write!(f, "memory"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub backend: StorageBackendType,
pub db_path: String,
pub max_size: usize,
pub aggressive_pruning: bool,
pub keep_recent: usize,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
backend: StorageBackendType::default(),
db_path: "./aingle_data.db".to_string(),
max_size: 5 * 1024 * 1024, aggressive_pruning: true,
keep_recent: 1000,
}
}
}
impl StorageConfig {
pub fn sqlite(path: &str) -> Self {
Self {
backend: StorageBackendType::Sqlite,
db_path: path.to_string(),
..Default::default()
}
}
pub fn rocksdb(path: &str) -> Self {
Self {
backend: StorageBackendType::Rocksdb,
db_path: path.to_string(),
max_size: 100 * 1024 * 1024, aggressive_pruning: false,
keep_recent: 100_000,
}
}
pub fn memory() -> Self {
Self {
backend: StorageBackendType::Memory,
db_path: ":memory:".to_string(),
max_size: 10 * 1024 * 1024,
aggressive_pruning: false,
keep_recent: 10_000,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub node_id: Option<String>,
pub publish_interval: Duration,
pub power_mode: PowerMode,
pub transport: TransportConfig,
pub gossip: GossipConfig,
pub storage: StorageConfig,
pub memory_limit: usize,
pub enable_metrics: bool,
pub enable_mdns: bool,
pub log_level: String,
}
impl Default for Config {
fn default() -> Self {
Self {
node_id: None,
publish_interval: Duration::from_secs(5),
power_mode: PowerMode::Balanced,
transport: TransportConfig::default(),
gossip: GossipConfig::default(),
storage: StorageConfig::default(),
memory_limit: 512 * 1024, enable_metrics: false,
enable_mdns: true, log_level: "info".to_string(),
}
}
}
impl Config {
pub fn iot_mode() -> Self {
Self {
node_id: None,
publish_interval: Duration::ZERO, power_mode: PowerMode::Full,
transport: TransportConfig::Coap {
bind_addr: "0.0.0.0".to_string(),
port: 5683,
},
gossip: GossipConfig::iot_mode(),
storage: StorageConfig {
backend: StorageBackendType::Sqlite,
db_path: "./aingle_iot.db".to_string(),
max_size: 1024 * 1024, aggressive_pruning: true,
keep_recent: 100,
},
memory_limit: 256 * 1024, enable_metrics: false,
enable_mdns: true, log_level: "warn".to_string(),
}
}
pub fn low_power() -> Self {
Self {
node_id: None,
publish_interval: Duration::from_secs(30),
power_mode: PowerMode::Low,
transport: TransportConfig::Coap {
bind_addr: "0.0.0.0".to_string(),
port: 5683,
},
gossip: GossipConfig::low_power(),
storage: StorageConfig {
backend: StorageBackendType::Sqlite,
db_path: "./aingle_lowpower.db".to_string(),
max_size: 512 * 1024, aggressive_pruning: true,
keep_recent: 50,
},
memory_limit: 128 * 1024, enable_metrics: false,
enable_mdns: false, log_level: "error".to_string(),
}
}
pub fn production(db_path: &str) -> Self {
Self {
node_id: None,
publish_interval: Duration::from_millis(100),
power_mode: PowerMode::Full,
transport: TransportConfig::Quic {
bind_addr: "0.0.0.0".to_string(),
port: 19081,
},
gossip: GossipConfig::default(),
storage: StorageConfig::rocksdb(db_path),
memory_limit: 512 * 1024 * 1024, enable_metrics: true,
enable_mdns: true, log_level: "info".to_string(),
}
}
pub fn from_env() -> Self {
let mut config = if std::env::var(ENV_IOT_MODE).is_ok() {
Self::iot_mode()
} else {
Self::default()
};
if let Ok(interval_str) = std::env::var(ENV_PUBLISH_INTERVAL) {
if let Ok(interval_ms) = interval_str.parse::<u64>() {
config.publish_interval = Duration::from_millis(interval_ms);
}
}
if let Ok(delay_str) = std::env::var("AINGLE_GOSSIP_LOOP_ITERATION_DELAY_MS") {
if let Ok(delay_ms) = delay_str.parse::<u64>() {
config.gossip.loop_delay = Duration::from_millis(delay_ms);
}
}
if let Ok(limit_str) = std::env::var("AINGLE_MEMORY_LIMIT_KB") {
if let Ok(limit_kb) = limit_str.parse::<usize>() {
config.memory_limit = limit_kb * 1024;
}
}
config
}
pub fn test_mode() -> Self {
Self {
node_id: None,
publish_interval: Duration::from_millis(10),
power_mode: PowerMode::Full,
transport: TransportConfig::Memory,
gossip: GossipConfig {
loop_delay: Duration::from_millis(10),
success_delay: Duration::from_millis(50),
error_delay: Duration::from_millis(100),
output_target_mbps: 10.0,
max_peers: 5,
},
storage: StorageConfig::memory(),
memory_limit: 64 * 1024, enable_metrics: false,
enable_mdns: false,
log_level: "debug".to_string(),
}
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.memory_limit < 64 * 1024 {
return Err(ConfigError::MemoryTooLow(self.memory_limit));
}
if self.storage.max_size < 256 * 1024 {
return Err(ConfigError::StorageTooLow(self.storage.max_size));
}
Ok(())
}
}
#[derive(Debug)]
pub enum ConfigError {
MemoryTooLow(usize),
StorageTooLow(usize),
Invalid(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::MemoryTooLow(size) => {
write!(f, "Memory limit too low: {} bytes (minimum 64KB)", size)
}
ConfigError::StorageTooLow(size) => {
write!(f, "Storage limit too low: {} bytes (minimum 256KB)", size)
}
ConfigError::Invalid(msg) => write!(f, "Invalid configuration: {}", msg),
}
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.validate().is_ok());
}
#[test]
fn test_iot_config() {
let config = Config::iot_mode();
assert_eq!(config.publish_interval, Duration::ZERO);
assert!(config.validate().is_ok());
}
#[test]
fn test_low_power_config() {
let config = Config::low_power();
assert_eq!(config.power_mode, PowerMode::Low);
assert!(config.validate().is_ok());
}
#[test]
fn test_production_config() {
let config = Config::production("./test_db");
assert!(config.validate().is_ok());
}
#[test]
fn test_power_mode_default() {
let mode: PowerMode = Default::default();
assert_eq!(mode, PowerMode::Balanced);
}
#[test]
fn test_power_mode_variants() {
let modes = vec![
PowerMode::Full,
PowerMode::Balanced,
PowerMode::Low,
PowerMode::Critical,
];
for mode in modes {
let debug_str = format!("{:?}", mode);
assert!(!debug_str.is_empty());
}
}
#[test]
fn test_transport_config_default() {
let config: TransportConfig = Default::default();
if let TransportConfig::Coap { bind_addr, port } = config {
assert_eq!(bind_addr, "0.0.0.0");
assert_eq!(port, 5683);
} else {
panic!("Expected Coap transport");
}
}
#[test]
fn test_gossip_config_default() {
let config = GossipConfig::default();
assert!(config.loop_delay > Duration::ZERO);
assert!(config.max_peers > 0);
}
#[test]
fn test_gossip_config_iot() {
let config = GossipConfig::iot_mode();
assert!(config.loop_delay < Duration::from_secs(1));
}
#[test]
fn test_gossip_config_low_power() {
let config = GossipConfig::low_power();
assert!(config.loop_delay > Duration::from_secs(1));
}
#[test]
fn test_storage_backend_type_default() {
let backend: StorageBackendType = Default::default();
assert_eq!(backend, StorageBackendType::Sqlite);
}
#[test]
fn test_storage_backend_type_display() {
assert_eq!(StorageBackendType::Sqlite.to_string(), "sqlite");
assert_eq!(StorageBackendType::Rocksdb.to_string(), "rocksdb");
assert_eq!(StorageBackendType::Memory.to_string(), "memory");
}
#[test]
fn test_storage_config_default() {
let config = StorageConfig::default();
assert_eq!(config.backend, StorageBackendType::Sqlite);
assert!(config.max_size > 0);
assert!(config.aggressive_pruning);
}
#[test]
fn test_storage_config_sqlite() {
let config = StorageConfig::sqlite("./test.db");
assert_eq!(config.backend, StorageBackendType::Sqlite);
assert_eq!(config.db_path, "./test.db");
}
#[test]
fn test_storage_config_rocksdb() {
let config = StorageConfig::rocksdb("./rocksdb");
assert_eq!(config.backend, StorageBackendType::Rocksdb);
assert_eq!(config.db_path, "./rocksdb");
assert!(!config.aggressive_pruning);
assert!(config.max_size > StorageConfig::default().max_size);
}
#[test]
fn test_storage_config_memory() {
let config = StorageConfig::memory();
assert_eq!(config.backend, StorageBackendType::Memory);
assert_eq!(config.db_path, ":memory:");
}
#[test]
fn test_mesh_mode_variants() {
let modes = vec![MeshMode::WiFiDirect, MeshMode::BluetoothLE, MeshMode::LoRa];
for mode in modes {
let debug_str = format!("{:?}", mode);
assert!(!debug_str.is_empty());
}
}
#[test]
fn test_config_from_env_no_env() {
let config = Config::from_env();
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validate_invalid_memory() {
let mut config = Config::default();
config.memory_limit = 100; assert!(config.validate().is_err());
}
#[test]
fn test_config_validate_invalid_storage() {
let mut config = Config::default();
config.storage.max_size = 100; assert!(config.validate().is_err());
}
#[test]
fn test_power_mode_serialization() {
let mode = PowerMode::Low;
let json = serde_json::to_string(&mode).unwrap();
let parsed: PowerMode = serde_json::from_str(&json).unwrap();
assert_eq!(mode, parsed);
}
#[test]
fn test_storage_backend_type_serialization() {
let backend = StorageBackendType::Rocksdb;
let json = serde_json::to_string(&backend).unwrap();
assert!(json.contains("rocksdb"));
let parsed: StorageBackendType = serde_json::from_str(&json).unwrap();
assert_eq!(backend, parsed);
}
#[test]
fn test_gossip_config_serialization() {
let config = GossipConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: GossipConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.max_peers, parsed.max_peers);
}
#[test]
fn test_storage_config_serialization() {
let config = StorageConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: StorageConfig = serde_json::from_str(&json).unwrap();
assert_eq!(config.db_path, parsed.db_path);
}
#[test]
fn test_transport_config_serialization() {
let config = TransportConfig::Coap {
bind_addr: "0.0.0.0".to_string(),
port: 5683,
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("Coap"));
}
#[test]
fn test_config_error_display() {
let error = ConfigError::MemoryTooLow(100);
let display = error.to_string();
assert!(display.contains("100"));
let error = ConfigError::StorageTooLow(256);
let display = error.to_string();
assert!(display.contains("256"));
let error = ConfigError::Invalid("bad value".to_string());
let display = error.to_string();
assert!(display.contains("bad value"));
}
#[test]
fn test_gossip_config_clone() {
let config = GossipConfig::iot_mode();
let cloned = config.clone();
assert_eq!(config.loop_delay, cloned.loop_delay);
assert_eq!(config.max_peers, cloned.max_peers);
}
#[test]
fn test_storage_config_clone() {
let config = StorageConfig::rocksdb("./test");
let cloned = config.clone();
assert_eq!(config.db_path, cloned.db_path);
assert_eq!(config.backend, cloned.backend);
}
#[test]
fn test_config_clone() {
let config = Config::iot_mode();
let cloned = config.clone();
assert_eq!(config.power_mode, cloned.power_mode);
assert_eq!(config.enable_mdns, cloned.enable_mdns);
}
#[test]
fn test_config_fields() {
let config = Config::default();
assert!(config.memory_limit > 0);
assert!(config.node_id.is_none() || !config.node_id.as_ref().unwrap().is_empty());
}
}