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:
Claude
2026-01-13 17:24:50 +00:00
parent 0fa9a0b882
commit a17b630c02
31 changed files with 9042 additions and 0 deletions

View File

@@ -0,0 +1,458 @@
//! Alert types for emergency notifications.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{SurvivorId, TriageStatus, Coordinates3D};
/// Unique identifier for an alert
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertId(Uuid);
impl AlertId {
/// Create a new random alert ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for AlertId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for AlertId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Alert priority levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Priority {
/// Critical - immediate action required
Critical = 1,
/// High - urgent attention needed
High = 2,
/// Medium - important but not urgent
Medium = 3,
/// Low - informational
Low = 4,
}
impl Priority {
/// Create from triage status
pub fn from_triage(status: &TriageStatus) -> Self {
match status {
TriageStatus::Immediate => Priority::Critical,
TriageStatus::Delayed => Priority::High,
TriageStatus::Minor => Priority::Medium,
TriageStatus::Deceased => Priority::Low,
TriageStatus::Unknown => Priority::Medium,
}
}
/// Get numeric value (lower = higher priority)
pub fn value(&self) -> u8 {
*self as u8
}
/// Get display color
pub fn color(&self) -> &'static str {
match self {
Priority::Critical => "red",
Priority::High => "orange",
Priority::Medium => "yellow",
Priority::Low => "blue",
}
}
/// Get sound pattern for audio alerts
pub fn audio_pattern(&self) -> &'static str {
match self {
Priority::Critical => "rapid_beep",
Priority::High => "double_beep",
Priority::Medium => "single_beep",
Priority::Low => "soft_tone",
}
}
}
impl std::fmt::Display for Priority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Priority::Critical => write!(f, "CRITICAL"),
Priority::High => write!(f, "HIGH"),
Priority::Medium => write!(f, "MEDIUM"),
Priority::Low => write!(f, "LOW"),
}
}
}
/// Payload containing alert details
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertPayload {
/// Human-readable title
pub title: String,
/// Detailed message
pub message: String,
/// Triage status of survivor
pub triage_status: TriageStatus,
/// Location if known
pub location: Option<Coordinates3D>,
/// Recommended action
pub recommended_action: String,
/// Time-critical deadline (if any)
pub deadline: Option<DateTime<Utc>>,
/// Additional metadata
pub metadata: std::collections::HashMap<String, String>,
}
impl AlertPayload {
/// Create a new alert payload
pub fn new(
title: impl Into<String>,
message: impl Into<String>,
triage_status: TriageStatus,
) -> Self {
Self {
title: title.into(),
message: message.into(),
triage_status,
location: None,
recommended_action: String::new(),
metadata: std::collections::HashMap::new(),
}
}
/// Set location
pub fn with_location(mut self, location: Coordinates3D) -> Self {
self.location = Some(location);
self
}
/// Set recommended action
pub fn with_action(mut self, action: impl Into<String>) -> Self {
self.recommended_action = action.into();
self
}
/// Set deadline
pub fn with_deadline(mut self, deadline: DateTime<Utc>) -> Self {
self.deadline = Some(deadline);
self
}
/// Add metadata
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
/// Status of an alert
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AlertStatus {
/// Alert is pending acknowledgement
Pending,
/// Alert has been acknowledged
Acknowledged,
/// Alert is being worked on
InProgress,
/// Alert has been resolved
Resolved,
/// Alert was cancelled/superseded
Cancelled,
/// Alert expired without action
Expired,
}
/// Resolution details for a closed alert
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AlertResolution {
/// Resolution type
pub resolution_type: ResolutionType,
/// Resolution notes
pub notes: String,
/// Team that resolved
pub resolved_by: Option<String>,
/// Resolution time
pub resolved_at: DateTime<Utc>,
}
/// Types of alert resolution
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ResolutionType {
/// Survivor was rescued
Rescued,
/// Alert was a false positive
FalsePositive,
/// Survivor deceased before rescue
Deceased,
/// Alert superseded by new information
Superseded,
/// Alert timed out
TimedOut,
/// Other resolution
Other,
}
/// An alert for rescue teams
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Alert {
id: AlertId,
survivor_id: SurvivorId,
priority: Priority,
payload: AlertPayload,
status: AlertStatus,
created_at: DateTime<Utc>,
acknowledged_at: Option<DateTime<Utc>>,
acknowledged_by: Option<String>,
resolution: Option<AlertResolution>,
escalation_count: u32,
}
impl Alert {
/// Create a new alert
pub fn new(survivor_id: SurvivorId, priority: Priority, payload: AlertPayload) -> Self {
Self {
id: AlertId::new(),
survivor_id,
priority,
payload,
status: AlertStatus::Pending,
created_at: Utc::now(),
acknowledged_at: None,
acknowledged_by: None,
resolution: None,
escalation_count: 0,
}
}
/// Get the alert ID
pub fn id(&self) -> &AlertId {
&self.id
}
/// Get the survivor ID
pub fn survivor_id(&self) -> &SurvivorId {
&self.survivor_id
}
/// Get the priority
pub fn priority(&self) -> Priority {
self.priority
}
/// Get the payload
pub fn payload(&self) -> &AlertPayload {
&self.payload
}
/// Get the status
pub fn status(&self) -> &AlertStatus {
&self.status
}
/// Get creation time
pub fn created_at(&self) -> &DateTime<Utc> {
&self.created_at
}
/// Get acknowledgement time
pub fn acknowledged_at(&self) -> Option<&DateTime<Utc>> {
self.acknowledged_at.as_ref()
}
/// Get who acknowledged
pub fn acknowledged_by(&self) -> Option<&str> {
self.acknowledged_by.as_deref()
}
/// Get resolution
pub fn resolution(&self) -> Option<&AlertResolution> {
self.resolution.as_ref()
}
/// Get escalation count
pub fn escalation_count(&self) -> u32 {
self.escalation_count
}
/// Acknowledge the alert
pub fn acknowledge(&mut self, by: impl Into<String>) {
self.status = AlertStatus::Acknowledged;
self.acknowledged_at = Some(Utc::now());
self.acknowledged_by = Some(by.into());
}
/// Mark as in progress
pub fn start_work(&mut self) {
if self.status == AlertStatus::Acknowledged {
self.status = AlertStatus::InProgress;
}
}
/// Resolve the alert
pub fn resolve(&mut self, resolution: AlertResolution) {
self.status = AlertStatus::Resolved;
self.resolution = Some(resolution);
}
/// Cancel the alert
pub fn cancel(&mut self, reason: &str) {
self.status = AlertStatus::Cancelled;
self.resolution = Some(AlertResolution {
resolution_type: ResolutionType::Other,
notes: reason.to_string(),
resolved_by: None,
resolved_at: Utc::now(),
});
}
/// Escalate the alert (increase priority)
pub fn escalate(&mut self) {
self.escalation_count += 1;
if self.priority != Priority::Critical {
self.priority = match self.priority {
Priority::Low => Priority::Medium,
Priority::Medium => Priority::High,
Priority::High => Priority::Critical,
Priority::Critical => Priority::Critical,
};
}
}
/// Check if alert is pending
pub fn is_pending(&self) -> bool {
self.status == AlertStatus::Pending
}
/// Check if alert is active (not resolved/cancelled)
pub fn is_active(&self) -> bool {
matches!(
self.status,
AlertStatus::Pending | AlertStatus::Acknowledged | AlertStatus::InProgress
)
}
/// Time since alert was created
pub fn age(&self) -> chrono::Duration {
Utc::now() - self.created_at
}
/// Time since acknowledgement
pub fn time_since_ack(&self) -> Option<chrono::Duration> {
self.acknowledged_at.map(|t| Utc::now() - t)
}
/// Check if alert needs escalation based on time
pub fn needs_escalation(&self, max_pending_seconds: i64) -> bool {
if !self.is_pending() {
return false;
}
self.age().num_seconds() > max_pending_seconds
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_payload() -> AlertPayload {
AlertPayload::new(
"Survivor Detected",
"Vital signs detected in Zone A",
TriageStatus::Immediate,
)
}
#[test]
fn test_alert_creation() {
let survivor_id = SurvivorId::new();
let alert = Alert::new(
survivor_id.clone(),
Priority::Critical,
create_test_payload(),
);
assert_eq!(alert.survivor_id(), &survivor_id);
assert_eq!(alert.priority(), Priority::Critical);
assert!(alert.is_pending());
assert!(alert.is_active());
}
#[test]
fn test_alert_lifecycle() {
let mut alert = Alert::new(
SurvivorId::new(),
Priority::High,
create_test_payload(),
);
// Initial state
assert!(alert.is_pending());
// Acknowledge
alert.acknowledge("Team Alpha");
assert_eq!(alert.status(), &AlertStatus::Acknowledged);
assert_eq!(alert.acknowledged_by(), Some("Team Alpha"));
// Start work
alert.start_work();
assert_eq!(alert.status(), &AlertStatus::InProgress);
// Resolve
alert.resolve(AlertResolution {
resolution_type: ResolutionType::Rescued,
notes: "Survivor extracted successfully".to_string(),
resolved_by: Some("Team Alpha".to_string()),
resolved_at: Utc::now(),
});
assert_eq!(alert.status(), &AlertStatus::Resolved);
assert!(!alert.is_active());
}
#[test]
fn test_alert_escalation() {
let mut alert = Alert::new(
SurvivorId::new(),
Priority::Low,
create_test_payload(),
);
alert.escalate();
assert_eq!(alert.priority(), Priority::Medium);
assert_eq!(alert.escalation_count(), 1);
alert.escalate();
assert_eq!(alert.priority(), Priority::High);
alert.escalate();
assert_eq!(alert.priority(), Priority::Critical);
// Can't escalate beyond critical
alert.escalate();
assert_eq!(alert.priority(), Priority::Critical);
}
#[test]
fn test_priority_from_triage() {
assert_eq!(Priority::from_triage(&TriageStatus::Immediate), Priority::Critical);
assert_eq!(Priority::from_triage(&TriageStatus::Delayed), Priority::High);
assert_eq!(Priority::from_triage(&TriageStatus::Minor), Priority::Medium);
}
}

View File

@@ -0,0 +1,371 @@
//! 3D coordinate system and location types for survivor localization.
/// 3D coordinates representing survivor position
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Coordinates3D {
/// East-West offset from reference point (meters)
pub x: f64,
/// North-South offset from reference point (meters)
pub y: f64,
/// Vertical offset - negative is below surface (meters)
pub z: f64,
/// Uncertainty bounds for this position
pub uncertainty: LocationUncertainty,
}
impl Coordinates3D {
/// Create new coordinates with uncertainty
pub fn new(x: f64, y: f64, z: f64, uncertainty: LocationUncertainty) -> Self {
Self { x, y, z, uncertainty }
}
/// Create coordinates with default uncertainty
pub fn with_default_uncertainty(x: f64, y: f64, z: f64) -> Self {
Self {
x,
y,
z,
uncertainty: LocationUncertainty::default(),
}
}
/// Calculate 3D distance to another point
pub fn distance_to(&self, other: &Coordinates3D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
let dz = self.z - other.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
/// Calculate horizontal (2D) distance only
pub fn horizontal_distance_to(&self, other: &Coordinates3D) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
/// Get depth below surface (positive value)
pub fn depth(&self) -> f64 {
-self.z.min(0.0)
}
/// Check if position is below surface
pub fn is_buried(&self) -> bool {
self.z < 0.0
}
/// Get the 95% confidence radius (horizontal)
pub fn confidence_radius(&self) -> f64 {
self.uncertainty.horizontal_error
}
}
/// Uncertainty bounds for a position estimate
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct LocationUncertainty {
/// Horizontal error radius at 95% confidence (meters)
pub horizontal_error: f64,
/// Vertical error at 95% confidence (meters)
pub vertical_error: f64,
/// Confidence level (0.0-1.0)
pub confidence: f64,
}
impl Default for LocationUncertainty {
fn default() -> Self {
Self {
horizontal_error: 2.0, // 2 meter default uncertainty
vertical_error: 1.0, // 1 meter vertical uncertainty
confidence: 0.95, // 95% confidence
}
}
}
impl LocationUncertainty {
/// Create uncertainty with specific error bounds
pub fn new(horizontal_error: f64, vertical_error: f64) -> Self {
Self {
horizontal_error,
vertical_error,
confidence: 0.95,
}
}
/// Create high-confidence uncertainty
pub fn high_confidence(horizontal_error: f64, vertical_error: f64) -> Self {
Self {
horizontal_error,
vertical_error,
confidence: 0.99,
}
}
/// Check if uncertainty is acceptable for rescue operations
pub fn is_actionable(&self) -> bool {
// Within 3 meters horizontal is generally actionable
self.horizontal_error <= 3.0 && self.confidence >= 0.8
}
/// Combine two uncertainties (for sensor fusion)
pub fn combine(&self, other: &LocationUncertainty) -> LocationUncertainty {
// Weighted combination based on confidence
let total_conf = self.confidence + other.confidence;
let w1 = self.confidence / total_conf;
let w2 = other.confidence / total_conf;
// Combined uncertainty is reduced when multiple estimates agree
let h_var1 = self.horizontal_error * self.horizontal_error;
let h_var2 = other.horizontal_error * other.horizontal_error;
let combined_h_var = 1.0 / (1.0/h_var1 + 1.0/h_var2);
let v_var1 = self.vertical_error * self.vertical_error;
let v_var2 = other.vertical_error * other.vertical_error;
let combined_v_var = 1.0 / (1.0/v_var1 + 1.0/v_var2);
LocationUncertainty {
horizontal_error: combined_h_var.sqrt(),
vertical_error: combined_v_var.sqrt(),
confidence: (w1 * self.confidence + w2 * other.confidence).min(0.99),
}
}
}
/// Depth estimate with debris profile
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DepthEstimate {
/// Estimated depth in meters
pub depth: f64,
/// Uncertainty range (plus/minus)
pub uncertainty: f64,
/// Estimated debris composition
pub debris_profile: DebrisProfile,
/// Confidence in the estimate
pub confidence: f64,
}
impl DepthEstimate {
/// Create a new depth estimate
pub fn new(
depth: f64,
uncertainty: f64,
debris_profile: DebrisProfile,
confidence: f64,
) -> Self {
Self {
depth,
uncertainty,
debris_profile,
confidence,
}
}
/// Get minimum possible depth
pub fn min_depth(&self) -> f64 {
(self.depth - self.uncertainty).max(0.0)
}
/// Get maximum possible depth
pub fn max_depth(&self) -> f64 {
self.depth + self.uncertainty
}
/// Check if depth is shallow (easier rescue)
pub fn is_shallow(&self) -> bool {
self.depth < 1.5
}
/// Check if depth is moderate
pub fn is_moderate(&self) -> bool {
self.depth >= 1.5 && self.depth < 3.0
}
/// Check if depth is deep (difficult rescue)
pub fn is_deep(&self) -> bool {
self.depth >= 3.0
}
}
/// Profile of debris material between sensor and survivor
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DebrisProfile {
/// Primary material type
pub primary_material: DebrisMaterial,
/// Estimated void fraction (0.0-1.0, higher = more air gaps)
pub void_fraction: f64,
/// Estimated moisture content (affects signal propagation)
pub moisture_content: MoistureLevel,
/// Whether metal content is detected (blocks signals)
pub metal_content: MetalContent,
}
impl Default for DebrisProfile {
fn default() -> Self {
Self {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.3,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::None,
}
}
}
impl DebrisProfile {
/// Calculate signal attenuation factor
pub fn attenuation_factor(&self) -> f64 {
let base = self.primary_material.attenuation_coefficient();
let moisture_factor = self.moisture_content.attenuation_multiplier();
let void_factor = 1.0 - (self.void_fraction * 0.3); // Voids reduce attenuation
base * moisture_factor * void_factor
}
/// Check if debris allows good signal penetration
pub fn is_penetrable(&self) -> bool {
!matches!(self.metal_content, MetalContent::High | MetalContent::Blocking)
&& self.primary_material.attenuation_coefficient() < 5.0
}
}
/// Types of debris materials
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DebrisMaterial {
/// Lightweight concrete, drywall
LightConcrete,
/// Heavy concrete, brick
HeavyConcrete,
/// Wooden structures
Wood,
/// Soil, earth
Soil,
/// Mixed rubble (typical collapse)
Mixed,
/// Snow/ice (avalanche)
Snow,
/// Metal (poor penetration)
Metal,
}
impl DebrisMaterial {
/// Get RF attenuation coefficient (dB/meter)
pub fn attenuation_coefficient(&self) -> f64 {
match self {
DebrisMaterial::Snow => 0.5,
DebrisMaterial::Wood => 1.5,
DebrisMaterial::LightConcrete => 3.0,
DebrisMaterial::Soil => 4.0,
DebrisMaterial::Mixed => 4.5,
DebrisMaterial::HeavyConcrete => 6.0,
DebrisMaterial::Metal => 20.0,
}
}
}
/// Moisture level in debris
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MoistureLevel {
/// Dry conditions
Dry,
/// Slightly damp
Damp,
/// Wet (rain, flooding)
Wet,
/// Saturated (submerged)
Saturated,
}
impl MoistureLevel {
/// Get attenuation multiplier
pub fn attenuation_multiplier(&self) -> f64 {
match self {
MoistureLevel::Dry => 1.0,
MoistureLevel::Damp => 1.3,
MoistureLevel::Wet => 1.8,
MoistureLevel::Saturated => 2.5,
}
}
}
/// Metal content in debris
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MetalContent {
/// No significant metal
None,
/// Low metal content (rebar, pipes)
Low,
/// Moderate metal (structural steel)
Moderate,
/// High metal content
High,
/// Metal is blocking signal
Blocking,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_distance_calculation() {
let p1 = Coordinates3D::with_default_uncertainty(0.0, 0.0, 0.0);
let p2 = Coordinates3D::with_default_uncertainty(3.0, 4.0, 0.0);
assert!((p1.distance_to(&p2) - 5.0).abs() < 0.001);
assert!((p1.horizontal_distance_to(&p2) - 5.0).abs() < 0.001);
}
#[test]
fn test_depth_calculation() {
let surface = Coordinates3D::with_default_uncertainty(0.0, 0.0, 0.0);
assert!(!surface.is_buried());
assert!(surface.depth().abs() < 0.001);
let buried = Coordinates3D::with_default_uncertainty(0.0, 0.0, -2.5);
assert!(buried.is_buried());
assert!((buried.depth() - 2.5).abs() < 0.001);
}
#[test]
fn test_uncertainty_combination() {
let u1 = LocationUncertainty::new(2.0, 1.0);
let u2 = LocationUncertainty::new(2.0, 1.0);
let combined = u1.combine(&u2);
// Combined uncertainty should be lower than individual
assert!(combined.horizontal_error < u1.horizontal_error);
}
#[test]
fn test_depth_estimate_categories() {
let shallow = DepthEstimate::new(1.0, 0.2, DebrisProfile::default(), 0.8);
assert!(shallow.is_shallow());
let moderate = DepthEstimate::new(2.0, 0.3, DebrisProfile::default(), 0.7);
assert!(moderate.is_moderate());
let deep = DepthEstimate::new(4.0, 0.5, DebrisProfile::default(), 0.6);
assert!(deep.is_deep());
}
#[test]
fn test_debris_attenuation() {
let snow = DebrisProfile {
primary_material: DebrisMaterial::Snow,
..Default::default()
};
let concrete = DebrisProfile {
primary_material: DebrisMaterial::HeavyConcrete,
..Default::default()
};
assert!(snow.attenuation_factor() < concrete.attenuation_factor());
assert!(snow.is_penetrable());
}
}

View File

@@ -0,0 +1,490 @@
//! Disaster event aggregate root.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use geo::Point;
use super::{
Survivor, SurvivorId, ScanZone, ScanZoneId,
VitalSignsReading, Coordinates3D,
};
use crate::MatError;
/// Unique identifier for a disaster event
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DisasterEventId(Uuid);
impl DisasterEventId {
/// Create a new random event ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for DisasterEventId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for DisasterEventId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Types of disaster events
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DisasterType {
/// Building collapse (explosion, structural failure)
BuildingCollapse,
/// Earthquake
Earthquake,
/// Landslide or mudslide
Landslide,
/// Avalanche (snow)
Avalanche,
/// Flood
Flood,
/// Mine collapse
MineCollapse,
/// Industrial accident
Industrial,
/// Tunnel collapse
TunnelCollapse,
/// Unknown or other
Unknown,
}
impl DisasterType {
/// Get typical debris profile for this disaster type
pub fn typical_debris_profile(&self) -> super::DebrisProfile {
use super::{DebrisProfile, DebrisMaterial, MoistureLevel, MetalContent};
match self {
DisasterType::BuildingCollapse => DebrisProfile {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.25,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::Moderate,
},
DisasterType::Earthquake => DebrisProfile {
primary_material: DebrisMaterial::HeavyConcrete,
void_fraction: 0.2,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::Moderate,
},
DisasterType::Avalanche => DebrisProfile {
primary_material: DebrisMaterial::Snow,
void_fraction: 0.4,
moisture_content: MoistureLevel::Wet,
metal_content: MetalContent::None,
},
DisasterType::Landslide => DebrisProfile {
primary_material: DebrisMaterial::Soil,
void_fraction: 0.15,
moisture_content: MoistureLevel::Wet,
metal_content: MetalContent::None,
},
DisasterType::Flood => DebrisProfile {
primary_material: DebrisMaterial::Mixed,
void_fraction: 0.3,
moisture_content: MoistureLevel::Saturated,
metal_content: MetalContent::Low,
},
DisasterType::MineCollapse | DisasterType::TunnelCollapse => DebrisProfile {
primary_material: DebrisMaterial::Soil,
void_fraction: 0.2,
moisture_content: MoistureLevel::Damp,
metal_content: MetalContent::Low,
},
DisasterType::Industrial => DebrisProfile {
primary_material: DebrisMaterial::Metal,
void_fraction: 0.35,
moisture_content: MoistureLevel::Dry,
metal_content: MetalContent::High,
},
DisasterType::Unknown => DebrisProfile::default(),
}
}
/// Get expected maximum survival time (hours)
pub fn expected_survival_hours(&self) -> u32 {
match self {
DisasterType::Avalanche => 2, // Limited air, hypothermia
DisasterType::Flood => 6, // Drowning risk
DisasterType::MineCollapse => 72, // Air supply critical
DisasterType::BuildingCollapse => 96,
DisasterType::Earthquake => 120,
DisasterType::Landslide => 48,
DisasterType::TunnelCollapse => 72,
DisasterType::Industrial => 72,
DisasterType::Unknown => 72,
}
}
}
impl Default for DisasterType {
fn default() -> Self {
Self::Unknown
}
}
/// Current status of the disaster event
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EventStatus {
/// Event just reported, setting up
Initializing,
/// Active search and rescue
Active,
/// Search suspended (weather, safety)
Suspended,
/// Primary rescue complete, secondary search
SecondarySearch,
/// Event closed
Closed,
}
/// Aggregate root for a disaster event
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DisasterEvent {
id: DisasterEventId,
event_type: DisasterType,
start_time: DateTime<Utc>,
location: Point<f64>,
description: String,
scan_zones: Vec<ScanZone>,
survivors: Vec<Survivor>,
status: EventStatus,
metadata: EventMetadata,
}
/// Additional metadata for a disaster event
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EventMetadata {
/// Estimated number of people in area at time of disaster
pub estimated_occupancy: Option<u32>,
/// Known survivors (already rescued)
pub confirmed_rescued: u32,
/// Known fatalities
pub confirmed_deceased: u32,
/// Weather conditions
pub weather: Option<String>,
/// Lead agency
pub lead_agency: Option<String>,
/// Notes
pub notes: Vec<String>,
}
impl DisasterEvent {
/// Create a new disaster event
pub fn new(
event_type: DisasterType,
location: Point<f64>,
description: &str,
) -> Self {
Self {
id: DisasterEventId::new(),
event_type,
start_time: Utc::now(),
location,
description: description.to_string(),
scan_zones: Vec::new(),
survivors: Vec::new(),
status: EventStatus::Initializing,
metadata: EventMetadata::default(),
}
}
/// Get the event ID
pub fn id(&self) -> &DisasterEventId {
&self.id
}
/// Get the event type
pub fn event_type(&self) -> &DisasterType {
&self.event_type
}
/// Get the start time
pub fn start_time(&self) -> &DateTime<Utc> {
&self.start_time
}
/// Get the location
pub fn location(&self) -> &Point<f64> {
&self.location
}
/// Get the description
pub fn description(&self) -> &str {
&self.description
}
/// Get the scan zones
pub fn zones(&self) -> &[ScanZone] {
&self.scan_zones
}
/// Get mutable scan zones
pub fn zones_mut(&mut self) -> &mut [ScanZone] {
&mut self.scan_zones
}
/// Get the survivors
pub fn survivors(&self) -> Vec<&Survivor> {
self.survivors.iter().collect()
}
/// Get mutable survivors
pub fn survivors_mut(&mut self) -> &mut [Survivor] {
&mut self.survivors
}
/// Get the current status
pub fn status(&self) -> &EventStatus {
&self.status
}
/// Get metadata
pub fn metadata(&self) -> &EventMetadata {
&self.metadata
}
/// Get mutable metadata
pub fn metadata_mut(&mut self) -> &mut EventMetadata {
&mut self.metadata
}
/// Add a scan zone
pub fn add_zone(&mut self, zone: ScanZone) {
self.scan_zones.push(zone);
// Activate event if first zone
if self.status == EventStatus::Initializing {
self.status = EventStatus::Active;
}
}
/// Remove a scan zone
pub fn remove_zone(&mut self, zone_id: &ScanZoneId) {
self.scan_zones.retain(|z| z.id() != zone_id);
}
/// Record a new detection
pub fn record_detection(
&mut self,
zone_id: ScanZoneId,
vitals: VitalSignsReading,
location: Option<Coordinates3D>,
) -> Result<&Survivor, MatError> {
// Check if this might be an existing survivor
if let Some(loc) = &location {
if let Some(existing) = self.find_nearby_survivor(loc, 2.0) {
// Update existing survivor
let survivor = self.survivors.iter_mut()
.find(|s| s.id() == existing)
.ok_or_else(|| MatError::Domain("Survivor not found".into()))?;
survivor.update_vitals(vitals);
if let Some(l) = location {
survivor.update_location(l);
}
return Ok(survivor);
}
}
// Create new survivor
let survivor = Survivor::new(zone_id, vitals, location);
self.survivors.push(survivor);
Ok(self.survivors.last().unwrap())
}
/// Find a survivor near a location
fn find_nearby_survivor(&self, location: &Coordinates3D, radius: f64) -> Option<&SurvivorId> {
for survivor in &self.survivors {
if let Some(loc) = survivor.location() {
if loc.distance_to(location) < radius {
return Some(survivor.id());
}
}
}
None
}
/// Get survivor by ID
pub fn get_survivor(&self, id: &SurvivorId) -> Option<&Survivor> {
self.survivors.iter().find(|s| s.id() == id)
}
/// Get mutable survivor by ID
pub fn get_survivor_mut(&mut self, id: &SurvivorId) -> Option<&mut Survivor> {
self.survivors.iter_mut().find(|s| s.id() == id)
}
/// Get zone by ID
pub fn get_zone(&self, id: &ScanZoneId) -> Option<&ScanZone> {
self.scan_zones.iter().find(|z| z.id() == id)
}
/// Set event status
pub fn set_status(&mut self, status: EventStatus) {
self.status = status;
}
/// Suspend operations
pub fn suspend(&mut self, reason: &str) {
self.status = EventStatus::Suspended;
self.metadata.notes.push(format!(
"[{}] Suspended: {}",
Utc::now().format("%Y-%m-%d %H:%M:%S"),
reason
));
}
/// Resume operations
pub fn resume(&mut self) {
if self.status == EventStatus::Suspended {
self.status = EventStatus::Active;
self.metadata.notes.push(format!(
"[{}] Resumed operations",
Utc::now().format("%Y-%m-%d %H:%M:%S")
));
}
}
/// Close the event
pub fn close(&mut self) {
self.status = EventStatus::Closed;
}
/// Get time since event started
pub fn elapsed_time(&self) -> chrono::Duration {
Utc::now() - self.start_time
}
/// Get count of survivors by triage status
pub fn triage_counts(&self) -> TriageCounts {
use super::TriageStatus;
let mut counts = TriageCounts::default();
for survivor in &self.survivors {
match survivor.triage_status() {
TriageStatus::Immediate => counts.immediate += 1,
TriageStatus::Delayed => counts.delayed += 1,
TriageStatus::Minor => counts.minor += 1,
TriageStatus::Deceased => counts.deceased += 1,
TriageStatus::Unknown => counts.unknown += 1,
}
}
counts
}
}
/// Triage status counts
#[derive(Debug, Clone, Default)]
pub struct TriageCounts {
/// Immediate (Red)
pub immediate: u32,
/// Delayed (Yellow)
pub delayed: u32,
/// Minor (Green)
pub minor: u32,
/// Deceased (Black)
pub deceased: u32,
/// Unknown
pub unknown: u32,
}
impl TriageCounts {
/// Total count
pub fn total(&self) -> u32 {
self.immediate + self.delayed + self.minor + self.deceased + self.unknown
}
/// Count of living survivors
pub fn living(&self) -> u32 {
self.immediate + self.delayed + self.minor
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{ZoneBounds, BreathingPattern, BreathingType, ConfidenceScore};
fn create_test_vitals() -> VitalSignsReading {
VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 16.0,
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_event_creation() {
let event = DisasterEvent::new(
DisasterType::Earthquake,
Point::new(-122.4194, 37.7749),
"Test earthquake event",
);
assert!(matches!(event.event_type(), DisasterType::Earthquake));
assert_eq!(event.status(), &EventStatus::Initializing);
}
#[test]
fn test_add_zone_activates_event() {
let mut event = DisasterEvent::new(
DisasterType::BuildingCollapse,
Point::new(0.0, 0.0),
"Test",
);
assert_eq!(event.status(), &EventStatus::Initializing);
let zone = ScanZone::new("Zone A", ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0));
event.add_zone(zone);
assert_eq!(event.status(), &EventStatus::Active);
}
#[test]
fn test_record_detection() {
let mut event = DisasterEvent::new(
DisasterType::Earthquake,
Point::new(0.0, 0.0),
"Test",
);
let zone = ScanZone::new("Zone A", ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0));
let zone_id = zone.id().clone();
event.add_zone(zone);
let vitals = create_test_vitals();
event.record_detection(zone_id, vitals, None).unwrap();
assert_eq!(event.survivors().len(), 1);
}
#[test]
fn test_disaster_type_survival_hours() {
assert!(DisasterType::Avalanche.expected_survival_hours() < DisasterType::Earthquake.expected_survival_hours());
}
}

View File

@@ -0,0 +1,497 @@
//! Domain events for the wifi-Mat system.
use chrono::{DateTime, Utc};
use super::{
AlertId, Coordinates3D, Priority, ScanZoneId, SurvivorId,
TriageStatus, VitalSignsReading, AlertResolution,
};
/// All domain events in the system
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DomainEvent {
/// Detection-related events
Detection(DetectionEvent),
/// Alert-related events
Alert(AlertEvent),
/// Zone-related events
Zone(ZoneEvent),
/// System-level events
System(SystemEvent),
}
impl DomainEvent {
/// Get the timestamp of the event
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
DomainEvent::Detection(e) => e.timestamp(),
DomainEvent::Alert(e) => e.timestamp(),
DomainEvent::Zone(e) => e.timestamp(),
DomainEvent::System(e) => e.timestamp(),
}
}
/// Get event type name
pub fn event_type(&self) -> &'static str {
match self {
DomainEvent::Detection(e) => e.event_type(),
DomainEvent::Alert(e) => e.event_type(),
DomainEvent::Zone(e) => e.event_type(),
DomainEvent::System(e) => e.event_type(),
}
}
}
/// Detection-related events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DetectionEvent {
/// New survivor detected
SurvivorDetected {
survivor_id: SurvivorId,
zone_id: ScanZoneId,
vital_signs: VitalSignsReading,
location: Option<Coordinates3D>,
timestamp: DateTime<Utc>,
},
/// Survivor vital signs updated
VitalsUpdated {
survivor_id: SurvivorId,
previous_triage: TriageStatus,
current_triage: TriageStatus,
confidence: f64,
timestamp: DateTime<Utc>,
},
/// Survivor triage status changed
TriageStatusChanged {
survivor_id: SurvivorId,
previous: TriageStatus,
current: TriageStatus,
reason: String,
timestamp: DateTime<Utc>,
},
/// Survivor location refined
LocationRefined {
survivor_id: SurvivorId,
previous: Option<Coordinates3D>,
current: Coordinates3D,
uncertainty_reduced: bool,
timestamp: DateTime<Utc>,
},
/// Survivor no longer detected
SurvivorLost {
survivor_id: SurvivorId,
last_detection: DateTime<Utc>,
reason: LostReason,
timestamp: DateTime<Utc>,
},
/// Survivor rescued
SurvivorRescued {
survivor_id: SurvivorId,
rescue_team: Option<String>,
timestamp: DateTime<Utc>,
},
/// Survivor marked deceased
SurvivorDeceased {
survivor_id: SurvivorId,
timestamp: DateTime<Utc>,
},
}
impl DetectionEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::SurvivorDetected { timestamp, .. } => *timestamp,
Self::VitalsUpdated { timestamp, .. } => *timestamp,
Self::TriageStatusChanged { timestamp, .. } => *timestamp,
Self::LocationRefined { timestamp, .. } => *timestamp,
Self::SurvivorLost { timestamp, .. } => *timestamp,
Self::SurvivorRescued { timestamp, .. } => *timestamp,
Self::SurvivorDeceased { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::SurvivorDetected { .. } => "SurvivorDetected",
Self::VitalsUpdated { .. } => "VitalsUpdated",
Self::TriageStatusChanged { .. } => "TriageStatusChanged",
Self::LocationRefined { .. } => "LocationRefined",
Self::SurvivorLost { .. } => "SurvivorLost",
Self::SurvivorRescued { .. } => "SurvivorRescued",
Self::SurvivorDeceased { .. } => "SurvivorDeceased",
}
}
/// Get the survivor ID associated with this event
pub fn survivor_id(&self) -> &SurvivorId {
match self {
Self::SurvivorDetected { survivor_id, .. } => survivor_id,
Self::VitalsUpdated { survivor_id, .. } => survivor_id,
Self::TriageStatusChanged { survivor_id, .. } => survivor_id,
Self::LocationRefined { survivor_id, .. } => survivor_id,
Self::SurvivorLost { survivor_id, .. } => survivor_id,
Self::SurvivorRescued { survivor_id, .. } => survivor_id,
Self::SurvivorDeceased { survivor_id, .. } => survivor_id,
}
}
}
/// Reasons for losing a survivor signal
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum LostReason {
/// Survivor was rescued (signal expected to stop)
Rescued,
/// Detection determined to be false positive
FalsePositive,
/// Signal lost (interference, debris shift, etc.)
SignalLost,
/// Zone was deactivated
ZoneDeactivated,
/// Sensor malfunction
SensorFailure,
/// Unknown reason
Unknown,
}
/// Alert-related events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AlertEvent {
/// New alert generated
AlertGenerated {
alert_id: AlertId,
survivor_id: SurvivorId,
priority: Priority,
timestamp: DateTime<Utc>,
},
/// Alert acknowledged by rescue team
AlertAcknowledged {
alert_id: AlertId,
acknowledged_by: String,
timestamp: DateTime<Utc>,
},
/// Alert escalated
AlertEscalated {
alert_id: AlertId,
previous_priority: Priority,
new_priority: Priority,
reason: String,
timestamp: DateTime<Utc>,
},
/// Alert resolved
AlertResolved {
alert_id: AlertId,
resolution: AlertResolution,
timestamp: DateTime<Utc>,
},
/// Alert cancelled
AlertCancelled {
alert_id: AlertId,
reason: String,
timestamp: DateTime<Utc>,
},
}
impl AlertEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::AlertGenerated { timestamp, .. } => *timestamp,
Self::AlertAcknowledged { timestamp, .. } => *timestamp,
Self::AlertEscalated { timestamp, .. } => *timestamp,
Self::AlertResolved { timestamp, .. } => *timestamp,
Self::AlertCancelled { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::AlertGenerated { .. } => "AlertGenerated",
Self::AlertAcknowledged { .. } => "AlertAcknowledged",
Self::AlertEscalated { .. } => "AlertEscalated",
Self::AlertResolved { .. } => "AlertResolved",
Self::AlertCancelled { .. } => "AlertCancelled",
}
}
/// Get the alert ID associated with this event
pub fn alert_id(&self) -> &AlertId {
match self {
Self::AlertGenerated { alert_id, .. } => alert_id,
Self::AlertAcknowledged { alert_id, .. } => alert_id,
Self::AlertEscalated { alert_id, .. } => alert_id,
Self::AlertResolved { alert_id, .. } => alert_id,
Self::AlertCancelled { alert_id, .. } => alert_id,
}
}
}
/// Zone-related events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZoneEvent {
/// Zone activated
ZoneActivated {
zone_id: ScanZoneId,
zone_name: String,
timestamp: DateTime<Utc>,
},
/// Zone scan completed
ZoneScanCompleted {
zone_id: ScanZoneId,
detections_found: u32,
scan_duration_ms: u64,
timestamp: DateTime<Utc>,
},
/// Zone paused
ZonePaused {
zone_id: ScanZoneId,
reason: String,
timestamp: DateTime<Utc>,
},
/// Zone resumed
ZoneResumed {
zone_id: ScanZoneId,
timestamp: DateTime<Utc>,
},
/// Zone marked complete
ZoneCompleted {
zone_id: ScanZoneId,
total_survivors_found: u32,
timestamp: DateTime<Utc>,
},
/// Zone deactivated
ZoneDeactivated {
zone_id: ScanZoneId,
reason: String,
timestamp: DateTime<Utc>,
},
}
impl ZoneEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::ZoneActivated { timestamp, .. } => *timestamp,
Self::ZoneScanCompleted { timestamp, .. } => *timestamp,
Self::ZonePaused { timestamp, .. } => *timestamp,
Self::ZoneResumed { timestamp, .. } => *timestamp,
Self::ZoneCompleted { timestamp, .. } => *timestamp,
Self::ZoneDeactivated { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::ZoneActivated { .. } => "ZoneActivated",
Self::ZoneScanCompleted { .. } => "ZoneScanCompleted",
Self::ZonePaused { .. } => "ZonePaused",
Self::ZoneResumed { .. } => "ZoneResumed",
Self::ZoneCompleted { .. } => "ZoneCompleted",
Self::ZoneDeactivated { .. } => "ZoneDeactivated",
}
}
/// Get the zone ID associated with this event
pub fn zone_id(&self) -> &ScanZoneId {
match self {
Self::ZoneActivated { zone_id, .. } => zone_id,
Self::ZoneScanCompleted { zone_id, .. } => zone_id,
Self::ZonePaused { zone_id, .. } => zone_id,
Self::ZoneResumed { zone_id, .. } => zone_id,
Self::ZoneCompleted { zone_id, .. } => zone_id,
Self::ZoneDeactivated { zone_id, .. } => zone_id,
}
}
}
/// System-level events
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SystemEvent {
/// System started
SystemStarted {
version: String,
timestamp: DateTime<Utc>,
},
/// System stopped
SystemStopped {
reason: String,
timestamp: DateTime<Utc>,
},
/// Sensor connected
SensorConnected {
sensor_id: String,
zone_id: ScanZoneId,
timestamp: DateTime<Utc>,
},
/// Sensor disconnected
SensorDisconnected {
sensor_id: String,
reason: String,
timestamp: DateTime<Utc>,
},
/// Configuration changed
ConfigChanged {
setting: String,
previous_value: String,
new_value: String,
timestamp: DateTime<Utc>,
},
/// Error occurred
ErrorOccurred {
error_type: String,
message: String,
severity: ErrorSeverity,
timestamp: DateTime<Utc>,
},
}
impl SystemEvent {
/// Get the timestamp
pub fn timestamp(&self) -> DateTime<Utc> {
match self {
Self::SystemStarted { timestamp, .. } => *timestamp,
Self::SystemStopped { timestamp, .. } => *timestamp,
Self::SensorConnected { timestamp, .. } => *timestamp,
Self::SensorDisconnected { timestamp, .. } => *timestamp,
Self::ConfigChanged { timestamp, .. } => *timestamp,
Self::ErrorOccurred { timestamp, .. } => *timestamp,
}
}
/// Get the event type name
pub fn event_type(&self) -> &'static str {
match self {
Self::SystemStarted { .. } => "SystemStarted",
Self::SystemStopped { .. } => "SystemStopped",
Self::SensorConnected { .. } => "SensorConnected",
Self::SensorDisconnected { .. } => "SensorDisconnected",
Self::ConfigChanged { .. } => "ConfigChanged",
Self::ErrorOccurred { .. } => "ErrorOccurred",
}
}
}
/// Error severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ErrorSeverity {
/// Warning - operation continues
Warning,
/// Error - operation may be affected
Error,
/// Critical - immediate attention required
Critical,
}
/// Event store for persisting domain events
pub trait EventStore: Send + Sync {
/// Append an event to the store
fn append(&self, event: DomainEvent) -> Result<(), crate::MatError>;
/// Get all events
fn all(&self) -> Result<Vec<DomainEvent>, crate::MatError>;
/// Get events since a timestamp
fn since(&self, timestamp: DateTime<Utc>) -> Result<Vec<DomainEvent>, crate::MatError>;
/// Get events for a specific survivor
fn for_survivor(&self, survivor_id: &SurvivorId) -> Result<Vec<DomainEvent>, crate::MatError>;
}
/// In-memory event store implementation
#[derive(Debug, Default)]
pub struct InMemoryEventStore {
events: parking_lot::RwLock<Vec<DomainEvent>>,
}
impl InMemoryEventStore {
/// Create a new in-memory event store
pub fn new() -> Self {
Self::default()
}
}
impl EventStore for InMemoryEventStore {
fn append(&self, event: DomainEvent) -> Result<(), crate::MatError> {
self.events.write().push(event);
Ok(())
}
fn all(&self) -> Result<Vec<DomainEvent>, crate::MatError> {
Ok(self.events.read().clone())
}
fn since(&self, timestamp: DateTime<Utc>) -> Result<Vec<DomainEvent>, crate::MatError> {
Ok(self
.events
.read()
.iter()
.filter(|e| e.timestamp() >= timestamp)
.cloned()
.collect())
}
fn for_survivor(&self, survivor_id: &SurvivorId) -> Result<Vec<DomainEvent>, crate::MatError> {
Ok(self
.events
.read()
.iter()
.filter(|e| {
if let DomainEvent::Detection(de) = e {
de.survivor_id() == survivor_id
} else {
false
}
})
.cloned()
.collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_in_memory_event_store() {
let store = InMemoryEventStore::new();
let event = DomainEvent::System(SystemEvent::SystemStarted {
version: "1.0.0".to_string(),
timestamp: Utc::now(),
});
store.append(event).unwrap();
let events = store.all().unwrap();
assert_eq!(events.len(), 1);
}
}

View File

@@ -0,0 +1,26 @@
//! Domain module containing core entities, value objects, and domain events.
//!
//! This module follows Domain-Driven Design principles with:
//! - **Entities**: Objects with identity (Survivor, DisasterEvent, ScanZone)
//! - **Value Objects**: Immutable objects without identity (VitalSignsReading, Coordinates3D)
//! - **Domain Events**: Events that capture domain significance
//! - **Aggregates**: Consistency boundaries (DisasterEvent is the root)
pub mod alert;
pub mod coordinates;
pub mod disaster_event;
pub mod events;
pub mod scan_zone;
pub mod survivor;
pub mod triage;
pub mod vital_signs;
// Re-export all domain types
pub use alert::*;
pub use coordinates::*;
pub use disaster_event::*;
pub use events::*;
pub use scan_zone::*;
pub use survivor::*;
pub use triage::*;
pub use vital_signs::*;

View File

@@ -0,0 +1,494 @@
//! Scan zone entity for defining areas to monitor.
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Unique identifier for a scan zone
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanZoneId(Uuid);
impl ScanZoneId {
/// Create a new random zone ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for ScanZoneId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ScanZoneId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Bounds of a scan zone
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZoneBounds {
/// Rectangular zone
Rectangle {
/// Minimum X coordinate
min_x: f64,
/// Minimum Y coordinate
min_y: f64,
/// Maximum X coordinate
max_x: f64,
/// Maximum Y coordinate
max_y: f64,
},
/// Circular zone
Circle {
/// Center X coordinate
center_x: f64,
/// Center Y coordinate
center_y: f64,
/// Radius in meters
radius: f64,
},
/// Polygon zone (ordered vertices)
Polygon {
/// List of (x, y) vertices
vertices: Vec<(f64, f64)>,
},
}
impl ZoneBounds {
/// Create a rectangular zone
pub fn rectangle(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y }
}
/// Create a circular zone
pub fn circle(center_x: f64, center_y: f64, radius: f64) -> Self {
ZoneBounds::Circle { center_x, center_y, radius }
}
/// Create a polygon zone
pub fn polygon(vertices: Vec<(f64, f64)>) -> Self {
ZoneBounds::Polygon { vertices }
}
/// Calculate the area of the zone in square meters
pub fn area(&self) -> f64 {
match self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
(max_x - min_x) * (max_y - min_y)
}
ZoneBounds::Circle { radius, .. } => {
std::f64::consts::PI * radius * radius
}
ZoneBounds::Polygon { vertices } => {
// Shoelace formula
if vertices.len() < 3 {
return 0.0;
}
let mut area = 0.0;
let n = vertices.len();
for i in 0..n {
let j = (i + 1) % n;
area += vertices[i].0 * vertices[j].1;
area -= vertices[j].0 * vertices[i].1;
}
(area / 2.0).abs()
}
}
}
/// Check if a point is within the zone bounds
pub fn contains(&self, x: f64, y: f64) -> bool {
match self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
x >= *min_x && x <= *max_x && y >= *min_y && y <= *max_y
}
ZoneBounds::Circle { center_x, center_y, radius } => {
let dx = x - center_x;
let dy = y - center_y;
(dx * dx + dy * dy).sqrt() <= *radius
}
ZoneBounds::Polygon { vertices } => {
// Ray casting algorithm
if vertices.len() < 3 {
return false;
}
let mut inside = false;
let n = vertices.len();
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = vertices[i];
let (xj, yj) = vertices[j];
if ((yi > y) != (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi)
{
inside = !inside;
}
j = i;
}
inside
}
}
}
/// Get the center point of the zone
pub fn center(&self) -> (f64, f64) {
match self {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
((min_x + max_x) / 2.0, (min_y + max_y) / 2.0)
}
ZoneBounds::Circle { center_x, center_y, .. } => {
(*center_x, *center_y)
}
ZoneBounds::Polygon { vertices } => {
if vertices.is_empty() {
return (0.0, 0.0);
}
let sum_x: f64 = vertices.iter().map(|(x, _)| x).sum();
let sum_y: f64 = vertices.iter().map(|(_, y)| y).sum();
let n = vertices.len() as f64;
(sum_x / n, sum_y / n)
}
}
}
}
/// Status of a scan zone
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ZoneStatus {
/// Zone is active and being scanned
Active,
/// Zone is paused (temporary)
Paused,
/// Zone scan is complete
Complete,
/// Zone is inaccessible
Inaccessible,
/// Zone is deactivated
Deactivated,
}
/// Parameters for scanning a zone
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanParameters {
/// Scan sensitivity (0.0-1.0)
pub sensitivity: f64,
/// Maximum depth to scan (meters)
pub max_depth: f64,
/// Scan resolution (higher = more detailed but slower)
pub resolution: ScanResolution,
/// Whether to use enhanced breathing detection
pub enhanced_breathing: bool,
/// Whether to use heartbeat detection (more sensitive but slower)
pub heartbeat_detection: bool,
}
impl Default for ScanParameters {
fn default() -> Self {
Self {
sensitivity: 0.8,
max_depth: 5.0,
resolution: ScanResolution::Standard,
enhanced_breathing: true,
heartbeat_detection: false,
}
}
}
/// Scan resolution levels
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScanResolution {
/// Quick scan, lower accuracy
Quick,
/// Standard scan
Standard,
/// High resolution scan
High,
/// Maximum resolution (slowest)
Maximum,
}
impl ScanResolution {
/// Get scan time multiplier
pub fn time_multiplier(&self) -> f64 {
match self {
ScanResolution::Quick => 0.5,
ScanResolution::Standard => 1.0,
ScanResolution::High => 2.0,
ScanResolution::Maximum => 4.0,
}
}
}
/// Position of a sensor (WiFi transmitter/receiver)
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SensorPosition {
/// Sensor identifier
pub id: String,
/// X coordinate (meters)
pub x: f64,
/// Y coordinate (meters)
pub y: f64,
/// Z coordinate (meters, height above ground)
pub z: f64,
/// Sensor type
pub sensor_type: SensorType,
/// Whether sensor is operational
pub is_operational: bool,
}
/// Types of sensors
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SensorType {
/// WiFi transmitter
Transmitter,
/// WiFi receiver
Receiver,
/// Combined transmitter/receiver
Transceiver,
}
/// A defined geographic area being monitored for survivors
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScanZone {
id: ScanZoneId,
name: String,
bounds: ZoneBounds,
sensor_positions: Vec<SensorPosition>,
parameters: ScanParameters,
status: ZoneStatus,
created_at: DateTime<Utc>,
last_scan: Option<DateTime<Utc>>,
scan_count: u32,
detections_count: u32,
}
impl ScanZone {
/// Create a new scan zone
pub fn new(name: &str, bounds: ZoneBounds) -> Self {
Self {
id: ScanZoneId::new(),
name: name.to_string(),
bounds,
sensor_positions: Vec::new(),
parameters: ScanParameters::default(),
status: ZoneStatus::Active,
created_at: Utc::now(),
last_scan: None,
scan_count: 0,
detections_count: 0,
}
}
/// Create with custom parameters
pub fn with_parameters(name: &str, bounds: ZoneBounds, parameters: ScanParameters) -> Self {
let mut zone = Self::new(name, bounds);
zone.parameters = parameters;
zone
}
/// Get the zone ID
pub fn id(&self) -> &ScanZoneId {
&self.id
}
/// Get the zone name
pub fn name(&self) -> &str {
&self.name
}
/// Get the bounds
pub fn bounds(&self) -> &ZoneBounds {
&self.bounds
}
/// Get sensor positions
pub fn sensor_positions(&self) -> &[SensorPosition] {
&self.sensor_positions
}
/// Get scan parameters
pub fn parameters(&self) -> &ScanParameters {
&self.parameters
}
/// Get mutable scan parameters
pub fn parameters_mut(&mut self) -> &mut ScanParameters {
&mut self.parameters
}
/// Get the status
pub fn status(&self) -> &ZoneStatus {
&self.status
}
/// Get last scan time
pub fn last_scan(&self) -> Option<&DateTime<Utc>> {
self.last_scan.as_ref()
}
/// Get scan count
pub fn scan_count(&self) -> u32 {
self.scan_count
}
/// Get detection count
pub fn detections_count(&self) -> u32 {
self.detections_count
}
/// Add a sensor to the zone
pub fn add_sensor(&mut self, sensor: SensorPosition) {
self.sensor_positions.push(sensor);
}
/// Remove a sensor
pub fn remove_sensor(&mut self, sensor_id: &str) {
self.sensor_positions.retain(|s| s.id != sensor_id);
}
/// Set zone status
pub fn set_status(&mut self, status: ZoneStatus) {
self.status = status;
}
/// Pause the zone
pub fn pause(&mut self) {
self.status = ZoneStatus::Paused;
}
/// Resume the zone
pub fn resume(&mut self) {
if self.status == ZoneStatus::Paused {
self.status = ZoneStatus::Active;
}
}
/// Mark zone as complete
pub fn complete(&mut self) {
self.status = ZoneStatus::Complete;
}
/// Record a scan
pub fn record_scan(&mut self, found_detections: u32) {
self.last_scan = Some(Utc::now());
self.scan_count += 1;
self.detections_count += found_detections;
}
/// Check if a point is within this zone
pub fn contains_point(&self, x: f64, y: f64) -> bool {
self.bounds.contains(x, y)
}
/// Get the area of the zone
pub fn area(&self) -> f64 {
self.bounds.area()
}
/// Check if zone has enough sensors for localization
pub fn has_sufficient_sensors(&self) -> bool {
// Need at least 3 sensors for 2D localization
self.sensor_positions.iter()
.filter(|s| s.is_operational)
.count() >= 3
}
/// Time since last scan
pub fn time_since_scan(&self) -> Option<chrono::Duration> {
self.last_scan.map(|t| Utc::now() - t)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_bounds() {
let bounds = ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0);
assert!((bounds.area() - 100.0).abs() < 0.001);
assert!(bounds.contains(5.0, 5.0));
assert!(!bounds.contains(15.0, 5.0));
assert_eq!(bounds.center(), (5.0, 5.0));
}
#[test]
fn test_circle_bounds() {
let bounds = ZoneBounds::circle(0.0, 0.0, 10.0);
assert!((bounds.area() - std::f64::consts::PI * 100.0).abs() < 0.001);
assert!(bounds.contains(0.0, 0.0));
assert!(bounds.contains(5.0, 5.0));
assert!(!bounds.contains(10.0, 10.0));
}
#[test]
fn test_scan_zone_creation() {
let zone = ScanZone::new(
"Test Zone",
ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
);
assert_eq!(zone.name(), "Test Zone");
assert!(matches!(zone.status(), ZoneStatus::Active));
assert_eq!(zone.scan_count(), 0);
}
#[test]
fn test_scan_zone_sensors() {
let mut zone = ScanZone::new(
"Test Zone",
ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
);
assert!(!zone.has_sufficient_sensors());
for i in 0..3 {
zone.add_sensor(SensorPosition {
id: format!("sensor-{}", i),
x: i as f64 * 10.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
});
}
assert!(zone.has_sufficient_sensors());
}
#[test]
fn test_scan_zone_status_transitions() {
let mut zone = ScanZone::new(
"Test",
ZoneBounds::rectangle(0.0, 0.0, 10.0, 10.0),
);
assert!(matches!(zone.status(), ZoneStatus::Active));
zone.pause();
assert!(matches!(zone.status(), ZoneStatus::Paused));
zone.resume();
assert!(matches!(zone.status(), ZoneStatus::Active));
zone.complete();
assert!(matches!(zone.status(), ZoneStatus::Complete));
}
}

View File

@@ -0,0 +1,424 @@
//! Survivor entity representing a detected human in a disaster zone.
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{
Coordinates3D, TriageStatus, VitalSignsReading, ScanZoneId,
triage::TriageCalculator,
};
/// Unique identifier for a survivor
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SurvivorId(Uuid);
impl SurvivorId {
/// Create a new random survivor ID
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Create from an existing UUID
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
/// Get the inner UUID
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for SurvivorId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for SurvivorId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Current status of a survivor
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SurvivorStatus {
/// Actively being tracked
Active,
/// Confirmed rescued
Rescued,
/// Lost signal, may need re-detection
Lost,
/// Confirmed deceased
Deceased,
/// Determined to be false positive
FalsePositive,
}
/// Additional metadata about a survivor
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SurvivorMetadata {
/// Estimated age category based on vital patterns
pub estimated_age_category: Option<AgeCategory>,
/// Notes from rescue team
pub notes: Vec<String>,
/// Tags for organization
pub tags: Vec<String>,
/// Assigned rescue team ID
pub assigned_team: Option<String>,
}
/// Estimated age category based on vital sign patterns
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AgeCategory {
/// Infant (0-2 years)
Infant,
/// Child (2-12 years)
Child,
/// Adult (12-65 years)
Adult,
/// Elderly (65+ years)
Elderly,
/// Cannot determine
Unknown,
}
/// History of vital signs readings
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VitalSignsHistory {
readings: Vec<VitalSignsReading>,
max_history: usize,
}
impl VitalSignsHistory {
/// Create a new history with specified max size
pub fn new(max_history: usize) -> Self {
Self {
readings: Vec::with_capacity(max_history),
max_history,
}
}
/// Add a new reading
pub fn add(&mut self, reading: VitalSignsReading) {
if self.readings.len() >= self.max_history {
self.readings.remove(0);
}
self.readings.push(reading);
}
/// Get the most recent reading
pub fn latest(&self) -> Option<&VitalSignsReading> {
self.readings.last()
}
/// Get all readings
pub fn all(&self) -> &[VitalSignsReading] {
&self.readings
}
/// Get the number of readings
pub fn len(&self) -> usize {
self.readings.len()
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.readings.is_empty()
}
/// Calculate average confidence across readings
pub fn average_confidence(&self) -> f64 {
if self.readings.is_empty() {
return 0.0;
}
let sum: f64 = self.readings.iter()
.map(|r| r.confidence.value())
.sum();
sum / self.readings.len() as f64
}
/// Check if vitals are deteriorating
pub fn is_deteriorating(&self) -> bool {
if self.readings.len() < 3 {
return false;
}
let recent: Vec<_> = self.readings.iter().rev().take(3).collect();
// Check breathing trend
let breathing_declining = recent.windows(2).all(|w| {
match (&w[0].breathing, &w[1].breathing) {
(Some(a), Some(b)) => a.rate_bpm < b.rate_bpm,
_ => false,
}
});
// Check confidence trend
let confidence_declining = recent.windows(2).all(|w| {
w[0].confidence.value() < w[1].confidence.value()
});
breathing_declining || confidence_declining
}
}
/// A detected survivor in the disaster zone
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Survivor {
id: SurvivorId,
zone_id: ScanZoneId,
first_detected: DateTime<Utc>,
last_updated: DateTime<Utc>,
location: Option<Coordinates3D>,
vital_signs: VitalSignsHistory,
triage_status: TriageStatus,
status: SurvivorStatus,
confidence: f64,
metadata: SurvivorMetadata,
alert_sent: bool,
}
impl Survivor {
/// Create a new survivor from initial detection
pub fn new(
zone_id: ScanZoneId,
initial_vitals: VitalSignsReading,
location: Option<Coordinates3D>,
) -> Self {
let now = Utc::now();
let confidence = initial_vitals.confidence.value();
let triage_status = TriageCalculator::calculate(&initial_vitals);
let mut vital_signs = VitalSignsHistory::new(100);
vital_signs.add(initial_vitals);
Self {
id: SurvivorId::new(),
zone_id,
first_detected: now,
last_updated: now,
location,
vital_signs,
triage_status,
status: SurvivorStatus::Active,
confidence,
metadata: SurvivorMetadata::default(),
alert_sent: false,
}
}
/// Get the survivor ID
pub fn id(&self) -> &SurvivorId {
&self.id
}
/// Get the zone ID where survivor was detected
pub fn zone_id(&self) -> &ScanZoneId {
&self.zone_id
}
/// Get the first detection time
pub fn first_detected(&self) -> &DateTime<Utc> {
&self.first_detected
}
/// Get the last update time
pub fn last_updated(&self) -> &DateTime<Utc> {
&self.last_updated
}
/// Get the estimated location
pub fn location(&self) -> Option<&Coordinates3D> {
self.location.as_ref()
}
/// Get the vital signs history
pub fn vital_signs(&self) -> &VitalSignsHistory {
&self.vital_signs
}
/// Get the current triage status
pub fn triage_status(&self) -> &TriageStatus {
&self.triage_status
}
/// Get the current status
pub fn status(&self) -> &SurvivorStatus {
&self.status
}
/// Get the confidence score
pub fn confidence(&self) -> f64 {
self.confidence
}
/// Get the metadata
pub fn metadata(&self) -> &SurvivorMetadata {
&self.metadata
}
/// Get mutable metadata
pub fn metadata_mut(&mut self) -> &mut SurvivorMetadata {
&mut self.metadata
}
/// Update with new vital signs reading
pub fn update_vitals(&mut self, reading: VitalSignsReading) {
let previous_triage = self.triage_status.clone();
self.vital_signs.add(reading.clone());
self.confidence = self.vital_signs.average_confidence();
self.triage_status = TriageCalculator::calculate(&reading);
self.last_updated = Utc::now();
// Log triage change for audit
if previous_triage != self.triage_status {
tracing::info!(
survivor_id = %self.id,
previous = ?previous_triage,
current = ?self.triage_status,
"Triage status changed"
);
}
}
/// Update the location estimate
pub fn update_location(&mut self, location: Coordinates3D) {
self.location = Some(location);
self.last_updated = Utc::now();
}
/// Mark as rescued
pub fn mark_rescued(&mut self) {
self.status = SurvivorStatus::Rescued;
self.last_updated = Utc::now();
tracing::info!(survivor_id = %self.id, "Survivor marked as rescued");
}
/// Mark as lost (signal lost)
pub fn mark_lost(&mut self) {
self.status = SurvivorStatus::Lost;
self.last_updated = Utc::now();
}
/// Mark as deceased
pub fn mark_deceased(&mut self) {
self.status = SurvivorStatus::Deceased;
self.triage_status = TriageStatus::Deceased;
self.last_updated = Utc::now();
}
/// Mark as false positive
pub fn mark_false_positive(&mut self) {
self.status = SurvivorStatus::FalsePositive;
self.last_updated = Utc::now();
}
/// Check if survivor should generate an alert
pub fn should_alert(&self) -> bool {
if self.alert_sent {
return false;
}
// Alert for high-priority survivors
matches!(
self.triage_status,
TriageStatus::Immediate | TriageStatus::Delayed
) && self.confidence >= 0.5
}
/// Mark that alert was sent
pub fn mark_alert_sent(&mut self) {
self.alert_sent = true;
}
/// Check if vitals are deteriorating (needs priority upgrade)
pub fn is_deteriorating(&self) -> bool {
self.vital_signs.is_deteriorating()
}
/// Get time since last update
pub fn time_since_update(&self) -> chrono::Duration {
Utc::now() - self.last_updated
}
/// Check if survivor data is stale
pub fn is_stale(&self, threshold_seconds: i64) -> bool {
self.time_since_update().num_seconds() > threshold_seconds
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, BreathingType, ConfidenceScore};
fn create_test_vitals(confidence: f64) -> VitalSignsReading {
VitalSignsReading {
breathing: Some(BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
heartbeat: None,
movement: Default::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(confidence),
}
}
#[test]
fn test_survivor_creation() {
let zone_id = ScanZoneId::new();
let vitals = create_test_vitals(0.8);
let survivor = Survivor::new(zone_id.clone(), vitals, None);
assert_eq!(survivor.zone_id(), &zone_id);
assert!(survivor.confidence() >= 0.8);
assert!(matches!(survivor.status(), SurvivorStatus::Active));
}
#[test]
fn test_vital_signs_history() {
let mut history = VitalSignsHistory::new(5);
for i in 0..7 {
history.add(create_test_vitals(0.5 + (i as f64 * 0.05)));
}
// Should only keep last 5
assert_eq!(history.len(), 5);
// Average should be based on last 5 readings
assert!(history.average_confidence() > 0.5);
}
#[test]
fn test_survivor_should_alert() {
let zone_id = ScanZoneId::new();
let vitals = create_test_vitals(0.8);
let survivor = Survivor::new(zone_id, vitals, None);
// Should alert if triage is Immediate or Delayed
// Depends on triage calculation from vitals
assert!(!survivor.alert_sent);
}
#[test]
fn test_survivor_mark_rescued() {
let zone_id = ScanZoneId::new();
let vitals = create_test_vitals(0.8);
let mut survivor = Survivor::new(zone_id, vitals, None);
survivor.mark_rescued();
assert!(matches!(survivor.status(), SurvivorStatus::Rescued));
}
}

View File

@@ -0,0 +1,350 @@
//! Triage classification following START protocol.
//!
//! The START (Simple Triage and Rapid Treatment) protocol is used to
//! quickly categorize victims in mass casualty incidents.
use super::{VitalSignsReading, BreathingType, MovementType};
/// Triage status following START protocol
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TriageStatus {
/// Immediate (Red) - Life-threatening, requires immediate intervention
/// RPM: Respiration >30 or <10, or absent pulse, or unable to follow commands
Immediate,
/// Delayed (Yellow) - Serious but stable, can wait for treatment
/// RPM: Normal respiration, pulse present, follows commands, non-life-threatening
Delayed,
/// Minor (Green) - Walking wounded, minimal treatment needed
/// Can walk, minor injuries
Minor,
/// Deceased (Black) - No vital signs, or not breathing after airway cleared
Deceased,
/// Unknown - Insufficient data for classification
Unknown,
}
impl TriageStatus {
/// Get the priority level (1 = highest)
pub fn priority(&self) -> u8 {
match self {
TriageStatus::Immediate => 1,
TriageStatus::Delayed => 2,
TriageStatus::Minor => 3,
TriageStatus::Deceased => 4,
TriageStatus::Unknown => 5,
}
}
/// Get display color
pub fn color(&self) -> &'static str {
match self {
TriageStatus::Immediate => "red",
TriageStatus::Delayed => "yellow",
TriageStatus::Minor => "green",
TriageStatus::Deceased => "black",
TriageStatus::Unknown => "gray",
}
}
/// Get human-readable description
pub fn description(&self) -> &'static str {
match self {
TriageStatus::Immediate => "Requires immediate life-saving intervention",
TriageStatus::Delayed => "Serious but can wait for treatment",
TriageStatus::Minor => "Minor injuries, walking wounded",
TriageStatus::Deceased => "No vital signs detected",
TriageStatus::Unknown => "Unable to determine status",
}
}
/// Check if this status requires urgent attention
pub fn is_urgent(&self) -> bool {
matches!(self, TriageStatus::Immediate | TriageStatus::Delayed)
}
}
impl std::fmt::Display for TriageStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TriageStatus::Immediate => write!(f, "IMMEDIATE (Red)"),
TriageStatus::Delayed => write!(f, "DELAYED (Yellow)"),
TriageStatus::Minor => write!(f, "MINOR (Green)"),
TriageStatus::Deceased => write!(f, "DECEASED (Black)"),
TriageStatus::Unknown => write!(f, "UNKNOWN"),
}
}
}
/// Calculator for triage status based on vital signs
pub struct TriageCalculator;
impl TriageCalculator {
/// Calculate triage status from vital signs reading
///
/// Uses modified START protocol adapted for remote sensing:
/// 1. Check breathing (respiration)
/// 2. Check for movement/responsiveness (proxy for perfusion/mental status)
/// 3. Classify based on combined assessment
pub fn calculate(vitals: &VitalSignsReading) -> TriageStatus {
// Step 1: Check if any vitals are detected
if !vitals.has_vitals() {
// No vitals at all - either deceased or signal issue
return TriageStatus::Unknown;
}
// Step 2: Assess breathing
let breathing_status = Self::assess_breathing(vitals);
// Step 3: Assess movement/responsiveness
let movement_status = Self::assess_movement(vitals);
// Step 4: Combine assessments
Self::combine_assessments(breathing_status, movement_status)
}
/// Assess breathing status
fn assess_breathing(vitals: &VitalSignsReading) -> BreathingAssessment {
match &vitals.breathing {
None => BreathingAssessment::Absent,
Some(breathing) => {
// Check for agonal breathing (pre-death)
if breathing.pattern_type == BreathingType::Agonal {
return BreathingAssessment::Agonal;
}
// Check rate
if breathing.rate_bpm < 10.0 {
BreathingAssessment::TooSlow
} else if breathing.rate_bpm > 30.0 {
BreathingAssessment::TooFast
} else {
BreathingAssessment::Normal
}
}
}
}
/// Assess movement/responsiveness
fn assess_movement(vitals: &VitalSignsReading) -> MovementAssessment {
match vitals.movement.movement_type {
MovementType::Gross if vitals.movement.is_voluntary => {
MovementAssessment::Responsive
}
MovementType::Gross => MovementAssessment::Moving,
MovementType::Fine => MovementAssessment::MinimalMovement,
MovementType::Tremor => MovementAssessment::InvoluntaryOnly,
MovementType::Periodic => MovementAssessment::MinimalMovement,
MovementType::None => MovementAssessment::None,
}
}
/// Combine breathing and movement assessments into triage status
fn combine_assessments(
breathing: BreathingAssessment,
movement: MovementAssessment,
) -> TriageStatus {
match (breathing, movement) {
// No breathing
(BreathingAssessment::Absent, MovementAssessment::None) => {
TriageStatus::Deceased
}
(BreathingAssessment::Agonal, _) => {
TriageStatus::Immediate
}
(BreathingAssessment::Absent, _) => {
// No breathing but movement - possible airway obstruction
TriageStatus::Immediate
}
// Abnormal breathing rates
(BreathingAssessment::TooFast, _) => {
TriageStatus::Immediate
}
(BreathingAssessment::TooSlow, _) => {
TriageStatus::Immediate
}
// Normal breathing with movement assessment
(BreathingAssessment::Normal, MovementAssessment::Responsive) => {
TriageStatus::Minor
}
(BreathingAssessment::Normal, MovementAssessment::Moving) => {
TriageStatus::Delayed
}
(BreathingAssessment::Normal, MovementAssessment::MinimalMovement) => {
TriageStatus::Delayed
}
(BreathingAssessment::Normal, MovementAssessment::InvoluntaryOnly) => {
TriageStatus::Immediate // Not following commands
}
(BreathingAssessment::Normal, MovementAssessment::None) => {
TriageStatus::Immediate // Breathing but unresponsive
}
}
}
/// Check if status should be upgraded based on deterioration
pub fn should_upgrade(current: &TriageStatus, is_deteriorating: bool) -> bool {
if !is_deteriorating {
return false;
}
// Upgrade if not already at highest priority
matches!(current, TriageStatus::Delayed | TriageStatus::Minor)
}
/// Get upgraded triage status
pub fn upgrade(current: &TriageStatus) -> TriageStatus {
match current {
TriageStatus::Minor => TriageStatus::Delayed,
TriageStatus::Delayed => TriageStatus::Immediate,
other => other.clone(),
}
}
}
/// Internal breathing assessment
#[derive(Debug, Clone, Copy)]
enum BreathingAssessment {
Normal,
TooFast,
TooSlow,
Agonal,
Absent,
}
/// Internal movement assessment
#[derive(Debug, Clone, Copy)]
enum MovementAssessment {
Responsive, // Voluntary purposeful movement
Moving, // Movement but unclear if responsive
MinimalMovement, // Small movements only
InvoluntaryOnly, // Only tremors/involuntary
None, // No movement detected
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{BreathingPattern, ConfidenceScore, MovementProfile};
use chrono::Utc;
fn create_vitals(
breathing: Option<BreathingPattern>,
movement: MovementProfile,
) -> VitalSignsReading {
VitalSignsReading {
breathing,
heartbeat: None,
movement,
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
}
}
#[test]
fn test_no_vitals_is_unknown() {
let vitals = create_vitals(None, MovementProfile::default());
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Unknown);
}
#[test]
fn test_normal_breathing_responsive_is_minor() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
}),
MovementProfile {
movement_type: MovementType::Gross,
intensity: 0.8,
frequency: 0.5,
is_voluntary: true,
},
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Minor);
}
#[test]
fn test_fast_breathing_is_immediate() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
}),
MovementProfile {
movement_type: MovementType::Fine,
intensity: 0.3,
frequency: 0.2,
is_voluntary: false,
},
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Immediate);
}
#[test]
fn test_slow_breathing_is_immediate() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 8.0,
amplitude: 0.5,
regularity: 0.6,
pattern_type: BreathingType::Shallow,
}),
MovementProfile {
movement_type: MovementType::None,
intensity: 0.0,
frequency: 0.0,
is_voluntary: false,
},
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Immediate);
}
#[test]
fn test_agonal_breathing_is_immediate() {
let vitals = create_vitals(
Some(BreathingPattern {
rate_bpm: 4.0,
amplitude: 0.3,
regularity: 0.2,
pattern_type: BreathingType::Agonal,
}),
MovementProfile::default(),
);
assert_eq!(TriageCalculator::calculate(&vitals), TriageStatus::Immediate);
}
#[test]
fn test_triage_priority() {
assert_eq!(TriageStatus::Immediate.priority(), 1);
assert_eq!(TriageStatus::Delayed.priority(), 2);
assert_eq!(TriageStatus::Minor.priority(), 3);
assert_eq!(TriageStatus::Deceased.priority(), 4);
}
#[test]
fn test_upgrade_triage() {
assert_eq!(
TriageCalculator::upgrade(&TriageStatus::Minor),
TriageStatus::Delayed
);
assert_eq!(
TriageCalculator::upgrade(&TriageStatus::Delayed),
TriageStatus::Immediate
);
assert_eq!(
TriageCalculator::upgrade(&TriageStatus::Immediate),
TriageStatus::Immediate
);
}
}

View File

@@ -0,0 +1,375 @@
//! Vital signs value objects for survivor detection.
use chrono::{DateTime, Utc};
/// Confidence score for a detection (0.0 to 1.0)
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConfidenceScore(f64);
impl ConfidenceScore {
/// Create a new confidence score, clamped to [0.0, 1.0]
pub fn new(value: f64) -> Self {
Self(value.clamp(0.0, 1.0))
}
/// Get the raw value
pub fn value(&self) -> f64 {
self.0
}
/// Check if confidence is high (>= 0.8)
pub fn is_high(&self) -> bool {
self.0 >= 0.8
}
/// Check if confidence is medium (>= 0.5)
pub fn is_medium(&self) -> bool {
self.0 >= 0.5
}
/// Check if confidence is low (< 0.5)
pub fn is_low(&self) -> bool {
self.0 < 0.5
}
}
impl Default for ConfidenceScore {
fn default() -> Self {
Self(0.0)
}
}
/// Complete vital signs reading at a point in time
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VitalSignsReading {
/// Breathing pattern if detected
pub breathing: Option<BreathingPattern>,
/// Heartbeat signature if detected
pub heartbeat: Option<HeartbeatSignature>,
/// Movement profile
pub movement: MovementProfile,
/// Timestamp of reading
pub timestamp: DateTime<Utc>,
/// Overall confidence in the reading
pub confidence: ConfidenceScore,
}
impl VitalSignsReading {
/// Create a new vital signs reading
pub fn new(
breathing: Option<BreathingPattern>,
heartbeat: Option<HeartbeatSignature>,
movement: MovementProfile,
) -> Self {
// Calculate combined confidence
let confidence = Self::calculate_confidence(&breathing, &heartbeat, &movement);
Self {
breathing,
heartbeat,
movement,
timestamp: Utc::now(),
confidence,
}
}
/// Calculate combined confidence from individual detections
fn calculate_confidence(
breathing: &Option<BreathingPattern>,
heartbeat: &Option<HeartbeatSignature>,
movement: &MovementProfile,
) -> ConfidenceScore {
let mut total = 0.0;
let mut count = 0.0;
if let Some(b) = breathing {
total += b.confidence();
count += 1.5; // Weight breathing higher
}
if let Some(h) = heartbeat {
total += h.confidence();
count += 1.0;
}
if movement.movement_type != MovementType::None {
total += movement.confidence();
count += 1.0;
}
if count > 0.0 {
ConfidenceScore::new(total / count)
} else {
ConfidenceScore::new(0.0)
}
}
/// Check if any vital sign is detected
pub fn has_vitals(&self) -> bool {
self.breathing.is_some()
|| self.heartbeat.is_some()
|| self.movement.movement_type != MovementType::None
}
/// Check if breathing is detected
pub fn has_breathing(&self) -> bool {
self.breathing.is_some()
}
/// Check if heartbeat is detected
pub fn has_heartbeat(&self) -> bool {
self.heartbeat.is_some()
}
/// Check if movement is detected
pub fn has_movement(&self) -> bool {
self.movement.movement_type != MovementType::None
}
}
/// Breathing pattern detected from CSI analysis
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BreathingPattern {
/// Breaths per minute (normal adult: 12-20)
pub rate_bpm: f32,
/// Signal amplitude/strength
pub amplitude: f32,
/// Pattern regularity (0.0-1.0)
pub regularity: f32,
/// Type of breathing pattern
pub pattern_type: BreathingType,
}
impl BreathingPattern {
/// Check if breathing rate is normal
pub fn is_normal_rate(&self) -> bool {
self.rate_bpm >= 12.0 && self.rate_bpm <= 20.0
}
/// Check if rate is critically low
pub fn is_bradypnea(&self) -> bool {
self.rate_bpm < 10.0
}
/// Check if rate is critically high
pub fn is_tachypnea(&self) -> bool {
self.rate_bpm > 30.0
}
/// Get confidence based on signal quality
pub fn confidence(&self) -> f64 {
(self.amplitude * self.regularity) as f64
}
}
/// Types of breathing patterns
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BreathingType {
/// Normal, regular breathing
Normal,
/// Shallow, weak breathing
Shallow,
/// Deep, labored breathing
Labored,
/// Irregular pattern
Irregular,
/// Agonal breathing (pre-death gasping)
Agonal,
/// Apnea (no breathing detected)
Apnea,
}
/// Heartbeat signature from micro-Doppler analysis
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct HeartbeatSignature {
/// Heart rate in beats per minute (normal: 60-100)
pub rate_bpm: f32,
/// Heart rate variability
pub variability: f32,
/// Signal strength
pub strength: SignalStrength,
}
impl HeartbeatSignature {
/// Check if heart rate is normal
pub fn is_normal_rate(&self) -> bool {
self.rate_bpm >= 60.0 && self.rate_bpm <= 100.0
}
/// Check if rate indicates bradycardia
pub fn is_bradycardia(&self) -> bool {
self.rate_bpm < 50.0
}
/// Check if rate indicates tachycardia
pub fn is_tachycardia(&self) -> bool {
self.rate_bpm > 120.0
}
/// Get confidence based on signal strength
pub fn confidence(&self) -> f64 {
match self.strength {
SignalStrength::Strong => 0.9,
SignalStrength::Moderate => 0.7,
SignalStrength::Weak => 0.4,
SignalStrength::VeryWeak => 0.2,
}
}
}
/// Signal strength levels
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SignalStrength {
/// Strong, clear signal
Strong,
/// Moderate signal
Moderate,
/// Weak signal
Weak,
/// Very weak, borderline
VeryWeak,
}
/// Movement profile from CSI analysis
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MovementProfile {
/// Type of movement detected
pub movement_type: MovementType,
/// Intensity of movement (0.0-1.0)
pub intensity: f32,
/// Frequency of movement patterns
pub frequency: f32,
/// Whether movement appears voluntary/purposeful
pub is_voluntary: bool,
}
impl Default for MovementProfile {
fn default() -> Self {
Self {
movement_type: MovementType::None,
intensity: 0.0,
frequency: 0.0,
is_voluntary: false,
}
}
}
impl MovementProfile {
/// Get confidence based on movement characteristics
pub fn confidence(&self) -> f64 {
match self.movement_type {
MovementType::None => 0.0,
MovementType::Gross => 0.9,
MovementType::Fine => 0.7,
MovementType::Tremor => 0.6,
MovementType::Periodic => 0.5,
}
}
/// Check if movement indicates consciousness
pub fn indicates_consciousness(&self) -> bool {
self.is_voluntary && self.movement_type == MovementType::Gross
}
}
/// Types of movement detected
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum MovementType {
/// No movement detected
None,
/// Large body movements (limbs, torso)
Gross,
/// Small movements (fingers, head)
Fine,
/// Involuntary tremor/shaking
Tremor,
/// Periodic movement (possibly breathing-related)
Periodic,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_confidence_score_clamping() {
assert_eq!(ConfidenceScore::new(1.5).value(), 1.0);
assert_eq!(ConfidenceScore::new(-0.5).value(), 0.0);
assert_eq!(ConfidenceScore::new(0.7).value(), 0.7);
}
#[test]
fn test_breathing_pattern_rates() {
let normal = BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
};
assert!(normal.is_normal_rate());
assert!(!normal.is_bradypnea());
assert!(!normal.is_tachypnea());
let slow = BreathingPattern {
rate_bpm: 8.0,
amplitude: 0.5,
regularity: 0.6,
pattern_type: BreathingType::Shallow,
};
assert!(slow.is_bradypnea());
let fast = BreathingPattern {
rate_bpm: 35.0,
amplitude: 0.7,
regularity: 0.5,
pattern_type: BreathingType::Labored,
};
assert!(fast.is_tachypnea());
}
#[test]
fn test_vital_signs_reading() {
let breathing = BreathingPattern {
rate_bpm: 16.0,
amplitude: 0.8,
regularity: 0.9,
pattern_type: BreathingType::Normal,
};
let reading = VitalSignsReading::new(
Some(breathing),
None,
MovementProfile::default(),
);
assert!(reading.has_vitals());
assert!(reading.has_breathing());
assert!(!reading.has_heartbeat());
assert!(!reading.has_movement());
}
#[test]
fn test_signal_strength_confidence() {
let strong = HeartbeatSignature {
rate_bpm: 72.0,
variability: 0.1,
strength: SignalStrength::Strong,
};
assert_eq!(strong.confidence(), 0.9);
let weak = HeartbeatSignature {
rate_bpm: 72.0,
variability: 0.1,
strength: SignalStrength::Weak,
};
assert_eq!(weak.confidence(), 0.4);
}
}