settings.rs
1 use anyhow::Result; 2 use esp_idf_svc::nvs::{EspNvs, EspNvsPartition, NvsDefault}; 3 use log::{error, info}; 4 use serde::{Deserialize, Serialize}; 5 use std::sync::Mutex; 6 7 use crate::network::auth; 8 9 // ============================================================================= 10 // SYSTEM CONSTANTS (Compile-time, not user-configurable) 11 // ============================================================================= 12 13 // Admin credentials for web interface (loaded at compile-time from .env or defaults) 14 pub const ADMIN_USERNAME: &str = match option_env!("ADMIN_USERNAME") { 15 Some(v) => v, 16 None => "admin", 17 }; 18 pub const ADMIN_PASSWORD: &str = match option_env!("ADMIN_PASSWORD") { 19 Some(v) => v, 20 None => "admin", 21 }; 22 23 // Timing constants 24 pub const SENSOR_READ_INTERVAL_MS: u64 = 2000; // Read every 2 seconds 25 pub const BME680_MEASUREMENT_DELAY_MS: u32 = 150; // BME680 measurement delay with gas heater stability 26 pub const WIFI_CHECK_INTERVAL_SECS: i64 = 30; // Check WiFi every 30 seconds 27 pub const MQTT_RECONNECT_DELAY_MS: u64 = 5000; // Wait 5s before MQTT reconnect 28 pub const WATCHDOG_TIMEOUT_SECS: u32 = 60; // System watchdog timeout 29 pub const OTA_CHECK_INTERVAL_SECS: i64 = 86400; // Check for OTA updates daily (24 hours) 30 pub const LED_BLINK_INTERVAL_MS: i64 = 500; // LED blink interval when WiFi disconnected (milliseconds) 31 pub const MAIN_LOOP_DELAY_MS: u32 = 50; // Main loop delay for power efficiency (50ms reduces CPU load) 32 pub const NVS_WRITE_BATCH_DELAY_SECS: u64 = 300; // Batch NVS writes (5 minutes) to reduce flash wear 33 34 // Time conversion helpers (prevent overflow in microsecond calculations) 35 #[allow(dead_code)] 36 pub const MICROS_PER_SECOND: i64 = 1_000_000; 37 #[allow(dead_code)] 38 pub const MICROS_PER_MILLISECOND: i64 = 1_000; 39 40 // Hardware configuration (informational - GPIO pins selected at compile time) 41 #[allow(dead_code)] 42 pub const STATUS_LED_GPIO: u8 = 2; // Onboard LED GPIO (usually GPIO2 on ESP32) 43 #[allow(dead_code)] 44 pub const ALARM_RELAY_GPIO: u8 = 25; // External alarm relay GPIO 45 46 // ============================================================================= 47 // NVS KEY CONSTANTS (8-character max due to ESP32 constraints) 48 // ============================================================================= 49 50 const NVS_NAMESPACE: &str = "settings"; 51 const KEY_WIFI_SSID: &str = "wifi_ssid"; 52 const KEY_WIFI_PASS: &str = "wifi_pass"; 53 const KEY_MQTT_BROKER: &str = "mqtt_broker"; 54 const KEY_PUBLISH_INT: &str = "pub_interval"; 55 56 // Multi-device relay configuration keys 57 const KEY_EXHAUST_FAN_EN: &str = "exh_fan_en"; 58 const KEY_EXHAUST_FAN_GPIO: &str = "exh_fan_gpio"; 59 const KEY_CIRC_FAN_EN: &str = "circ_fan_en"; 60 const KEY_CIRC_FAN_GPIO: &str = "circ_fan_gpio"; 61 const KEY_HEATER_EN: &str = "heater_en"; 62 const KEY_HEATER_GPIO: &str = "heater_gpio"; 63 const KEY_PURIFIER_EN: &str = "purif_en"; 64 const KEY_PURIFIER_GPIO: &str = "purif_gpio"; 65 66 // Automation keys 67 const KEY_AUTO_ENABLED: &str = "auto_en"; 68 const KEY_TEMP_HIGH_THRESH: &str = "temp_high"; 69 const KEY_TEMP_LOW_THRESH: &str = "temp_low"; 70 const KEY_HUM_HIGH_THRESH: &str = "hum_high"; 71 const KEY_GAS_THRESH: &str = "gas_thresh"; 72 73 // OTA keys 74 const KEY_OTA_URL: &str = "ota_url"; 75 const KEY_OTA_AUTO: &str = "ota_auto"; 76 77 // Alarm keys 78 const KEY_ALARM_ENABLED: &str = "alarm_en"; 79 const KEY_ALARM_WIFI_SEC: &str = "alarm_wifi"; 80 const KEY_ALARM_MQTT_SEC: &str = "alarm_mqtt"; 81 const KEY_ALARM_SENSOR_SEC: &str = "alarm_sens"; 82 const KEY_ALARM_TEMP_LOW: &str = "alarm_tlow"; 83 const KEY_ALARM_TEMP_HIGH: &str = "alarm_thigh"; 84 const KEY_ALARM_AUTO_SEC: &str = "alarm_auto"; 85 86 // Syslog keys 87 const KEY_SYSLOG_ENABLED: &str = "syslog_en"; 88 const KEY_SYSLOG_SERVER: &str = "syslog_srv"; 89 const KEY_SYSLOG_PORT: &str = "syslog_port"; 90 91 // Device identification 92 const KEY_APP_NAME: &str = "app_name"; 93 const KEY_HOSTNAME: &str = "hostname"; 94 const KEY_DEVICE_ID: &str = "device_id"; 95 96 // Backup keys 97 const KEY_BACKUP_PASSWORD: &str = "backup_pw"; 98 const KEY_BACKUP_ENDPOINT: &str = "backup_ep"; 99 100 // Authentication keys 101 const KEY_DEVICE_SECRET: &str = "dev_secret"; 102 const KEY_REGISTRATION_URL: &str = "reg_url"; 103 104 // ============================================================================= 105 // DATA STRUCTURES 106 // ============================================================================= 107 108 /// Device configuration 109 #[derive(Debug, Clone, Serialize, Deserialize)] 110 pub struct DeviceConfig { 111 pub enabled: bool, 112 pub gpio_pin: u8, 113 } 114 115 impl Default for DeviceConfig { 116 fn default() -> Self { 117 Self { 118 enabled: false, 119 gpio_pin: 0, 120 } 121 } 122 } 123 124 // ============================================================================= 125 // SETTINGS IMPLEMENTATION 126 // ============================================================================= 127 128 /// Application settings that can be modified via web interface 129 #[derive(Debug, Serialize, Deserialize)] 130 pub struct Settings { 131 pub wifi_ssid: String, 132 pub wifi_pass: String, 133 pub mqtt_broker_url: String, 134 pub publish_interval_secs: i64, 135 136 // Device configurations 137 pub exhaust_fan: DeviceConfig, 138 pub circulation_fan: DeviceConfig, 139 pub heater: DeviceConfig, 140 pub air_purifier: DeviceConfig, 141 142 // Automation settings 143 pub automation_enabled: bool, 144 pub temp_high_threshold_c: f32, // Turn on cooling/exhaust 145 pub temp_low_threshold_c: f32, // Turn on heating 146 pub humidity_high_threshold: f32, // Turn on exhaust/purifier 147 pub gas_threshold_kohm: f32, // Turn on purifier if gas < threshold 148 149 // OTA settings 150 pub ota_update_url: String, 151 pub ota_auto_update: bool, 152 153 // Alarm settings 154 pub alarm_enabled: bool, 155 pub alarm_wifi_disconnect_secs: i64, 156 pub alarm_mqtt_failure_secs: i64, 157 pub alarm_sensor_failure_secs: i64, 158 pub alarm_critical_temp_low: f32, 159 pub alarm_critical_temp_high: f32, 160 pub alarm_automation_delay_secs: i64, 161 162 // Syslog settings 163 pub syslog_enabled: bool, 164 pub syslog_server: String, 165 pub syslog_port: u16, 166 pub app_name: String, 167 pub hostname: String, 168 169 // Device identification (generated from MAC address on first boot) 170 pub device_id: String, 171 172 // Settings backup configuration 173 pub backup_password: String, 174 pub backup_endpoint: String, 175 176 // Device authentication (generated on first boot, never exported) 177 pub device_secret: String, 178 pub registration_url: String, // Server endpoint for first-boot registration 179 180 // Internal: last NVS save timestamp for wear leveling (not persisted) 181 #[serde(skip)] 182 last_save_time: Mutex<u64>, 183 } 184 185 impl Default for Settings { 186 fn default() -> Self { 187 Self { 188 wifi_ssid: option_env!("WIFI_SSID").unwrap_or("").to_string(), 189 wifi_pass: option_env!("WIFI_PASS").unwrap_or("").to_string(), 190 mqtt_broker_url: option_env!("MQTT_BROKER_URL").unwrap_or("").to_string(), 191 publish_interval_secs: option_env!("PUBLISH_INTERVAL_SECS") 192 .and_then(|s| s.parse().ok()) 193 .unwrap_or(60), 194 exhaust_fan: DeviceConfig { 195 enabled: false, 196 gpio_pin: 26, // GPIO26 197 }, 198 circulation_fan: DeviceConfig { 199 enabled: false, 200 gpio_pin: 27, // GPIO27 201 }, 202 heater: DeviceConfig { 203 enabled: false, 204 gpio_pin: 14, // GPIO14 205 }, 206 air_purifier: DeviceConfig { 207 enabled: false, 208 gpio_pin: 12, // GPIO12 209 }, 210 automation_enabled: false, 211 temp_high_threshold_c: 28.0, // Cooling threshold 212 temp_low_threshold_c: 18.0, // Heating threshold 213 humidity_high_threshold: 70.0, // High humidity threshold 214 gas_threshold_kohm: 50.0, // Air quality threshold 215 ota_update_url: option_env!("OTA_UPDATE_URL") 216 .unwrap_or("https://update.ret.ac/firmware.bin") 217 .to_string(), 218 ota_auto_update: false, 219 alarm_enabled: true, 220 alarm_wifi_disconnect_secs: 300, 221 alarm_mqtt_failure_secs: 600, 222 alarm_sensor_failure_secs: 120, 223 alarm_critical_temp_low: 5.0, 224 alarm_critical_temp_high: 45.0, 225 alarm_automation_delay_secs: 180, 226 syslog_enabled: option_env!("SYSLOG_ENABLED") 227 .and_then(|s| s.parse().ok()) 228 .unwrap_or(false), 229 syslog_server: option_env!("SYSLOG_SERVER").unwrap_or("").to_string(), 230 syslog_port: option_env!("SYSLOG_PORT") 231 .and_then(|s| s.parse().ok()) 232 .unwrap_or(514), 233 app_name: option_env!("APP_NAME").unwrap_or("Harrastila").to_string(), 234 hostname: option_env!("HOSTNAME").unwrap_or("ESP32").to_string(), 235 device_id: Self::generate_device_id(), 236 backup_password: String::new(), 237 backup_endpoint: option_env!("BACKUP_ENDPOINT") 238 .unwrap_or("https://backup.ret.ac:8080/backup") 239 .to_string(), 240 device_secret: String::new(), // Generated on first boot 241 registration_url: option_env!("REGISTRATION_URL") 242 .unwrap_or("https://api.ret.ac/api/register") 243 .to_string(), 244 last_save_time: Mutex::new(0), 245 } 246 } 247 } 248 249 impl Clone for Settings { 250 fn clone(&self) -> Self { 251 Self { 252 wifi_ssid: self.wifi_ssid.clone(), 253 wifi_pass: self.wifi_pass.clone(), 254 mqtt_broker_url: self.mqtt_broker_url.clone(), 255 publish_interval_secs: self.publish_interval_secs, 256 exhaust_fan: self.exhaust_fan.clone(), 257 circulation_fan: self.circulation_fan.clone(), 258 heater: self.heater.clone(), 259 air_purifier: self.air_purifier.clone(), 260 automation_enabled: self.automation_enabled, 261 temp_high_threshold_c: self.temp_high_threshold_c, 262 temp_low_threshold_c: self.temp_low_threshold_c, 263 humidity_high_threshold: self.humidity_high_threshold, 264 gas_threshold_kohm: self.gas_threshold_kohm, 265 ota_update_url: self.ota_update_url.clone(), 266 ota_auto_update: self.ota_auto_update, 267 alarm_enabled: self.alarm_enabled, 268 alarm_wifi_disconnect_secs: self.alarm_wifi_disconnect_secs, 269 alarm_mqtt_failure_secs: self.alarm_mqtt_failure_secs, 270 alarm_sensor_failure_secs: self.alarm_sensor_failure_secs, 271 alarm_critical_temp_low: self.alarm_critical_temp_low, 272 alarm_critical_temp_high: self.alarm_critical_temp_high, 273 alarm_automation_delay_secs: self.alarm_automation_delay_secs, 274 syslog_enabled: self.syslog_enabled, 275 syslog_server: self.syslog_server.clone(), 276 syslog_port: self.syslog_port, 277 app_name: self.app_name.clone(), 278 hostname: self.hostname.clone(), 279 device_id: self.device_id.clone(), 280 backup_password: self.backup_password.clone(), 281 backup_endpoint: self.backup_endpoint.clone(), 282 device_secret: self.device_secret.clone(), 283 registration_url: self.registration_url.clone(), 284 last_save_time: Mutex::new(*self.last_save_time.lock().unwrap()), 285 } 286 } 287 } 288 289 impl Settings { 290 /// Load settings from NVS, falling back to defaults if not found 291 /// Returns (Settings, is_first_boot) 292 pub fn load(nvs: &EspNvsPartition<NvsDefault>) -> Result<(Self, bool)> { 293 let nvs_handle = match EspNvs::new(nvs.clone(), NVS_NAMESPACE, true) { 294 Ok(h) => h, 295 Err(e) => { 296 error!("Failed to open NVS namespace: {:?}", e); 297 info!("Using default settings"); 298 return Ok((Settings::default(), false)); 299 } 300 }; 301 let settings = Settings { 302 wifi_ssid: Self::read_string(&nvs_handle, KEY_WIFI_SSID) 303 .unwrap_or_else(|| option_env!("WIFI_SSID").unwrap_or("").to_string()), 304 wifi_pass: Self::read_string(&nvs_handle, KEY_WIFI_PASS) 305 .unwrap_or_else(|| option_env!("WIFI_PASS").unwrap_or("").to_string()), 306 mqtt_broker_url: Self::read_string(&nvs_handle, KEY_MQTT_BROKER) 307 .unwrap_or_else(|| option_env!("MQTT_BROKER_URL").unwrap_or("").to_string()), 308 publish_interval_secs: Self::read_i64(&nvs_handle, KEY_PUBLISH_INT).unwrap_or_else( 309 || { 310 option_env!("PUBLISH_INTERVAL_SECS") 311 .and_then(|s| s.parse().ok()) 312 .unwrap_or(60) 313 }, 314 ), 315 exhaust_fan: DeviceConfig { 316 enabled: Self::read_u8(&nvs_handle, KEY_EXHAUST_FAN_EN).unwrap_or(0) != 0, 317 gpio_pin: Self::read_u8(&nvs_handle, KEY_EXHAUST_FAN_GPIO).unwrap_or(26), 318 }, 319 circulation_fan: DeviceConfig { 320 enabled: Self::read_u8(&nvs_handle, KEY_CIRC_FAN_EN).unwrap_or(0) != 0, 321 gpio_pin: Self::read_u8(&nvs_handle, KEY_CIRC_FAN_GPIO).unwrap_or(27), 322 }, 323 heater: DeviceConfig { 324 enabled: Self::read_u8(&nvs_handle, KEY_HEATER_EN).unwrap_or(0) != 0, 325 gpio_pin: Self::read_u8(&nvs_handle, KEY_HEATER_GPIO).unwrap_or(14), 326 }, 327 air_purifier: DeviceConfig { 328 enabled: Self::read_u8(&nvs_handle, KEY_PURIFIER_EN).unwrap_or(0) != 0, 329 gpio_pin: Self::read_u8(&nvs_handle, KEY_PURIFIER_GPIO).unwrap_or(12), 330 }, 331 automation_enabled: Self::read_u8(&nvs_handle, KEY_AUTO_ENABLED).unwrap_or(0) != 0, 332 temp_high_threshold_c: Self::read_i64(&nvs_handle, KEY_TEMP_HIGH_THRESH) 333 .unwrap_or(28000) as f32 334 / 1000.0, 335 temp_low_threshold_c: Self::read_i64(&nvs_handle, KEY_TEMP_LOW_THRESH).unwrap_or(18000) 336 as f32 337 / 1000.0, 338 humidity_high_threshold: Self::read_i64(&nvs_handle, KEY_HUM_HIGH_THRESH) 339 .unwrap_or(70000) as f32 340 / 1000.0, 341 gas_threshold_kohm: Self::read_i64(&nvs_handle, KEY_GAS_THRESH).unwrap_or(50000) as f32 342 / 1000.0, 343 ota_update_url: Self::read_string(&nvs_handle, KEY_OTA_URL).unwrap_or_else(|| { 344 option_env!("OTA_UPDATE_URL") 345 .unwrap_or("https://update.ret.ac/firmware.bin") 346 .to_string() 347 }), 348 ota_auto_update: Self::read_u8(&nvs_handle, KEY_OTA_AUTO).unwrap_or(0) != 0, 349 alarm_enabled: Self::read_u8(&nvs_handle, KEY_ALARM_ENABLED).unwrap_or(1) != 0, 350 alarm_wifi_disconnect_secs: Self::read_i64(&nvs_handle, KEY_ALARM_WIFI_SEC) 351 .unwrap_or(300), 352 alarm_mqtt_failure_secs: Self::read_i64(&nvs_handle, KEY_ALARM_MQTT_SEC).unwrap_or(600), 353 alarm_sensor_failure_secs: Self::read_i64(&nvs_handle, KEY_ALARM_SENSOR_SEC) 354 .unwrap_or(120), 355 alarm_critical_temp_low: Self::read_i64(&nvs_handle, KEY_ALARM_TEMP_LOW).unwrap_or(5000) 356 as f32 357 / 1000.0, 358 alarm_critical_temp_high: Self::read_i64(&nvs_handle, KEY_ALARM_TEMP_HIGH) 359 .unwrap_or(45000) as f32 360 / 1000.0, 361 alarm_automation_delay_secs: Self::read_i64(&nvs_handle, KEY_ALARM_AUTO_SEC) 362 .unwrap_or(180), 363 syslog_enabled: Self::read_u8(&nvs_handle, KEY_SYSLOG_ENABLED).unwrap_or_else(|| { 364 option_env!("SYSLOG_ENABLED") 365 .and_then(|s| s.parse().ok()) 366 .unwrap_or(0) 367 }) != 0, 368 syslog_server: Self::read_string(&nvs_handle, KEY_SYSLOG_SERVER) 369 .unwrap_or_else(|| option_env!("SYSLOG_SERVER").unwrap_or("").to_string()), 370 syslog_port: Self::read_i64(&nvs_handle, KEY_SYSLOG_PORT).unwrap_or_else(|| { 371 option_env!("SYSLOG_PORT") 372 .and_then(|s| s.parse().ok()) 373 .unwrap_or(514) 374 }) as u16, 375 app_name: Self::read_string(&nvs_handle, KEY_APP_NAME) 376 .unwrap_or_else(|| option_env!("APP_NAME").unwrap_or("Harrastila").to_string()), 377 hostname: Self::read_string(&nvs_handle, KEY_HOSTNAME) 378 .unwrap_or_else(|| option_env!("HOSTNAME").unwrap_or("ESP32").to_string()), 379 device_id: Self::read_string(&nvs_handle, KEY_DEVICE_ID) 380 .unwrap_or_else(Self::generate_device_id), 381 backup_password: Self::read_string(&nvs_handle, KEY_BACKUP_PASSWORD) 382 .unwrap_or_else(|| String::new()), 383 backup_endpoint: Self::read_string(&nvs_handle, KEY_BACKUP_ENDPOINT).unwrap_or_else( 384 || { 385 option_env!("BACKUP_ENDPOINT") 386 .unwrap_or("https://backup.ret.ac/backup") 387 .to_string() 388 }, 389 ), 390 device_secret: Self::read_string(&nvs_handle, KEY_DEVICE_SECRET) 391 .unwrap_or_else(auth::generate_device_secret), 392 registration_url: Self::read_string(&nvs_handle, KEY_REGISTRATION_URL).unwrap_or_else( 393 || { 394 option_env!("REGISTRATION_URL") 395 .unwrap_or("https://api.ret.ac/api/register") 396 .to_string() 397 }, 398 ), 399 last_save_time: Mutex::new(0), 400 }; 401 402 // Save device_id and device_secret to NVS if they were just generated (first boot) 403 let mut first_boot = false; 404 if Self::read_string(&nvs_handle, KEY_DEVICE_ID).is_none() { 405 first_boot = true; 406 if let Err(e) = Self::write_string( 407 &mut EspNvs::new(nvs.clone(), NVS_NAMESPACE, true)?, 408 KEY_DEVICE_ID, 409 &settings.device_id, 410 ) { 411 error!("Failed to save device_id: {:?}", e); 412 } else { 413 info!("Generated and saved device_id: {}", settings.device_id); 414 } 415 } 416 417 if Self::read_string(&nvs_handle, KEY_DEVICE_SECRET).is_none() { 418 first_boot = true; 419 if let Err(e) = Self::write_string( 420 &mut EspNvs::new(nvs.clone(), NVS_NAMESPACE, true)?, 421 KEY_DEVICE_SECRET, 422 &settings.device_secret, 423 ) { 424 error!("Failed to save device_secret: {:?}", e); 425 } else { 426 info!("Generated and saved device_secret (first boot)"); 427 } 428 } 429 430 if first_boot { 431 info!( 432 "IMPORTANT: Device ID: {} - Register this device with the server!", 433 settings.device_id 434 ); 435 } 436 437 info!("Settings loaded successfully"); 438 Ok((settings, first_boot)) 439 } 440 441 /// Save settings to NVS 442 pub fn save(&self, nvs: &EspNvsPartition<NvsDefault>) -> Result<()> { 443 let mut nvs_handle = EspNvs::new(nvs.clone(), NVS_NAMESPACE, true)?; 444 445 Self::write_string(&mut nvs_handle, KEY_WIFI_SSID, &self.wifi_ssid)?; 446 Self::write_string(&mut nvs_handle, KEY_WIFI_PASS, &self.wifi_pass)?; 447 Self::write_string(&mut nvs_handle, KEY_MQTT_BROKER, &self.mqtt_broker_url)?; 448 Self::write_i64(&mut nvs_handle, KEY_PUBLISH_INT, self.publish_interval_secs)?; 449 450 // Device configurations 451 Self::write_u8( 452 &mut nvs_handle, 453 KEY_EXHAUST_FAN_EN, 454 if self.exhaust_fan.enabled { 1 } else { 0 }, 455 )?; 456 Self::write_u8( 457 &mut nvs_handle, 458 KEY_EXHAUST_FAN_GPIO, 459 self.exhaust_fan.gpio_pin, 460 )?; 461 Self::write_u8( 462 &mut nvs_handle, 463 KEY_CIRC_FAN_EN, 464 if self.circulation_fan.enabled { 1 } else { 0 }, 465 )?; 466 Self::write_u8( 467 &mut nvs_handle, 468 KEY_CIRC_FAN_GPIO, 469 self.circulation_fan.gpio_pin, 470 )?; 471 Self::write_u8( 472 &mut nvs_handle, 473 KEY_HEATER_EN, 474 if self.heater.enabled { 1 } else { 0 }, 475 )?; 476 Self::write_u8(&mut nvs_handle, KEY_HEATER_GPIO, self.heater.gpio_pin)?; 477 Self::write_u8( 478 &mut nvs_handle, 479 KEY_PURIFIER_EN, 480 if self.air_purifier.enabled { 1 } else { 0 }, 481 )?; 482 Self::write_u8( 483 &mut nvs_handle, 484 KEY_PURIFIER_GPIO, 485 self.air_purifier.gpio_pin, 486 )?; 487 488 // Automation settings 489 Self::write_u8( 490 &mut nvs_handle, 491 KEY_AUTO_ENABLED, 492 if self.automation_enabled { 1 } else { 0 }, 493 )?; 494 Self::write_i64( 495 &mut nvs_handle, 496 KEY_TEMP_HIGH_THRESH, 497 (self.temp_high_threshold_c * 1000.0) as i64, 498 )?; 499 Self::write_i64( 500 &mut nvs_handle, 501 KEY_TEMP_LOW_THRESH, 502 (self.temp_low_threshold_c * 1000.0) as i64, 503 )?; 504 Self::write_i64( 505 &mut nvs_handle, 506 KEY_HUM_HIGH_THRESH, 507 (self.humidity_high_threshold * 1000.0) as i64, 508 )?; 509 Self::write_i64( 510 &mut nvs_handle, 511 KEY_GAS_THRESH, 512 (self.gas_threshold_kohm * 1000.0) as i64, 513 )?; 514 515 // OTA settings 516 Self::write_string(&mut nvs_handle, KEY_OTA_URL, &self.ota_update_url)?; 517 Self::write_u8( 518 &mut nvs_handle, 519 KEY_OTA_AUTO, 520 if self.ota_auto_update { 1 } else { 0 }, 521 )?; 522 523 // Alarm settings 524 Self::write_u8( 525 &mut nvs_handle, 526 KEY_ALARM_ENABLED, 527 if self.alarm_enabled { 1 } else { 0 }, 528 )?; 529 Self::write_i64( 530 &mut nvs_handle, 531 KEY_ALARM_WIFI_SEC, 532 self.alarm_wifi_disconnect_secs, 533 )?; 534 Self::write_i64( 535 &mut nvs_handle, 536 KEY_ALARM_MQTT_SEC, 537 self.alarm_mqtt_failure_secs, 538 )?; 539 Self::write_i64( 540 &mut nvs_handle, 541 KEY_ALARM_SENSOR_SEC, 542 self.alarm_sensor_failure_secs, 543 )?; 544 Self::write_i64( 545 &mut nvs_handle, 546 KEY_ALARM_TEMP_LOW, 547 (self.alarm_critical_temp_low * 1000.0) as i64, 548 )?; 549 Self::write_i64( 550 &mut nvs_handle, 551 KEY_ALARM_TEMP_HIGH, 552 (self.alarm_critical_temp_high * 1000.0) as i64, 553 )?; 554 Self::write_i64( 555 &mut nvs_handle, 556 KEY_ALARM_AUTO_SEC, 557 self.alarm_automation_delay_secs, 558 )?; 559 560 // Syslog settings 561 Self::write_u8( 562 &mut nvs_handle, 563 KEY_SYSLOG_ENABLED, 564 if self.syslog_enabled { 1 } else { 0 }, 565 )?; 566 Self::write_string(&mut nvs_handle, KEY_SYSLOG_SERVER, &self.syslog_server)?; 567 Self::write_i64(&mut nvs_handle, KEY_SYSLOG_PORT, self.syslog_port as i64)?; 568 Self::write_string(&mut nvs_handle, KEY_APP_NAME, &self.app_name)?; 569 Self::write_string(&mut nvs_handle, KEY_HOSTNAME, &self.hostname)?; 570 Self::write_string(&mut nvs_handle, KEY_DEVICE_ID, &self.device_id)?; 571 Self::write_string(&mut nvs_handle, KEY_BACKUP_PASSWORD, &self.backup_password)?; 572 Self::write_string(&mut nvs_handle, KEY_BACKUP_ENDPOINT, &self.backup_endpoint)?; 573 Self::write_string(&mut nvs_handle, KEY_DEVICE_SECRET, &self.device_secret)?; 574 Self::write_string( 575 &mut nvs_handle, 576 KEY_REGISTRATION_URL, 577 &self.registration_url, 578 )?; 579 580 // Update last save timestamp for wear leveling 581 *self.last_save_time.lock().unwrap() = Self::get_current_time_secs(); 582 583 info!("Settings saved successfully"); 584 Ok(()) 585 } 586 587 /// Save settings to NVS only if enough time has passed since last save 588 /// This reduces flash wear by batching writes 589 /// Returns true if settings were actually saved, false if skipped 590 #[allow(dead_code)] 591 pub fn save_if_dirty(&self, nvs: &EspNvsPartition<NvsDefault>) -> Result<bool> { 592 let now = Self::get_current_time_secs(); 593 let last_save = *self.last_save_time.lock().unwrap(); 594 595 // Skip save if within batch delay window 596 if last_save > 0 && (now - last_save) < NVS_WRITE_BATCH_DELAY_SECS { 597 return Ok(false); // Settings not saved (batched for later) 598 } 599 600 self.save(nvs)?; 601 Ok(true) 602 } 603 604 /// Force immediate save to NVS (bypasses wear leveling delay) 605 #[allow(dead_code)] 606 pub fn save_immediate(&self, nvs: &EspNvsPartition<NvsDefault>) -> Result<()> { 607 self.save(nvs) 608 } 609 610 fn read_string(nvs: &EspNvs<NvsDefault>, key: &str) -> Option<String> { 611 let mut buf = [0u8; 128]; 612 match nvs.get_str(key, &mut buf) { 613 Ok(Some(s)) => Some(s.to_string()), 614 _ => None, 615 } 616 } 617 618 fn write_string(nvs: &mut EspNvs<NvsDefault>, key: &str, value: &str) -> Result<()> { 619 nvs.set_str(key, value)?; 620 Ok(()) 621 } 622 623 fn read_u8(nvs: &EspNvs<NvsDefault>, key: &str) -> Option<u8> { 624 nvs.get_u8(key).ok().flatten() 625 } 626 627 fn write_u8(nvs: &mut EspNvs<NvsDefault>, key: &str, value: u8) -> Result<()> { 628 nvs.set_u8(key, value)?; 629 Ok(()) 630 } 631 632 fn read_i64(nvs: &EspNvs<NvsDefault>, key: &str) -> Option<i64> { 633 nvs.get_i64(key).ok().flatten() 634 } 635 636 fn write_i64(nvs: &mut EspNvs<NvsDefault>, key: &str, value: i64) -> Result<()> { 637 nvs.set_i64(key, value)?; 638 Ok(()) 639 } 640 641 /// Get current time in seconds since boot (for timestamps) 642 fn get_current_time_secs() -> u64 { 643 unsafe { esp_idf_svc::sys::esp_timer_get_time() as u64 / 1_000_000 } 644 } 645 646 /// Generate a unique device identifier from MAC address 647 /// Returns last 6 characters of base32-encoded MAC for readability 648 fn generate_device_id() -> String { 649 use esp_idf_svc::sys::{esp_efuse_mac_get_default, ESP_OK}; 650 651 let mut mac: [u8; 6] = [0; 6]; 652 unsafe { 653 let result = esp_efuse_mac_get_default(mac.as_mut_ptr()); 654 if result != ESP_OK { 655 // Fallback to random value if MAC read fails 656 return format!( 657 "UNKN{:04X}", 658 (std::time::SystemTime::now() 659 .duration_since(std::time::UNIX_EPOCH) 660 .unwrap_or_default() 661 .as_secs() 662 & 0xFFFF) 663 ); 664 } 665 } 666 667 // Create compact ID using last 4 bytes of MAC in hex 668 // Format: AABBCCDD (8 characters, easy to read) 669 format!("{:02X}{:02X}{:02X}{:02X}", mac[2], mac[3], mac[4], mac[5]) 670 } 671 }