// WIP mesh/transport protocol — suppress dead code warnings #![allow(dead_code)] //! Emergency alert system and dead man's switch for mesh networking. //! //! The dead man's switch automatically broadcasts a signed alert with GPS //! coordinates if the node operator hasn't interacted with the system for //! a configurable interval (default 6 hours). Useful for remote/off-grid //! deployments where physical safety is a concern. use super::message_types::{ self, AlertPayload, AlertType, Coordinate, MeshMessageType, TypedEnvelope, }; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use tokio::sync::RwLock; /// Default dead man's switch interval: 6 hours. const DEFAULT_INTERVAL_SECS: u64 = 21600; /// How often the background task checks the switch (60 seconds). const CHECK_INTERVAL_SECS: u64 = 60; const ALERT_CONFIG_FILE: &str = "alert-config.json"; /// Alert system configuration (persisted to disk). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AlertConfig { /// Whether the dead man's switch is enabled. pub dead_man_enabled: bool, /// Interval in seconds before the switch triggers. pub dead_man_interval_secs: u64, /// Last known GPS coordinates (for inclusion in alerts). #[serde(default, skip_serializing_if = "Option::is_none")] pub last_gps: Option, /// DIDs of peers to alert directly (in addition to mesh broadcast). #[serde(default)] pub emergency_contacts: Vec, /// Whether to automatically include GPS in dead man alerts. pub auto_include_gps: bool, /// Custom message to include in dead man alert. #[serde(default, skip_serializing_if = "Option::is_none")] pub custom_message: Option, } impl Default for AlertConfig { fn default() -> Self { Self { dead_man_enabled: false, dead_man_interval_secs: DEFAULT_INTERVAL_SECS, last_gps: None, emergency_contacts: Vec::new(), auto_include_gps: false, custom_message: None, } } } /// Load alert config from disk. pub async fn load_config(data_dir: &Path) -> Result { let path = data_dir.join(ALERT_CONFIG_FILE); if !path.exists() { return Ok(AlertConfig::default()); } let content = tokio::fs::read_to_string(&path) .await .context("Failed to read alert config")?; let config: AlertConfig = serde_json::from_str(&content).unwrap_or_default(); Ok(config) } /// Save alert config to disk. pub async fn save_config(data_dir: &Path, config: &AlertConfig) -> Result<()> { let content = serde_json::to_string_pretty(config) .context("Failed to serialize alert config")?; tokio::fs::write(data_dir.join(ALERT_CONFIG_FILE), content) .await .context("Failed to write alert config")?; Ok(()) } /// Dead man's switch state. pub struct DeadManSwitch { config: RwLock, last_activity: RwLock, triggered: RwLock, data_dir: PathBuf, } impl DeadManSwitch { /// Create a new dead man's switch. pub async fn new(data_dir: &Path) -> Result { let config = load_config(data_dir).await?; Ok(Self { config: RwLock::new(config), last_activity: RwLock::new(Instant::now()), triggered: RwLock::new(false), data_dir: data_dir.to_path_buf(), }) } /// Record user activity (resets the timer). pub async fn check_in(&self) { *self.last_activity.write().await = Instant::now(); *self.triggered.write().await = false; } /// Check if the switch has been triggered. pub async fn is_triggered(&self) -> bool { let config = self.config.read().await; if !config.dead_man_enabled { return false; } let last = *self.last_activity.read().await; let interval = Duration::from_secs(config.dead_man_interval_secs); last.elapsed() > interval } /// Update configuration. pub async fn configure(&self, config: AlertConfig) -> Result<()> { save_config(&self.data_dir, &config).await?; *self.config.write().await = config; Ok(()) } /// Get current configuration. pub async fn get_config(&self) -> AlertConfig { self.config.read().await.clone() } /// Get time remaining before trigger (in seconds), or 0 if triggered. pub async fn time_remaining_secs(&self) -> u64 { let config = self.config.read().await; if !config.dead_man_enabled { return u64::MAX; } let last = *self.last_activity.read().await; let interval = Duration::from_secs(config.dead_man_interval_secs); let elapsed = last.elapsed(); if elapsed > interval { 0 } else { (interval - elapsed).as_secs() } } /// Build the dead man alert payload. pub async fn build_alert(&self) -> AlertPayload { let config = self.config.read().await; let message = config .custom_message .clone() .unwrap_or_else(|| "Dead man's switch triggered — node operator unresponsive".to_string()); AlertPayload { alert_type: AlertType::DeadMan, message, coordinate: if config.auto_include_gps { config.last_gps.clone() } else { None }, } } /// Build a signed alert envelope ready for mesh transmission. pub async fn build_signed_alert( &self, signing_key: &ed25519_dalek::SigningKey, ) -> Result> { let alert = self.build_alert().await; let payload = message_types::encode_payload(&alert)?; let envelope = TypedEnvelope::new_signed(MeshMessageType::Alert, payload, signing_key); envelope.to_wire() } /// Check if the alert has already been sent (prevents re-broadcasting every 60s). pub async fn triggered_flag(&self) -> tokio::sync::RwLockReadGuard<'_, bool> { self.triggered.read().await } /// Mark the switch as having fired (alert already sent). pub async fn mark_triggered(&self) { *self.triggered.write().await = true; } /// Get the list of emergency contact DIDs. pub async fn emergency_contacts(&self) -> Vec { self.config.read().await.emergency_contacts.clone() } /// Update GPS coordinates. pub async fn update_gps(&self, coord: Coordinate) -> Result<()> { let mut config = self.config.write().await; config.last_gps = Some(coord); save_config(&self.data_dir, &config).await?; Ok(()) } /// Get status info for RPC. pub async fn status(&self) -> AlertStatus { let config = self.config.read().await; let triggered = self.is_triggered().await; let remaining = self.time_remaining_secs().await; AlertStatus { dead_man_enabled: config.dead_man_enabled, dead_man_interval_secs: config.dead_man_interval_secs, triggered, time_remaining_secs: remaining, has_gps: config.last_gps.is_some(), emergency_contacts: config.emergency_contacts.len(), } } } /// Status info returned via RPC. #[derive(Debug, Clone, Serialize)] pub struct AlertStatus { pub dead_man_enabled: bool, pub dead_man_interval_secs: u64, pub triggered: bool, pub time_remaining_secs: u64, pub has_gps: bool, pub emergency_contacts: usize, } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_config_roundtrip() { let dir = tempfile::tempdir().unwrap(); let config = AlertConfig { dead_man_enabled: true, dead_man_interval_secs: 3600, last_gps: Some(Coordinate::from_degrees(30.2672, -97.7431, Some("Austin".into()))), emergency_contacts: vec!["did:key:z6MkContact1".into()], auto_include_gps: true, custom_message: Some("Help!".into()), }; save_config(dir.path(), &config).await.unwrap(); let loaded = load_config(dir.path()).await.unwrap(); assert!(loaded.dead_man_enabled); assert_eq!(loaded.dead_man_interval_secs, 3600); assert_eq!(loaded.emergency_contacts.len(), 1); } #[tokio::test] async fn test_dead_man_not_triggered_when_disabled() { let dir = tempfile::tempdir().unwrap(); let switch = DeadManSwitch::new(dir.path()).await.unwrap(); // Default config has dead_man_enabled = false assert!(!switch.is_triggered().await); } #[tokio::test] async fn test_check_in_resets_timer() { let dir = tempfile::tempdir().unwrap(); let switch = DeadManSwitch::new(dir.path()).await.unwrap(); switch .configure(AlertConfig { dead_man_enabled: true, dead_man_interval_secs: 1, // 1 second for test ..Default::default() }) .await .unwrap(); // Wait for trigger tokio::time::sleep(Duration::from_secs(2)).await; assert!(switch.is_triggered().await); // Check in switch.check_in().await; assert!(!switch.is_triggered().await); } #[tokio::test] async fn test_build_alert_payload() { let dir = tempfile::tempdir().unwrap(); let switch = DeadManSwitch::new(dir.path()).await.unwrap(); switch .configure(AlertConfig { dead_man_enabled: true, last_gps: Some(Coordinate::from_degrees(51.5074, -0.1278, None)), auto_include_gps: true, custom_message: Some("SOS".into()), ..Default::default() }) .await .unwrap(); let alert = switch.build_alert().await; assert_eq!(alert.alert_type, AlertType::DeadMan); assert_eq!(alert.message, "SOS"); assert!(alert.coordinate.is_some()); } }