Skip to main content

wifi_densepose_mat/
lib.rs

1//! # WiFi-DensePose MAT (Mass Casualty Assessment Tool)
2//!
3//! A modular extension for WiFi-based disaster survivor detection and localization.
4//!
5//! This crate provides capabilities for detecting human survivors trapped in rubble,
6//! debris, or collapsed structures using WiFi Channel State Information (CSI) analysis.
7//!
8//! ## Features
9//!
10//! - **Vital Signs Detection**: Breathing patterns, heartbeat signatures, and movement
11//! - **Survivor Localization**: 3D position estimation through debris
12//! - **Triage Classification**: Automatic START protocol-compatible triage
13//! - **Real-time Alerting**: Priority-based alert generation and dispatch
14//!
15//! ## Use Cases
16//!
17//! - Earthquake search and rescue
18//! - Building collapse response
19//! - Avalanche victim location
20//! - Flood rescue operations
21//! - Mine collapse detection
22//!
23//! ## Architecture
24//!
25//! The crate follows Domain-Driven Design (DDD) principles with clear bounded contexts:
26//!
27//! ```text
28//! ┌─────────────────────────────────────────────────────────┐
29//! │                    wifi-densepose-mat                    │
30//! ├─────────────────────────────────────────────────────────┤
31//! │  ┌───────────┐  ┌─────────────┐  ┌─────────────────┐   │
32//! │  │ Detection │  │Localization │  │    Alerting     │   │
33//! │  │  Context  │  │   Context   │  │    Context      │   │
34//! │  └─────┬─────┘  └──────┬──────┘  └────────┬────────┘   │
35//! │        └───────────────┼──────────────────┘            │
36//! │                        │                                │
37//! │              ┌─────────▼─────────┐                      │
38//! │              │   Integration     │                      │
39//! │              │      Layer        │                      │
40//! │              └───────────────────┘                      │
41//! └─────────────────────────────────────────────────────────┘
42//! ```
43//!
44//! ## Example
45//!
46//! ```rust,no_run
47//! use wifi_densepose_mat::{
48//!     DisasterResponse, DisasterConfig, DisasterType,
49//!     ScanZone, ZoneBounds,
50//! };
51//!
52//! #[tokio::main]
53//! async fn main() -> anyhow::Result<()> {
54//!     // Initialize disaster response system
55//!     let config = DisasterConfig::builder()
56//!         .disaster_type(DisasterType::Earthquake)
57//!         .sensitivity(0.8)
58//!         .build();
59//!
60//!     let mut response = DisasterResponse::new(config);
61//!
62//!     // Define scan zone
63//!     let zone = ScanZone::new(
64//!         "Building A - North Wing",
65//!         ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
66//!     );
67//!     response.add_zone(zone)?;
68//!
69//!     // Start scanning
70//!     response.start_scanning().await?;
71//!
72//!     Ok(())
73//! }
74//! ```
75
76#![cfg_attr(docsrs, feature(doc_cfg))]
77#![warn(missing_docs)]
78#![warn(rustdoc::missing_crate_level_docs)]
79
80pub mod alerting;
81pub mod api;
82pub mod detection;
83pub mod domain;
84pub mod integration;
85pub mod localization;
86pub mod ml;
87pub mod tracking;
88
89// Re-export main types
90pub use domain::{
91    survivor::{Survivor, SurvivorId, SurvivorMetadata, SurvivorStatus},
92    disaster_event::{DisasterEvent, DisasterEventId, DisasterType, EventStatus},
93    scan_zone::{ScanZone, ScanZoneId, ZoneBounds, ZoneStatus, ScanParameters},
94    alert::{Alert, AlertId, AlertPayload, Priority},
95    vital_signs::{
96        VitalSignsReading, BreathingPattern, BreathingType,
97        HeartbeatSignature, MovementProfile, MovementType,
98    },
99    triage::{TriageStatus, TriageCalculator},
100    coordinates::{Coordinates3D, LocationUncertainty, DepthEstimate},
101    events::{DetectionEvent, AlertEvent, DomainEvent, EventStore, InMemoryEventStore, TrackingEvent},
102};
103
104pub use detection::{
105    BreathingDetector, BreathingDetectorConfig,
106    HeartbeatDetector, HeartbeatDetectorConfig,
107    MovementClassifier, MovementClassifierConfig,
108    VitalSignsDetector, DetectionPipeline, DetectionConfig,
109    EnsembleClassifier, EnsembleConfig, EnsembleResult,
110};
111
112pub use localization::{
113    Triangulator, TriangulationConfig,
114    DepthEstimator, DepthEstimatorConfig,
115    PositionFuser, LocalizationService,
116};
117
118pub use alerting::{
119    AlertGenerator, AlertDispatcher, AlertConfig,
120    TriageService, PriorityCalculator,
121};
122
123pub use integration::{
124    SignalAdapter, NeuralAdapter, HardwareAdapter,
125    AdapterError, IntegrationConfig,
126};
127
128pub use api::{
129    create_router, AppState,
130};
131
132pub use ml::{
133    // Core ML types
134    MlError, MlResult, MlDetectionConfig, MlDetectionPipeline, MlDetectionResult,
135    // Debris penetration model
136    DebrisPenetrationModel, DebrisFeatures, DepthEstimate as MlDepthEstimate,
137    DebrisModel, DebrisModelConfig, DebrisFeatureExtractor,
138    MaterialType, DebrisClassification, AttenuationPrediction,
139    // Vital signs classifier
140    VitalSignsClassifier, VitalSignsClassifierConfig,
141    BreathingClassification, HeartbeatClassification,
142    UncertaintyEstimate, ClassifierOutput,
143};
144
145pub use tracking::{
146    SurvivorTracker, TrackerConfig, TrackId, TrackedSurvivor,
147    DetectionObservation, AssociationResult,
148    KalmanState, CsiFingerprint,
149    TrackState, TrackLifecycle,
150};
151
152/// Library version
153pub const VERSION: &str = env!("CARGO_PKG_VERSION");
154
155/// Common result type for MAT operations
156pub type Result<T> = std::result::Result<T, MatError>;
157
158/// Unified error type for MAT operations
159#[derive(Debug, thiserror::Error)]
160pub enum MatError {
161    /// Detection error
162    #[error("Detection error: {0}")]
163    Detection(String),
164
165    /// Localization error
166    #[error("Localization error: {0}")]
167    Localization(String),
168
169    /// Alerting error
170    #[error("Alerting error: {0}")]
171    Alerting(String),
172
173    /// Integration error
174    #[error("Integration error: {0}")]
175    Integration(#[from] AdapterError),
176
177    /// Configuration error
178    #[error("Configuration error: {0}")]
179    Config(String),
180
181    /// Domain invariant violation
182    #[error("Domain error: {0}")]
183    Domain(String),
184
185    /// Repository error
186    #[error("Repository error: {0}")]
187    Repository(String),
188
189    /// Signal processing error
190    #[error("Signal processing error: {0}")]
191    Signal(#[from] wifi_densepose_signal::SignalError),
192
193    /// I/O error
194    #[error("I/O error: {0}")]
195    Io(#[from] std::io::Error),
196
197    /// Machine learning error
198    #[error("ML error: {0}")]
199    Ml(#[from] ml::MlError),
200}
201
202/// Configuration for the disaster response system
203#[derive(Debug, Clone)]
204pub struct DisasterConfig {
205    /// Type of disaster event
206    pub disaster_type: DisasterType,
207    /// Detection sensitivity (0.0-1.0)
208    pub sensitivity: f64,
209    /// Minimum confidence threshold for survivor detection
210    pub confidence_threshold: f64,
211    /// Maximum depth to scan (meters)
212    pub max_depth: f64,
213    /// Scan interval in milliseconds
214    pub scan_interval_ms: u64,
215    /// Enable continuous monitoring
216    pub continuous_monitoring: bool,
217    /// Alert configuration
218    pub alert_config: AlertConfig,
219}
220
221impl Default for DisasterConfig {
222    fn default() -> Self {
223        Self {
224            disaster_type: DisasterType::Unknown,
225            sensitivity: 0.8,
226            confidence_threshold: 0.5,
227            max_depth: 5.0,
228            scan_interval_ms: 500,
229            continuous_monitoring: true,
230            alert_config: AlertConfig::default(),
231        }
232    }
233}
234
235impl DisasterConfig {
236    /// Create a new configuration builder
237    pub fn builder() -> DisasterConfigBuilder {
238        DisasterConfigBuilder::default()
239    }
240}
241
242/// Builder for DisasterConfig
243#[derive(Debug, Default)]
244pub struct DisasterConfigBuilder {
245    config: DisasterConfig,
246}
247
248impl DisasterConfigBuilder {
249    /// Set disaster type
250    pub fn disaster_type(mut self, disaster_type: DisasterType) -> Self {
251        self.config.disaster_type = disaster_type;
252        self
253    }
254
255    /// Set detection sensitivity
256    pub fn sensitivity(mut self, sensitivity: f64) -> Self {
257        self.config.sensitivity = sensitivity.clamp(0.0, 1.0);
258        self
259    }
260
261    /// Set confidence threshold
262    pub fn confidence_threshold(mut self, threshold: f64) -> Self {
263        self.config.confidence_threshold = threshold.clamp(0.0, 1.0);
264        self
265    }
266
267    /// Set maximum scan depth
268    pub fn max_depth(mut self, depth: f64) -> Self {
269        self.config.max_depth = depth.max(0.0);
270        self
271    }
272
273    /// Set scan interval
274    pub fn scan_interval_ms(mut self, interval: u64) -> Self {
275        self.config.scan_interval_ms = interval.max(100);
276        self
277    }
278
279    /// Enable/disable continuous monitoring
280    pub fn continuous_monitoring(mut self, enabled: bool) -> Self {
281        self.config.continuous_monitoring = enabled;
282        self
283    }
284
285    /// Build the configuration
286    pub fn build(self) -> DisasterConfig {
287        self.config
288    }
289}
290
291/// Main disaster response coordinator
292pub struct DisasterResponse {
293    config: DisasterConfig,
294    event: Option<DisasterEvent>,
295    detection_pipeline: DetectionPipeline,
296    localization_service: LocalizationService,
297    alert_dispatcher: AlertDispatcher,
298    event_store: std::sync::Arc<dyn domain::events::EventStore>,
299    ensemble_classifier: EnsembleClassifier,
300    tracker: tracking::SurvivorTracker,
301    running: std::sync::atomic::AtomicBool,
302}
303
304impl DisasterResponse {
305    /// Create a new disaster response system
306    pub fn new(config: DisasterConfig) -> Self {
307        let detection_config = DetectionConfig::from_disaster_config(&config);
308        let detection_pipeline = DetectionPipeline::new(detection_config);
309
310        let localization_service = LocalizationService::new();
311        let alert_dispatcher = AlertDispatcher::new(config.alert_config.clone());
312        let event_store: std::sync::Arc<dyn domain::events::EventStore> =
313            std::sync::Arc::new(InMemoryEventStore::new());
314        let ensemble_classifier = EnsembleClassifier::new(EnsembleConfig::default());
315
316        Self {
317            config,
318            event: None,
319            detection_pipeline,
320            localization_service,
321            alert_dispatcher,
322            event_store,
323            ensemble_classifier,
324            tracker: tracking::SurvivorTracker::with_defaults(),
325            running: std::sync::atomic::AtomicBool::new(false),
326        }
327    }
328
329    /// Create with a custom event store (e.g. for persistence or testing)
330    pub fn with_event_store(
331        config: DisasterConfig,
332        event_store: std::sync::Arc<dyn domain::events::EventStore>,
333    ) -> Self {
334        let detection_config = DetectionConfig::from_disaster_config(&config);
335        let detection_pipeline = DetectionPipeline::new(detection_config);
336        let localization_service = LocalizationService::new();
337        let alert_dispatcher = AlertDispatcher::new(config.alert_config.clone());
338        let ensemble_classifier = EnsembleClassifier::new(EnsembleConfig::default());
339
340        Self {
341            config,
342            event: None,
343            detection_pipeline,
344            localization_service,
345            alert_dispatcher,
346            event_store,
347            ensemble_classifier,
348            tracker: tracking::SurvivorTracker::with_defaults(),
349            running: std::sync::atomic::AtomicBool::new(false),
350        }
351    }
352
353    /// Push CSI data into the detection pipeline for processing.
354    ///
355    /// This is the primary data ingestion point. Call this with real CSI
356    /// amplitude and phase readings from hardware (ESP32, Intel 5300, etc).
357    /// Returns an error string if data is invalid.
358    pub fn push_csi_data(&self, amplitudes: &[f64], phases: &[f64]) -> Result<()> {
359        if amplitudes.len() != phases.len() {
360            return Err(MatError::Detection(
361                "Amplitude and phase arrays must have equal length".into(),
362            ));
363        }
364        if amplitudes.is_empty() {
365            return Err(MatError::Detection("CSI data cannot be empty".into()));
366        }
367        self.detection_pipeline.add_data(amplitudes, phases);
368        Ok(())
369    }
370
371    /// Get the event store for querying domain events
372    pub fn event_store(&self) -> &std::sync::Arc<dyn domain::events::EventStore> {
373        &self.event_store
374    }
375
376    /// Get the ensemble classifier
377    pub fn ensemble_classifier(&self) -> &EnsembleClassifier {
378        &self.ensemble_classifier
379    }
380
381    /// Get the detection pipeline (for direct buffer inspection / data push)
382    pub fn detection_pipeline(&self) -> &DetectionPipeline {
383        &self.detection_pipeline
384    }
385
386    /// Get the survivor tracker
387    pub fn tracker(&self) -> &tracking::SurvivorTracker {
388        &self.tracker
389    }
390
391    /// Get mutable access to the tracker (for integration in scan_cycle)
392    pub fn tracker_mut(&mut self) -> &mut tracking::SurvivorTracker {
393        &mut self.tracker
394    }
395
396    /// Initialize a new disaster event
397    pub fn initialize_event(
398        &mut self,
399        location: geo::Point<f64>,
400        description: &str,
401    ) -> Result<&DisasterEvent> {
402        let event = DisasterEvent::new(
403            self.config.disaster_type.clone(),
404            location,
405            description,
406        );
407        self.event = Some(event);
408        self.event.as_ref().ok_or_else(|| MatError::Domain("Failed to create event".into()))
409    }
410
411    /// Add a scan zone to the current event
412    pub fn add_zone(&mut self, zone: ScanZone) -> Result<()> {
413        let event = self.event.as_mut()
414            .ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
415        event.add_zone(zone);
416        Ok(())
417    }
418
419    /// Start the scanning process
420    pub async fn start_scanning(&mut self) -> Result<()> {
421        use std::sync::atomic::Ordering;
422
423        self.running.store(true, Ordering::SeqCst);
424
425        while self.running.load(Ordering::SeqCst) {
426            self.scan_cycle().await?;
427
428            if !self.config.continuous_monitoring {
429                break;
430            }
431
432            tokio::time::sleep(
433                std::time::Duration::from_millis(self.config.scan_interval_ms)
434            ).await;
435        }
436
437        Ok(())
438    }
439
440    /// Stop the scanning process
441    pub fn stop_scanning(&self) {
442        use std::sync::atomic::Ordering;
443        self.running.store(false, Ordering::SeqCst);
444    }
445
446    /// Execute a single scan cycle.
447    ///
448    /// Processes all active zones, runs detection pipeline on buffered CSI data,
449    /// applies ensemble classification, emits domain events to the EventStore,
450    /// and dispatches alerts for newly detected survivors.
451    async fn scan_cycle(&mut self) -> Result<()> {
452        let scan_start = std::time::Instant::now();
453
454        // Collect detections first to avoid borrowing issues
455        let mut detections = Vec::new();
456
457        {
458            let event = self.event.as_ref()
459                .ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
460
461            for zone in event.zones() {
462                if zone.status() != &ZoneStatus::Active {
463                    continue;
464                }
465
466                // Process buffered CSI data through the detection pipeline
467                let detection_result = self.detection_pipeline.process_zone(zone).await?;
468
469                if let Some(vital_signs) = detection_result {
470                    // Run ensemble classifier to combine breathing + heartbeat + movement
471                    let ensemble_result = self.ensemble_classifier.classify(&vital_signs);
472
473                    // Only proceed if ensemble confidence meets threshold
474                    if ensemble_result.confidence >= self.config.confidence_threshold {
475                        // Attempt localization
476                        let location = self.localization_service
477                            .estimate_position(&vital_signs, zone);
478
479                        detections.push((zone.id().clone(), zone.name().to_string(), vital_signs, location, ensemble_result));
480                    }
481                }
482
483                // Emit zone scan completed event
484                let scan_duration = scan_start.elapsed();
485                let _ = self.event_store.append(DomainEvent::Zone(
486                    domain::events::ZoneEvent::ZoneScanCompleted {
487                        zone_id: zone.id().clone(),
488                        detections_found: detections.len() as u32,
489                        scan_duration_ms: scan_duration.as_millis() as u64,
490                        timestamp: chrono::Utc::now(),
491                    },
492                ));
493            }
494        }
495
496        // Now process detections with mutable access
497        let event = self.event.as_mut()
498            .ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
499
500        for (zone_id, _zone_name, vital_signs, location, _ensemble) in detections {
501            let survivor = event.record_detection(zone_id.clone(), vital_signs.clone(), location.clone())?;
502
503            // Emit SurvivorDetected domain event
504            let _ = self.event_store.append(DomainEvent::Detection(
505                DetectionEvent::SurvivorDetected {
506                    survivor_id: survivor.id().clone(),
507                    zone_id,
508                    vital_signs,
509                    location,
510                    timestamp: chrono::Utc::now(),
511                },
512            ));
513
514            // Generate and dispatch alert if needed
515            if survivor.should_alert() {
516                let alert = self.alert_dispatcher.generate_alert(survivor)?;
517                let alert_id = alert.id().clone();
518                let priority = alert.priority();
519                let survivor_id = alert.survivor_id().clone();
520
521                // Emit AlertGenerated domain event
522                let _ = self.event_store.append(DomainEvent::Alert(
523                    AlertEvent::AlertGenerated {
524                        alert_id,
525                        survivor_id,
526                        priority,
527                        timestamp: chrono::Utc::now(),
528                    },
529                ));
530
531                self.alert_dispatcher.dispatch(alert).await?;
532            }
533        }
534
535        Ok(())
536    }
537
538    /// Get the current disaster event
539    pub fn event(&self) -> Option<&DisasterEvent> {
540        self.event.as_ref()
541    }
542
543    /// Get all detected survivors
544    pub fn survivors(&self) -> Vec<&Survivor> {
545        self.event.as_ref()
546            .map(|e| e.survivors())
547            .unwrap_or_default()
548    }
549
550    /// Get survivors by triage status
551    pub fn survivors_by_triage(&self, status: TriageStatus) -> Vec<&Survivor> {
552        self.survivors()
553            .into_iter()
554            .filter(|s| s.triage_status() == &status)
555            .collect()
556    }
557}
558
559/// Prelude module for convenient imports
560pub mod prelude {
561    pub use crate::{
562        DisasterConfig, DisasterConfigBuilder, DisasterResponse,
563        MatError, Result,
564        // Domain types
565        Survivor, SurvivorId, DisasterEvent, DisasterType,
566        ScanZone, ZoneBounds, TriageStatus,
567        VitalSignsReading, BreathingPattern, HeartbeatSignature,
568        Coordinates3D, Alert, Priority,
569        // Event sourcing
570        DomainEvent, EventStore, InMemoryEventStore,
571        DetectionEvent, AlertEvent, TrackingEvent,
572        // Detection
573        DetectionPipeline, VitalSignsDetector,
574        EnsembleClassifier, EnsembleConfig, EnsembleResult,
575        // Localization
576        LocalizationService,
577        // Alerting
578        AlertDispatcher,
579        // ML types
580        MlDetectionConfig, MlDetectionPipeline, MlDetectionResult,
581        DebrisModel, MaterialType, DebrisClassification,
582        VitalSignsClassifier, UncertaintyEstimate,
583        // Tracking
584        SurvivorTracker, TrackerConfig, TrackId, DetectionObservation, AssociationResult,
585    };
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    #[test]
593    fn test_config_builder() {
594        let config = DisasterConfig::builder()
595            .disaster_type(DisasterType::Earthquake)
596            .sensitivity(0.9)
597            .confidence_threshold(0.6)
598            .max_depth(10.0)
599            .build();
600
601        assert!(matches!(config.disaster_type, DisasterType::Earthquake));
602        assert!((config.sensitivity - 0.9).abs() < f64::EPSILON);
603        assert!((config.confidence_threshold - 0.6).abs() < f64::EPSILON);
604        assert!((config.max_depth - 10.0).abs() < f64::EPSILON);
605    }
606
607    #[test]
608    fn test_sensitivity_clamping() {
609        let config = DisasterConfig::builder()
610            .sensitivity(1.5)
611            .build();
612
613        assert!((config.sensitivity - 1.0).abs() < f64::EPSILON);
614
615        let config = DisasterConfig::builder()
616            .sensitivity(-0.5)
617            .build();
618
619        assert!(config.sensitivity.abs() < f64::EPSILON);
620    }
621
622    #[test]
623    fn test_version() {
624        assert!(!VERSION.is_empty());
625    }
626}