Files
wifi-densepose/rust-port/wifi-densepose-rs/crates/wifi-densepose-mat/src/alerting/generator.rs
Claude a17b630c02 feat: Add wifi-densepose-mat disaster detection module
Implements WiFi-Mat (Mass Casualty Assessment Tool) for detecting and
localizing survivors trapped in rubble, earthquakes, and natural disasters.

Architecture:
- Domain-Driven Design with bounded contexts (Detection, Localization, Alerting)
- Modular Rust crate integrating with existing wifi-densepose-* crates
- Event-driven architecture for audit trails and distributed deployments

Features:
- Breathing pattern detection from CSI amplitude variations
- Heartbeat detection using micro-Doppler analysis
- Movement classification (gross, fine, tremor, periodic)
- START protocol-compatible triage classification
- 3D position estimation via triangulation and depth estimation
- Real-time alert generation with priority escalation

Documentation:
- ADR-001: Architecture Decision Record for wifi-Mat
- DDD domain model specification
2026-01-13 17:24:50 +00:00

289 lines
9.0 KiB
Rust

//! Alert generation from survivor detections.
use crate::domain::{
Alert, AlertPayload, Priority, Survivor, TriageStatus, ScanZoneId,
};
use crate::MatError;
/// Generator for alerts based on survivor status
pub struct AlertGenerator {
/// Zone name lookup (would be connected to event in production)
zone_names: std::collections::HashMap<ScanZoneId, String>,
}
impl AlertGenerator {
/// Create a new alert generator
pub fn new() -> Self {
Self {
zone_names: std::collections::HashMap::new(),
}
}
/// Register a zone name
pub fn register_zone(&mut self, zone_id: ScanZoneId, name: String) {
self.zone_names.insert(zone_id, name);
}
/// Generate an alert for a survivor
pub fn generate(&self, survivor: &Survivor) -> Result<Alert, MatError> {
let priority = Priority::from_triage(survivor.triage_status());
let payload = self.create_payload(survivor);
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Generate an escalation alert
pub fn generate_escalation(
&self,
survivor: &Survivor,
reason: &str,
) -> Result<Alert, MatError> {
let mut payload = self.create_payload(survivor);
payload.title = format!("ESCALATED: {}", payload.title);
payload.message = format!(
"{}\n\nReason for escalation: {}",
payload.message, reason
);
// Escalated alerts are always at least high priority
let priority = match survivor.triage_status() {
TriageStatus::Immediate => Priority::Critical,
_ => Priority::High,
};
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Generate a status change alert
pub fn generate_status_change(
&self,
survivor: &Survivor,
previous_status: &TriageStatus,
) -> Result<Alert, MatError> {
let mut payload = self.create_payload(survivor);
payload.title = format!(
"Status Change: {}{}",
previous_status, survivor.triage_status()
);
// Determine if this is an upgrade (worse) or downgrade (better)
let is_upgrade = survivor.triage_status().priority() < previous_status.priority();
if is_upgrade {
payload.message = format!(
"URGENT: Survivor condition has WORSENED.\n{}\n\nPrevious: {}\nCurrent: {}",
payload.message,
previous_status,
survivor.triage_status()
);
} else {
payload.message = format!(
"Survivor condition has improved.\n{}\n\nPrevious: {}\nCurrent: {}",
payload.message,
previous_status,
survivor.triage_status()
);
}
let priority = if is_upgrade {
Priority::from_triage(survivor.triage_status())
} else {
Priority::Medium
};
Ok(Alert::new(survivor.id().clone(), priority, payload))
}
/// Create alert payload from survivor data
fn create_payload(&self, survivor: &Survivor) -> AlertPayload {
let zone_name = self.zone_names
.get(survivor.zone_id())
.map(String::as_str)
.unwrap_or("Unknown Zone");
let title = format!(
"{} Survivor Detected - {}",
survivor.triage_status(),
zone_name
);
let vital_info = self.format_vital_signs(survivor);
let location_info = self.format_location(survivor);
let message = format!(
"Survivor ID: {}\n\
Zone: {}\n\
Triage: {}\n\
Confidence: {:.0}%\n\n\
Vital Signs:\n{}\n\n\
Location:\n{}",
survivor.id(),
zone_name,
survivor.triage_status(),
survivor.confidence() * 100.0,
vital_info,
location_info
);
let recommended_action = self.recommend_action(survivor);
AlertPayload::new(title, message, survivor.triage_status().clone())
.with_action(recommended_action)
.with_metadata("zone_id", survivor.zone_id().to_string())
.with_metadata("confidence", format!("{:.2}", survivor.confidence()))
}
/// Format vital signs for display
fn format_vital_signs(&self, survivor: &Survivor) -> String {
let vitals = survivor.vital_signs();
let mut lines = Vec::new();
if let Some(reading) = vitals.latest() {
if let Some(breathing) = &reading.breathing {
lines.push(format!(
" Breathing: {:.1} BPM ({:?})",
breathing.rate_bpm, breathing.pattern_type
));
} else {
lines.push(" Breathing: Not detected".to_string());
}
if let Some(heartbeat) = &reading.heartbeat {
lines.push(format!(
" Heartbeat: {:.0} BPM ({:?})",
heartbeat.rate_bpm, heartbeat.strength
));
}
lines.push(format!(
" Movement: {:?} (intensity: {:.1})",
reading.movement.movement_type,
reading.movement.intensity
));
} else {
lines.push(" No recent readings".to_string());
}
lines.join("\n")
}
/// Format location for display
fn format_location(&self, survivor: &Survivor) -> String {
match survivor.location() {
Some(loc) => {
let depth_str = if loc.is_buried() {
format!("{:.1}m below surface", loc.depth())
} else {
"At surface level".to_string()
};
format!(
" Position: ({:.1}, {:.1})\n\
Depth: {}\n\
Uncertainty: ±{:.1}m",
loc.x, loc.y,
depth_str,
loc.uncertainty.horizontal_error
)
}
None => " Position not yet determined".to_string(),
}
}
/// Recommend action based on triage status
fn recommend_action(&self, survivor: &Survivor) -> String {
match survivor.triage_status() {
TriageStatus::Immediate => {
"IMMEDIATE RESCUE REQUIRED. Deploy heavy rescue team. \
Prepare for airway management and critical care on extraction."
}
TriageStatus::Delayed => {
"Rescue team required. Mark location. Provide reassurance \
if communication is possible. Monitor for status changes."
}
TriageStatus::Minor => {
"Lower priority. Guide to extraction if conscious and mobile. \
Assign walking wounded assistance team."
}
TriageStatus::Deceased => {
"Mark location for recovery. Do not allocate rescue resources. \
Document for incident report."
}
TriageStatus::Unknown => {
"Requires additional assessment. Deploy scout team with \
enhanced detection equipment to confirm status."
}
}
.to_string()
}
}
impl Default for AlertGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore, VitalSignsReading};
use chrono::Utc;
fn create_test_survivor() -> Survivor {
let vitals = VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
};
Survivor::new(ScanZoneId::new(), vitals, None)
}
#[test]
fn test_generate_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let result = generator.generate(&survivor);
assert!(result.is_ok());
let alert = result.unwrap();
assert!(alert.is_pending());
}
#[test]
fn test_escalation_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let alert = generator.generate_escalation(&survivor, "Vital signs deteriorating")
.unwrap();
assert!(alert.payload().title.contains("ESCALATED"));
assert!(matches!(alert.priority(), Priority::Critical | Priority::High));
}
#[test]
fn test_status_change_alert() {
let generator = AlertGenerator::new();
let survivor = create_test_survivor();
let alert = generator.generate_status_change(
&survivor,
&TriageStatus::Minor,
).unwrap();
assert!(alert.payload().title.contains("Status Change"));
}
}