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
495 lines
13 KiB
Rust
495 lines
13 KiB
Rust
//! 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));
|
|
}
|
|
}
|