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
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
//! Alert dispatching and delivery.
|
||||
|
||||
use crate::domain::{Alert, AlertId, Priority, Survivor};
|
||||
use crate::MatError;
|
||||
use super::AlertGenerator;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Configuration for alert dispatch
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AlertConfig {
|
||||
/// Enable audio alerts
|
||||
pub audio_enabled: bool,
|
||||
/// Enable visual alerts
|
||||
pub visual_enabled: bool,
|
||||
/// Escalation timeout in seconds
|
||||
pub escalation_timeout_secs: u64,
|
||||
/// Maximum pending alerts before forced escalation
|
||||
pub max_pending_alerts: usize,
|
||||
/// Auto-acknowledge after seconds (0 = disabled)
|
||||
pub auto_ack_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for AlertConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
audio_enabled: true,
|
||||
visual_enabled: true,
|
||||
escalation_timeout_secs: 300, // 5 minutes
|
||||
max_pending_alerts: 50,
|
||||
auto_ack_secs: 0, // Disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatcher for sending alerts to rescue teams
|
||||
pub struct AlertDispatcher {
|
||||
config: AlertConfig,
|
||||
generator: AlertGenerator,
|
||||
pending_alerts: parking_lot::RwLock<HashMap<AlertId, Alert>>,
|
||||
handlers: Vec<Box<dyn AlertHandler>>,
|
||||
}
|
||||
|
||||
impl AlertDispatcher {
|
||||
/// Create a new alert dispatcher
|
||||
pub fn new(config: AlertConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
generator: AlertGenerator::new(),
|
||||
pending_alerts: parking_lot::RwLock::new(HashMap::new()),
|
||||
handlers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an alert handler
|
||||
pub fn add_handler(&mut self, handler: Box<dyn AlertHandler>) {
|
||||
self.handlers.push(handler);
|
||||
}
|
||||
|
||||
/// Generate an alert for a survivor
|
||||
pub fn generate_alert(&self, survivor: &Survivor) -> Result<Alert, MatError> {
|
||||
self.generator.generate(survivor)
|
||||
}
|
||||
|
||||
/// Dispatch an alert
|
||||
pub async fn dispatch(&self, alert: Alert) -> Result<(), MatError> {
|
||||
let alert_id = alert.id().clone();
|
||||
let priority = alert.priority();
|
||||
|
||||
// Store in pending alerts
|
||||
self.pending_alerts.write().insert(alert_id.clone(), alert.clone());
|
||||
|
||||
// Log the alert
|
||||
tracing::info!(
|
||||
alert_id = %alert_id,
|
||||
priority = ?priority,
|
||||
title = %alert.payload().title,
|
||||
"Dispatching alert"
|
||||
);
|
||||
|
||||
// Send to all handlers
|
||||
for handler in &self.handlers {
|
||||
if let Err(e) = handler.handle(&alert).await {
|
||||
tracing::warn!(
|
||||
alert_id = %alert_id,
|
||||
handler = %handler.name(),
|
||||
error = %e,
|
||||
"Handler failed to process alert"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're at capacity
|
||||
let pending_count = self.pending_alerts.read().len();
|
||||
if pending_count >= self.config.max_pending_alerts {
|
||||
tracing::warn!(
|
||||
pending_count,
|
||||
max = self.config.max_pending_alerts,
|
||||
"Alert capacity reached - escalating oldest alerts"
|
||||
);
|
||||
self.escalate_oldest().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Acknowledge an alert
|
||||
pub fn acknowledge(&self, alert_id: &AlertId, by: &str) -> Result<(), MatError> {
|
||||
let mut alerts = self.pending_alerts.write();
|
||||
|
||||
if let Some(alert) = alerts.get_mut(alert_id) {
|
||||
alert.acknowledge(by);
|
||||
tracing::info!(
|
||||
alert_id = %alert_id,
|
||||
acknowledged_by = by,
|
||||
"Alert acknowledged"
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(MatError::Alerting(format!("Alert {} not found", alert_id)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve an alert
|
||||
pub fn resolve(&self, alert_id: &AlertId, resolution: crate::domain::AlertResolution) -> Result<(), MatError> {
|
||||
let mut alerts = self.pending_alerts.write();
|
||||
|
||||
if let Some(alert) = alerts.remove(alert_id) {
|
||||
let mut resolved_alert = alert;
|
||||
resolved_alert.resolve(resolution);
|
||||
tracing::info!(
|
||||
alert_id = %alert_id,
|
||||
"Alert resolved"
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(MatError::Alerting(format!("Alert {} not found", alert_id)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all pending alerts
|
||||
pub fn pending(&self) -> Vec<Alert> {
|
||||
self.pending_alerts.read().values().cloned().collect()
|
||||
}
|
||||
|
||||
/// Get pending alerts by priority
|
||||
pub fn pending_by_priority(&self, priority: Priority) -> Vec<Alert> {
|
||||
self.pending_alerts
|
||||
.read()
|
||||
.values()
|
||||
.filter(|a| a.priority() == priority)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get count of pending alerts
|
||||
pub fn pending_count(&self) -> usize {
|
||||
self.pending_alerts.read().len()
|
||||
}
|
||||
|
||||
/// Check and escalate timed-out alerts
|
||||
pub async fn check_escalations(&self) -> Result<u32, MatError> {
|
||||
let timeout_secs = self.config.escalation_timeout_secs as i64;
|
||||
let mut escalated = 0;
|
||||
|
||||
let mut to_escalate = Vec::new();
|
||||
{
|
||||
let alerts = self.pending_alerts.read();
|
||||
for (id, alert) in alerts.iter() {
|
||||
if alert.needs_escalation(timeout_secs) {
|
||||
to_escalate.push(id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id in to_escalate {
|
||||
let mut alerts = self.pending_alerts.write();
|
||||
if let Some(alert) = alerts.get_mut(&id) {
|
||||
alert.escalate();
|
||||
escalated += 1;
|
||||
|
||||
tracing::warn!(
|
||||
alert_id = %id,
|
||||
new_priority = ?alert.priority(),
|
||||
"Alert escalated due to timeout"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(escalated)
|
||||
}
|
||||
|
||||
/// Escalate oldest pending alerts
|
||||
async fn escalate_oldest(&self) -> Result<(), MatError> {
|
||||
let mut alerts: Vec<_> = self.pending_alerts.read()
|
||||
.iter()
|
||||
.map(|(id, alert)| (id.clone(), *alert.created_at()))
|
||||
.collect();
|
||||
|
||||
// Sort by creation time (oldest first)
|
||||
alerts.sort_by_key(|(_, created)| *created);
|
||||
|
||||
// Escalate oldest 10%
|
||||
let to_escalate = (alerts.len() / 10).max(1);
|
||||
|
||||
let mut pending = self.pending_alerts.write();
|
||||
for (id, _) in alerts.into_iter().take(to_escalate) {
|
||||
if let Some(alert) = pending.get_mut(&id) {
|
||||
alert.escalate();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get configuration
|
||||
pub fn config(&self) -> &AlertConfig {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for processing alerts
|
||||
#[async_trait::async_trait]
|
||||
pub trait AlertHandler: Send + Sync {
|
||||
/// Handler name
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Handle an alert
|
||||
async fn handle(&self, alert: &Alert) -> Result<(), MatError>;
|
||||
}
|
||||
|
||||
/// Console/logging alert handler
|
||||
pub struct ConsoleAlertHandler;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AlertHandler for ConsoleAlertHandler {
|
||||
fn name(&self) -> &str {
|
||||
"console"
|
||||
}
|
||||
|
||||
async fn handle(&self, alert: &Alert) -> Result<(), MatError> {
|
||||
let priority_indicator = match alert.priority() {
|
||||
Priority::Critical => "🔴",
|
||||
Priority::High => "🟠",
|
||||
Priority::Medium => "🟡",
|
||||
Priority::Low => "🔵",
|
||||
};
|
||||
|
||||
println!("\n{} ALERT {}", priority_indicator, "=".repeat(50));
|
||||
println!("ID: {}", alert.id());
|
||||
println!("Priority: {:?}", alert.priority());
|
||||
println!("Title: {}", alert.payload().title);
|
||||
println!("{}", "=".repeat(60));
|
||||
println!("{}", alert.payload().message);
|
||||
println!("{}", "=".repeat(60));
|
||||
println!("Recommended Action: {}", alert.payload().recommended_action);
|
||||
println!("{}\n", "=".repeat(60));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Audio alert handler (placeholder)
|
||||
pub struct AudioAlertHandler;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AlertHandler for AudioAlertHandler {
|
||||
fn name(&self) -> &str {
|
||||
"audio"
|
||||
}
|
||||
|
||||
async fn handle(&self, alert: &Alert) -> Result<(), MatError> {
|
||||
// In production, this would trigger actual audio alerts
|
||||
let pattern = alert.priority().audio_pattern();
|
||||
tracing::debug!(
|
||||
alert_id = %alert.id(),
|
||||
pattern,
|
||||
"Would play audio alert"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::{SurvivorId, TriageStatus, AlertPayload};
|
||||
|
||||
fn create_test_alert() -> Alert {
|
||||
Alert::new(
|
||||
SurvivorId::new(),
|
||||
Priority::High,
|
||||
AlertPayload::new("Test Alert", "Test message", TriageStatus::Delayed),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dispatch_alert() {
|
||||
let dispatcher = AlertDispatcher::new(AlertConfig::default());
|
||||
let alert = create_test_alert();
|
||||
|
||||
let result = dispatcher.dispatch(alert).await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(dispatcher.pending_count(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_acknowledge_alert() {
|
||||
let dispatcher = AlertDispatcher::new(AlertConfig::default());
|
||||
let alert = create_test_alert();
|
||||
let alert_id = alert.id().clone();
|
||||
|
||||
dispatcher.dispatch(alert).await.unwrap();
|
||||
|
||||
let result = dispatcher.acknowledge(&alert_id, "Team Alpha");
|
||||
assert!(result.is_ok());
|
||||
|
||||
let pending = dispatcher.pending();
|
||||
assert!(pending.iter().any(|a| a.id() == &alert_id && a.acknowledged_by() == Some("Team Alpha")));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_resolve_alert() {
|
||||
let dispatcher = AlertDispatcher::new(AlertConfig::default());
|
||||
let alert = create_test_alert();
|
||||
let alert_id = alert.id().clone();
|
||||
|
||||
dispatcher.dispatch(alert).await.unwrap();
|
||||
|
||||
let resolution = crate::domain::AlertResolution {
|
||||
resolution_type: crate::domain::ResolutionType::Rescued,
|
||||
notes: "Survivor extracted successfully".to_string(),
|
||||
resolved_by: Some("Team Alpha".to_string()),
|
||||
resolved_at: chrono::Utc::now(),
|
||||
};
|
||||
|
||||
dispatcher.resolve(&alert_id, resolution).unwrap();
|
||||
assert_eq!(dispatcher.pending_count(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Alerting module for emergency notifications.
|
||||
|
||||
mod generator;
|
||||
mod dispatcher;
|
||||
mod triage_service;
|
||||
|
||||
pub use generator::AlertGenerator;
|
||||
pub use dispatcher::{AlertDispatcher, AlertConfig};
|
||||
pub use triage_service::{TriageService, PriorityCalculator};
|
||||
@@ -0,0 +1,317 @@
|
||||
//! Triage service for calculating and updating survivor priority.
|
||||
|
||||
use crate::domain::{
|
||||
Priority, Survivor, TriageStatus, VitalSignsReading,
|
||||
triage::TriageCalculator,
|
||||
};
|
||||
|
||||
/// Service for triage operations
|
||||
pub struct TriageService;
|
||||
|
||||
impl TriageService {
|
||||
/// Calculate triage status from vital signs
|
||||
pub fn calculate_triage(vitals: &VitalSignsReading) -> TriageStatus {
|
||||
TriageCalculator::calculate(vitals)
|
||||
}
|
||||
|
||||
/// Check if survivor should be upgraded
|
||||
pub fn should_upgrade(survivor: &Survivor) -> bool {
|
||||
TriageCalculator::should_upgrade(
|
||||
survivor.triage_status(),
|
||||
survivor.is_deteriorating(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get upgraded status
|
||||
pub fn upgrade_status(current: &TriageStatus) -> TriageStatus {
|
||||
TriageCalculator::upgrade(current)
|
||||
}
|
||||
|
||||
/// Evaluate overall severity for multiple survivors
|
||||
pub fn evaluate_mass_casualty(survivors: &[&Survivor]) -> MassCasualtyAssessment {
|
||||
let total = survivors.len() as u32;
|
||||
|
||||
let mut immediate = 0u32;
|
||||
let mut delayed = 0u32;
|
||||
let mut minor = 0u32;
|
||||
let mut deceased = 0u32;
|
||||
let mut unknown = 0u32;
|
||||
|
||||
for survivor in survivors {
|
||||
match survivor.triage_status() {
|
||||
TriageStatus::Immediate => immediate += 1,
|
||||
TriageStatus::Delayed => delayed += 1,
|
||||
TriageStatus::Minor => minor += 1,
|
||||
TriageStatus::Deceased => deceased += 1,
|
||||
TriageStatus::Unknown => unknown += 1,
|
||||
}
|
||||
}
|
||||
|
||||
let severity = Self::calculate_severity(immediate, delayed, total);
|
||||
let resource_level = Self::calculate_resource_level(immediate, delayed, minor);
|
||||
|
||||
MassCasualtyAssessment {
|
||||
total,
|
||||
immediate,
|
||||
delayed,
|
||||
minor,
|
||||
deceased,
|
||||
unknown,
|
||||
severity,
|
||||
resource_level,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate overall severity level
|
||||
fn calculate_severity(immediate: u32, delayed: u32, total: u32) -> SeverityLevel {
|
||||
if total == 0 {
|
||||
return SeverityLevel::Minimal;
|
||||
}
|
||||
|
||||
let critical_ratio = (immediate + delayed) as f64 / total as f64;
|
||||
|
||||
if immediate >= 10 || critical_ratio > 0.5 {
|
||||
SeverityLevel::Critical
|
||||
} else if immediate >= 5 || critical_ratio > 0.3 {
|
||||
SeverityLevel::Major
|
||||
} else if immediate >= 1 || critical_ratio > 0.1 {
|
||||
SeverityLevel::Moderate
|
||||
} else {
|
||||
SeverityLevel::Minimal
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate resource level needed
|
||||
fn calculate_resource_level(immediate: u32, delayed: u32, minor: u32) -> ResourceLevel {
|
||||
// Each immediate needs ~4 rescuers
|
||||
// Each delayed needs ~2 rescuers
|
||||
// Each minor needs ~0.5 rescuers
|
||||
let rescuers_needed = immediate * 4 + delayed * 2 + minor / 2;
|
||||
|
||||
if rescuers_needed >= 100 {
|
||||
ResourceLevel::MutualAid
|
||||
} else if rescuers_needed >= 50 {
|
||||
ResourceLevel::MultiAgency
|
||||
} else if rescuers_needed >= 20 {
|
||||
ResourceLevel::Enhanced
|
||||
} else if rescuers_needed >= 5 {
|
||||
ResourceLevel::Standard
|
||||
} else {
|
||||
ResourceLevel::Minimal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculator for alert priority
|
||||
pub struct PriorityCalculator;
|
||||
|
||||
impl PriorityCalculator {
|
||||
/// Calculate priority from triage status
|
||||
pub fn from_triage(status: &TriageStatus) -> Priority {
|
||||
Priority::from_triage(status)
|
||||
}
|
||||
|
||||
/// Calculate priority with additional factors
|
||||
pub fn calculate_with_factors(
|
||||
status: &TriageStatus,
|
||||
deteriorating: bool,
|
||||
time_since_detection_mins: u64,
|
||||
depth_meters: Option<f64>,
|
||||
) -> Priority {
|
||||
let base_priority = Priority::from_triage(status);
|
||||
|
||||
// Adjust for deterioration
|
||||
let priority = if deteriorating && base_priority != Priority::Critical {
|
||||
match base_priority {
|
||||
Priority::High => Priority::Critical,
|
||||
Priority::Medium => Priority::High,
|
||||
Priority::Low => Priority::Medium,
|
||||
Priority::Critical => Priority::Critical,
|
||||
}
|
||||
} else {
|
||||
base_priority
|
||||
};
|
||||
|
||||
// Adjust for time (longer = more urgent)
|
||||
let priority = if time_since_detection_mins > 30 && priority == Priority::Medium {
|
||||
Priority::High
|
||||
} else {
|
||||
priority
|
||||
};
|
||||
|
||||
// Adjust for depth (deeper = more complex rescue)
|
||||
if let Some(depth) = depth_meters {
|
||||
if depth > 3.0 && priority == Priority::High {
|
||||
return Priority::Critical;
|
||||
}
|
||||
}
|
||||
|
||||
priority
|
||||
}
|
||||
}
|
||||
|
||||
/// Mass casualty assessment result
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MassCasualtyAssessment {
|
||||
/// Total survivors detected
|
||||
pub total: u32,
|
||||
/// Immediate (Red) count
|
||||
pub immediate: u32,
|
||||
/// Delayed (Yellow) count
|
||||
pub delayed: u32,
|
||||
/// Minor (Green) count
|
||||
pub minor: u32,
|
||||
/// Deceased (Black) count
|
||||
pub deceased: u32,
|
||||
/// Unknown count
|
||||
pub unknown: u32,
|
||||
/// Overall severity level
|
||||
pub severity: SeverityLevel,
|
||||
/// Resource level needed
|
||||
pub resource_level: ResourceLevel,
|
||||
}
|
||||
|
||||
impl MassCasualtyAssessment {
|
||||
/// Get count of living survivors
|
||||
pub fn living(&self) -> u32 {
|
||||
self.immediate + self.delayed + self.minor
|
||||
}
|
||||
|
||||
/// Get count needing active rescue
|
||||
pub fn needs_rescue(&self) -> u32 {
|
||||
self.immediate + self.delayed
|
||||
}
|
||||
|
||||
/// Get summary string
|
||||
pub fn summary(&self) -> String {
|
||||
format!(
|
||||
"MCI Assessment:\n\
|
||||
Total: {} (Living: {}, Deceased: {})\n\
|
||||
Immediate: {}, Delayed: {}, Minor: {}\n\
|
||||
Severity: {:?}, Resources: {:?}",
|
||||
self.total, self.living(), self.deceased,
|
||||
self.immediate, self.delayed, self.minor,
|
||||
self.severity, self.resource_level
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Severity levels for mass casualty incidents
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SeverityLevel {
|
||||
/// Few or no critical patients
|
||||
Minimal,
|
||||
/// Some critical patients, manageable
|
||||
Moderate,
|
||||
/// Many critical patients, challenging
|
||||
Major,
|
||||
/// Overwhelming number of critical patients
|
||||
Critical,
|
||||
}
|
||||
|
||||
/// Resource levels for response
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ResourceLevel {
|
||||
/// Standard response adequate
|
||||
Minimal,
|
||||
/// Standard response needed
|
||||
Standard,
|
||||
/// Enhanced response needed
|
||||
Enhanced,
|
||||
/// Multi-agency response needed
|
||||
MultiAgency,
|
||||
/// Regional mutual aid required
|
||||
MutualAid,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::domain::{
|
||||
BreathingPattern, BreathingType, ConfidenceScore, ScanZoneId,
|
||||
};
|
||||
use chrono::Utc;
|
||||
|
||||
fn create_test_vitals(rate_bpm: f32) -> VitalSignsReading {
|
||||
VitalSignsReading {
|
||||
breathing: Some(BreathingPattern {
|
||||
rate_bpm,
|
||||
amplitude: 0.8,
|
||||
regularity: 0.9,
|
||||
pattern_type: BreathingType::Normal,
|
||||
}),
|
||||
heartbeat: None,
|
||||
movement: Default::default(),
|
||||
timestamp: Utc::now(),
|
||||
confidence: ConfidenceScore::new(0.8),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_triage() {
|
||||
let normal = create_test_vitals(16.0);
|
||||
assert!(matches!(
|
||||
TriageService::calculate_triage(&normal),
|
||||
TriageStatus::Immediate | TriageStatus::Delayed | TriageStatus::Minor
|
||||
));
|
||||
|
||||
let fast = create_test_vitals(35.0);
|
||||
assert!(matches!(
|
||||
TriageService::calculate_triage(&fast),
|
||||
TriageStatus::Immediate
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_from_triage() {
|
||||
assert_eq!(
|
||||
PriorityCalculator::from_triage(&TriageStatus::Immediate),
|
||||
Priority::Critical
|
||||
);
|
||||
assert_eq!(
|
||||
PriorityCalculator::from_triage(&TriageStatus::Delayed),
|
||||
Priority::High
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mass_casualty_assessment() {
|
||||
let survivors: Vec<Survivor> = (0..10)
|
||||
.map(|i| {
|
||||
let rate = if i < 3 { 35.0 } else if i < 6 { 16.0 } else { 18.0 };
|
||||
Survivor::new(
|
||||
ScanZoneId::new(),
|
||||
create_test_vitals(rate),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let survivor_refs: Vec<&Survivor> = survivors.iter().collect();
|
||||
let assessment = TriageService::evaluate_mass_casualty(&survivor_refs);
|
||||
|
||||
assert_eq!(assessment.total, 10);
|
||||
assert!(assessment.living() >= assessment.needs_rescue());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_with_factors() {
|
||||
// Deteriorating patient should be upgraded
|
||||
let priority = PriorityCalculator::calculate_with_factors(
|
||||
&TriageStatus::Delayed,
|
||||
true,
|
||||
0,
|
||||
None,
|
||||
);
|
||||
assert_eq!(priority, Priority::Critical);
|
||||
|
||||
// Deep burial should upgrade
|
||||
let priority = PriorityCalculator::calculate_with_factors(
|
||||
&TriageStatus::Delayed,
|
||||
false,
|
||||
0,
|
||||
Some(4.0),
|
||||
);
|
||||
assert_eq!(priority, Priority::Critical);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user