ADR-001 (WiFi-Mat disaster response pipeline): - Add EnsembleClassifier with weighted voting (breathing/heartbeat/movement) - Wire EventStore into DisasterResponse with domain event emission - Add scan control API endpoints (push CSI, scan control, pipeline status, domain events) - Implement START triage protocol (Immediate/Delayed/Minor/Deceased/Unknown) - Critical patterns (Agonal/Apnea) bypass confidence threshold for safety - Add 6 deterministic integration tests with synthetic sinusoidal CSI data ADR-009 (WASM signal pipeline): - Add pushCsiData() with zero-crossing breathing rate extraction - Add getPipelineConfig() for runtime configuration access - Update TypeScript type definitions for new WASM exports ADR-012 (ESP32 CSI sensor mesh): - Implement CsiFrame, CsiMetadata, SubcarrierData types - Implement Esp32CsiParser with binary frame parsing (magic/header/IQ pairs) - Add parse_stream() with automatic resync on corruption - Add ParseError enum with descriptive error variants - 12 unit tests covering valid frames, corruption, multi-frame streams All 275 workspace tests pass. No mocks, no stubs, no placeholders. https://claude.ai/code/session_01Ki7pvEZtJDvqJkmyn6B714
1016 lines
28 KiB
Rust
1016 lines
28 KiB
Rust
//! Data Transfer Objects (DTOs) for the MAT REST API.
|
|
//!
|
|
//! These types are used for serializing/deserializing API requests and responses.
|
|
//! They provide a clean separation between domain models and API contracts.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
use crate::domain::{
|
|
DisasterType, EventStatus, ZoneStatus, TriageStatus, Priority,
|
|
AlertStatus, SurvivorStatus,
|
|
};
|
|
|
|
// ============================================================================
|
|
// Event DTOs
|
|
// ============================================================================
|
|
|
|
/// Request body for creating a new disaster event.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "event_type": "Earthquake",
|
|
/// "latitude": 37.7749,
|
|
/// "longitude": -122.4194,
|
|
/// "description": "Magnitude 6.8 earthquake in San Francisco",
|
|
/// "estimated_occupancy": 500,
|
|
/// "lead_agency": "SF Fire Department"
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct CreateEventRequest {
|
|
/// Type of disaster event
|
|
pub event_type: DisasterTypeDto,
|
|
/// Latitude of disaster epicenter
|
|
pub latitude: f64,
|
|
/// Longitude of disaster epicenter
|
|
pub longitude: f64,
|
|
/// Human-readable description of the event
|
|
pub description: String,
|
|
/// Estimated number of people in the affected area
|
|
#[serde(default)]
|
|
pub estimated_occupancy: Option<u32>,
|
|
/// Lead responding agency
|
|
#[serde(default)]
|
|
pub lead_agency: Option<String>,
|
|
}
|
|
|
|
/// Response body for disaster event details.
|
|
///
|
|
/// ## Example Response
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "id": "550e8400-e29b-41d4-a716-446655440000",
|
|
/// "event_type": "Earthquake",
|
|
/// "status": "Active",
|
|
/// "start_time": "2024-01-15T14:30:00Z",
|
|
/// "latitude": 37.7749,
|
|
/// "longitude": -122.4194,
|
|
/// "description": "Magnitude 6.8 earthquake",
|
|
/// "zone_count": 5,
|
|
/// "survivor_count": 12,
|
|
/// "triage_summary": {
|
|
/// "immediate": 3,
|
|
/// "delayed": 5,
|
|
/// "minor": 4,
|
|
/// "deceased": 0
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct EventResponse {
|
|
/// Unique event identifier
|
|
pub id: Uuid,
|
|
/// Type of disaster
|
|
pub event_type: DisasterTypeDto,
|
|
/// Current event status
|
|
pub status: EventStatusDto,
|
|
/// When the event was created/started
|
|
pub start_time: DateTime<Utc>,
|
|
/// Latitude of epicenter
|
|
pub latitude: f64,
|
|
/// Longitude of epicenter
|
|
pub longitude: f64,
|
|
/// Event description
|
|
pub description: String,
|
|
/// Number of scan zones
|
|
pub zone_count: usize,
|
|
/// Number of detected survivors
|
|
pub survivor_count: usize,
|
|
/// Summary of triage classifications
|
|
pub triage_summary: TriageSummary,
|
|
/// Metadata about the event
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub metadata: Option<EventMetadataDto>,
|
|
}
|
|
|
|
/// Summary of triage counts across all survivors.
|
|
#[derive(Debug, Clone, Serialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct TriageSummary {
|
|
/// Immediate (Red) - life-threatening
|
|
pub immediate: u32,
|
|
/// Delayed (Yellow) - serious but stable
|
|
pub delayed: u32,
|
|
/// Minor (Green) - walking wounded
|
|
pub minor: u32,
|
|
/// Deceased (Black)
|
|
pub deceased: u32,
|
|
/// Unknown status
|
|
pub unknown: u32,
|
|
}
|
|
|
|
/// Event metadata DTO
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct EventMetadataDto {
|
|
/// Estimated number of people in area at time of disaster
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub estimated_occupancy: Option<u32>,
|
|
/// Known survivors (already rescued)
|
|
#[serde(default)]
|
|
pub confirmed_rescued: u32,
|
|
/// Known fatalities
|
|
#[serde(default)]
|
|
pub confirmed_deceased: u32,
|
|
/// Weather conditions
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub weather: Option<String>,
|
|
/// Lead agency
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub lead_agency: Option<String>,
|
|
}
|
|
|
|
/// Paginated list of events.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct EventListResponse {
|
|
/// List of events
|
|
pub events: Vec<EventResponse>,
|
|
/// Total count of events
|
|
pub total: usize,
|
|
/// Current page number (0-indexed)
|
|
pub page: usize,
|
|
/// Number of items per page
|
|
pub page_size: usize,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Zone DTOs
|
|
// ============================================================================
|
|
|
|
/// Request body for adding a scan zone to an event.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "name": "Building A - North Wing",
|
|
/// "bounds": {
|
|
/// "type": "rectangle",
|
|
/// "min_x": 0.0,
|
|
/// "min_y": 0.0,
|
|
/// "max_x": 50.0,
|
|
/// "max_y": 30.0
|
|
/// },
|
|
/// "parameters": {
|
|
/// "sensitivity": 0.85,
|
|
/// "max_depth": 5.0,
|
|
/// "heartbeat_detection": true
|
|
/// }
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct CreateZoneRequest {
|
|
/// Human-readable zone name
|
|
pub name: String,
|
|
/// Geographic bounds of the zone
|
|
pub bounds: ZoneBoundsDto,
|
|
/// Optional scan parameters
|
|
#[serde(default)]
|
|
pub parameters: Option<ScanParametersDto>,
|
|
}
|
|
|
|
/// Zone boundary definition.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum ZoneBoundsDto {
|
|
/// Rectangular boundary
|
|
Rectangle {
|
|
min_x: f64,
|
|
min_y: f64,
|
|
max_x: f64,
|
|
max_y: f64,
|
|
},
|
|
/// Circular boundary
|
|
Circle {
|
|
center_x: f64,
|
|
center_y: f64,
|
|
radius: f64,
|
|
},
|
|
/// Polygon boundary (list of vertices)
|
|
Polygon {
|
|
vertices: Vec<(f64, f64)>,
|
|
},
|
|
}
|
|
|
|
/// Scan parameters for a zone.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ScanParametersDto {
|
|
/// Detection sensitivity (0.0-1.0)
|
|
#[serde(default = "default_sensitivity")]
|
|
pub sensitivity: f64,
|
|
/// Maximum depth to scan in meters
|
|
#[serde(default = "default_max_depth")]
|
|
pub max_depth: f64,
|
|
/// Scan resolution level
|
|
#[serde(default)]
|
|
pub resolution: ScanResolutionDto,
|
|
/// Enable enhanced breathing detection
|
|
#[serde(default = "default_true")]
|
|
pub enhanced_breathing: bool,
|
|
/// Enable heartbeat detection (slower but more accurate)
|
|
#[serde(default)]
|
|
pub heartbeat_detection: bool,
|
|
}
|
|
|
|
fn default_sensitivity() -> f64 { 0.8 }
|
|
fn default_max_depth() -> f64 { 5.0 }
|
|
fn default_true() -> bool { true }
|
|
|
|
impl Default for ScanParametersDto {
|
|
fn default() -> Self {
|
|
Self {
|
|
sensitivity: default_sensitivity(),
|
|
max_depth: default_max_depth(),
|
|
resolution: ScanResolutionDto::default(),
|
|
enhanced_breathing: default_true(),
|
|
heartbeat_detection: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Scan resolution levels.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ScanResolutionDto {
|
|
Quick,
|
|
#[default]
|
|
Standard,
|
|
High,
|
|
Maximum,
|
|
}
|
|
|
|
/// Response for zone details.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ZoneResponse {
|
|
/// Zone identifier
|
|
pub id: Uuid,
|
|
/// Zone name
|
|
pub name: String,
|
|
/// Zone status
|
|
pub status: ZoneStatusDto,
|
|
/// Zone boundaries
|
|
pub bounds: ZoneBoundsDto,
|
|
/// Zone area in square meters
|
|
pub area: f64,
|
|
/// Scan parameters
|
|
pub parameters: ScanParametersDto,
|
|
/// Last scan time
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub last_scan: Option<DateTime<Utc>>,
|
|
/// Total scan count
|
|
pub scan_count: u32,
|
|
/// Number of detections in this zone
|
|
pub detections_count: u32,
|
|
}
|
|
|
|
/// List of zones response.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ZoneListResponse {
|
|
/// List of zones
|
|
pub zones: Vec<ZoneResponse>,
|
|
/// Total count
|
|
pub total: usize,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Survivor DTOs
|
|
// ============================================================================
|
|
|
|
/// Response for survivor details.
|
|
///
|
|
/// ## Example Response
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "id": "550e8400-e29b-41d4-a716-446655440001",
|
|
/// "zone_id": "550e8400-e29b-41d4-a716-446655440002",
|
|
/// "status": "Active",
|
|
/// "triage_status": "Immediate",
|
|
/// "location": {
|
|
/// "x": 25.5,
|
|
/// "y": 12.3,
|
|
/// "z": -2.1,
|
|
/// "uncertainty_radius": 1.5
|
|
/// },
|
|
/// "vital_signs": {
|
|
/// "breathing_rate": 22.5,
|
|
/// "has_heartbeat": true,
|
|
/// "has_movement": false
|
|
/// },
|
|
/// "confidence": 0.87,
|
|
/// "first_detected": "2024-01-15T14:32:00Z",
|
|
/// "last_updated": "2024-01-15T14:45:00Z"
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct SurvivorResponse {
|
|
/// Survivor identifier
|
|
pub id: Uuid,
|
|
/// Zone where survivor was detected
|
|
pub zone_id: Uuid,
|
|
/// Current survivor status
|
|
pub status: SurvivorStatusDto,
|
|
/// Triage classification
|
|
pub triage_status: TriageStatusDto,
|
|
/// Location information
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub location: Option<LocationDto>,
|
|
/// Latest vital signs summary
|
|
pub vital_signs: VitalSignsSummaryDto,
|
|
/// Detection confidence (0.0-1.0)
|
|
pub confidence: f64,
|
|
/// When survivor was first detected
|
|
pub first_detected: DateTime<Utc>,
|
|
/// Last update time
|
|
pub last_updated: DateTime<Utc>,
|
|
/// Whether survivor is deteriorating
|
|
pub is_deteriorating: bool,
|
|
/// Metadata
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub metadata: Option<SurvivorMetadataDto>,
|
|
}
|
|
|
|
/// Location information DTO.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct LocationDto {
|
|
/// X coordinate (east-west, meters)
|
|
pub x: f64,
|
|
/// Y coordinate (north-south, meters)
|
|
pub y: f64,
|
|
/// Z coordinate (depth, negative is below surface)
|
|
pub z: f64,
|
|
/// Estimated depth below surface (positive meters)
|
|
pub depth: f64,
|
|
/// Horizontal uncertainty radius in meters
|
|
pub uncertainty_radius: f64,
|
|
/// Location confidence score
|
|
pub confidence: f64,
|
|
}
|
|
|
|
/// Summary of vital signs for API response.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct VitalSignsSummaryDto {
|
|
/// Breathing rate (breaths per minute)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub breathing_rate: Option<f32>,
|
|
/// Breathing pattern type
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub breathing_type: Option<String>,
|
|
/// Heart rate if detected (bpm)
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub heart_rate: Option<f32>,
|
|
/// Whether heartbeat is detected
|
|
pub has_heartbeat: bool,
|
|
/// Whether movement is detected
|
|
pub has_movement: bool,
|
|
/// Movement type if present
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub movement_type: Option<String>,
|
|
/// Timestamp of reading
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
/// Survivor metadata DTO.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct SurvivorMetadataDto {
|
|
/// Estimated age category
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub estimated_age_category: Option<String>,
|
|
/// Assigned rescue team
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub assigned_team: Option<String>,
|
|
/// Notes
|
|
pub notes: Vec<String>,
|
|
/// Tags
|
|
pub tags: Vec<String>,
|
|
}
|
|
|
|
/// List of survivors response.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct SurvivorListResponse {
|
|
/// List of survivors
|
|
pub survivors: Vec<SurvivorResponse>,
|
|
/// Total count
|
|
pub total: usize,
|
|
/// Triage summary
|
|
pub triage_summary: TriageSummary,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Alert DTOs
|
|
// ============================================================================
|
|
|
|
/// Response for alert details.
|
|
///
|
|
/// ## Example Response
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "id": "550e8400-e29b-41d4-a716-446655440003",
|
|
/// "survivor_id": "550e8400-e29b-41d4-a716-446655440001",
|
|
/// "priority": "Critical",
|
|
/// "status": "Pending",
|
|
/// "title": "Immediate: Survivor detected with abnormal breathing",
|
|
/// "message": "Survivor in Zone A showing signs of respiratory distress",
|
|
/// "triage_status": "Immediate",
|
|
/// "location": { ... },
|
|
/// "created_at": "2024-01-15T14:35:00Z"
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct AlertResponse {
|
|
/// Alert identifier
|
|
pub id: Uuid,
|
|
/// Related survivor ID
|
|
pub survivor_id: Uuid,
|
|
/// Alert priority
|
|
pub priority: PriorityDto,
|
|
/// Alert status
|
|
pub status: AlertStatusDto,
|
|
/// Alert title
|
|
pub title: String,
|
|
/// Detailed message
|
|
pub message: String,
|
|
/// Associated triage status
|
|
pub triage_status: TriageStatusDto,
|
|
/// Location if available
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub location: Option<LocationDto>,
|
|
/// Recommended action
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub recommended_action: Option<String>,
|
|
/// When alert was created
|
|
pub created_at: DateTime<Utc>,
|
|
/// When alert was acknowledged
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub acknowledged_at: Option<DateTime<Utc>>,
|
|
/// Who acknowledged the alert
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub acknowledged_by: Option<String>,
|
|
/// Escalation count
|
|
pub escalation_count: u32,
|
|
}
|
|
|
|
/// Request to acknowledge an alert.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "acknowledged_by": "Team Alpha",
|
|
/// "notes": "En route to location"
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct AcknowledgeAlertRequest {
|
|
/// Who is acknowledging the alert
|
|
pub acknowledged_by: String,
|
|
/// Optional notes
|
|
#[serde(default)]
|
|
pub notes: Option<String>,
|
|
}
|
|
|
|
/// Response after acknowledging an alert.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct AcknowledgeAlertResponse {
|
|
/// Whether acknowledgement was successful
|
|
pub success: bool,
|
|
/// Updated alert
|
|
pub alert: AlertResponse,
|
|
}
|
|
|
|
/// List of alerts response.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct AlertListResponse {
|
|
/// List of alerts
|
|
pub alerts: Vec<AlertResponse>,
|
|
/// Total count
|
|
pub total: usize,
|
|
/// Count by priority
|
|
pub priority_counts: PriorityCounts,
|
|
}
|
|
|
|
/// Count of alerts by priority.
|
|
#[derive(Debug, Clone, Serialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct PriorityCounts {
|
|
pub critical: usize,
|
|
pub high: usize,
|
|
pub medium: usize,
|
|
pub low: usize,
|
|
}
|
|
|
|
// ============================================================================
|
|
// WebSocket DTOs
|
|
// ============================================================================
|
|
|
|
/// WebSocket message types for real-time streaming.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(tag = "type", rename_all = "snake_case")]
|
|
pub enum WebSocketMessage {
|
|
/// New survivor detected
|
|
SurvivorDetected {
|
|
event_id: Uuid,
|
|
survivor: SurvivorResponse,
|
|
},
|
|
/// Survivor status updated
|
|
SurvivorUpdated {
|
|
event_id: Uuid,
|
|
survivor: SurvivorResponse,
|
|
},
|
|
/// Survivor lost (signal lost)
|
|
SurvivorLost {
|
|
event_id: Uuid,
|
|
survivor_id: Uuid,
|
|
},
|
|
/// New alert generated
|
|
AlertCreated {
|
|
event_id: Uuid,
|
|
alert: AlertResponse,
|
|
},
|
|
/// Alert status changed
|
|
AlertUpdated {
|
|
event_id: Uuid,
|
|
alert: AlertResponse,
|
|
},
|
|
/// Zone scan completed
|
|
ZoneScanComplete {
|
|
event_id: Uuid,
|
|
zone_id: Uuid,
|
|
detections: u32,
|
|
},
|
|
/// Event status changed
|
|
EventStatusChanged {
|
|
event_id: Uuid,
|
|
old_status: EventStatusDto,
|
|
new_status: EventStatusDto,
|
|
},
|
|
/// Heartbeat/keep-alive
|
|
Heartbeat {
|
|
timestamp: DateTime<Utc>,
|
|
},
|
|
/// Error message
|
|
Error {
|
|
code: String,
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
/// WebSocket subscription request.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(tag = "action", rename_all = "snake_case")]
|
|
pub enum WebSocketRequest {
|
|
/// Subscribe to events for a disaster event
|
|
Subscribe {
|
|
event_id: Uuid,
|
|
},
|
|
/// Unsubscribe from events
|
|
Unsubscribe {
|
|
event_id: Uuid,
|
|
},
|
|
/// Subscribe to all events
|
|
SubscribeAll,
|
|
/// Request current state
|
|
GetState {
|
|
event_id: Uuid,
|
|
},
|
|
}
|
|
|
|
// ============================================================================
|
|
// Enum DTOs (mirroring domain enums with serde)
|
|
// ============================================================================
|
|
|
|
/// Disaster type DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum DisasterTypeDto {
|
|
BuildingCollapse,
|
|
Earthquake,
|
|
Landslide,
|
|
Avalanche,
|
|
Flood,
|
|
MineCollapse,
|
|
Industrial,
|
|
TunnelCollapse,
|
|
Unknown,
|
|
}
|
|
|
|
impl From<DisasterType> for DisasterTypeDto {
|
|
fn from(dt: DisasterType) -> Self {
|
|
match dt {
|
|
DisasterType::BuildingCollapse => DisasterTypeDto::BuildingCollapse,
|
|
DisasterType::Earthquake => DisasterTypeDto::Earthquake,
|
|
DisasterType::Landslide => DisasterTypeDto::Landslide,
|
|
DisasterType::Avalanche => DisasterTypeDto::Avalanche,
|
|
DisasterType::Flood => DisasterTypeDto::Flood,
|
|
DisasterType::MineCollapse => DisasterTypeDto::MineCollapse,
|
|
DisasterType::Industrial => DisasterTypeDto::Industrial,
|
|
DisasterType::TunnelCollapse => DisasterTypeDto::TunnelCollapse,
|
|
DisasterType::Unknown => DisasterTypeDto::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<DisasterTypeDto> for DisasterType {
|
|
fn from(dt: DisasterTypeDto) -> Self {
|
|
match dt {
|
|
DisasterTypeDto::BuildingCollapse => DisasterType::BuildingCollapse,
|
|
DisasterTypeDto::Earthquake => DisasterType::Earthquake,
|
|
DisasterTypeDto::Landslide => DisasterType::Landslide,
|
|
DisasterTypeDto::Avalanche => DisasterType::Avalanche,
|
|
DisasterTypeDto::Flood => DisasterType::Flood,
|
|
DisasterTypeDto::MineCollapse => DisasterType::MineCollapse,
|
|
DisasterTypeDto::Industrial => DisasterType::Industrial,
|
|
DisasterTypeDto::TunnelCollapse => DisasterType::TunnelCollapse,
|
|
DisasterTypeDto::Unknown => DisasterType::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Event status DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum EventStatusDto {
|
|
Initializing,
|
|
Active,
|
|
Suspended,
|
|
SecondarySearch,
|
|
Closed,
|
|
}
|
|
|
|
impl From<EventStatus> for EventStatusDto {
|
|
fn from(es: EventStatus) -> Self {
|
|
match es {
|
|
EventStatus::Initializing => EventStatusDto::Initializing,
|
|
EventStatus::Active => EventStatusDto::Active,
|
|
EventStatus::Suspended => EventStatusDto::Suspended,
|
|
EventStatus::SecondarySearch => EventStatusDto::SecondarySearch,
|
|
EventStatus::Closed => EventStatusDto::Closed,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Zone status DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum ZoneStatusDto {
|
|
Active,
|
|
Paused,
|
|
Complete,
|
|
Inaccessible,
|
|
Deactivated,
|
|
}
|
|
|
|
impl From<ZoneStatus> for ZoneStatusDto {
|
|
fn from(zs: ZoneStatus) -> Self {
|
|
match zs {
|
|
ZoneStatus::Active => ZoneStatusDto::Active,
|
|
ZoneStatus::Paused => ZoneStatusDto::Paused,
|
|
ZoneStatus::Complete => ZoneStatusDto::Complete,
|
|
ZoneStatus::Inaccessible => ZoneStatusDto::Inaccessible,
|
|
ZoneStatus::Deactivated => ZoneStatusDto::Deactivated,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Triage status DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum TriageStatusDto {
|
|
Immediate,
|
|
Delayed,
|
|
Minor,
|
|
Deceased,
|
|
Unknown,
|
|
}
|
|
|
|
impl From<TriageStatus> for TriageStatusDto {
|
|
fn from(ts: TriageStatus) -> Self {
|
|
match ts {
|
|
TriageStatus::Immediate => TriageStatusDto::Immediate,
|
|
TriageStatus::Delayed => TriageStatusDto::Delayed,
|
|
TriageStatus::Minor => TriageStatusDto::Minor,
|
|
TriageStatus::Deceased => TriageStatusDto::Deceased,
|
|
TriageStatus::Unknown => TriageStatusDto::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Priority DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum PriorityDto {
|
|
Critical,
|
|
High,
|
|
Medium,
|
|
Low,
|
|
}
|
|
|
|
impl From<Priority> for PriorityDto {
|
|
fn from(p: Priority) -> Self {
|
|
match p {
|
|
Priority::Critical => PriorityDto::Critical,
|
|
Priority::High => PriorityDto::High,
|
|
Priority::Medium => PriorityDto::Medium,
|
|
Priority::Low => PriorityDto::Low,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Alert status DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum AlertStatusDto {
|
|
Pending,
|
|
Acknowledged,
|
|
InProgress,
|
|
Resolved,
|
|
Cancelled,
|
|
Expired,
|
|
}
|
|
|
|
impl From<AlertStatus> for AlertStatusDto {
|
|
fn from(as_: AlertStatus) -> Self {
|
|
match as_ {
|
|
AlertStatus::Pending => AlertStatusDto::Pending,
|
|
AlertStatus::Acknowledged => AlertStatusDto::Acknowledged,
|
|
AlertStatus::InProgress => AlertStatusDto::InProgress,
|
|
AlertStatus::Resolved => AlertStatusDto::Resolved,
|
|
AlertStatus::Cancelled => AlertStatusDto::Cancelled,
|
|
AlertStatus::Expired => AlertStatusDto::Expired,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Survivor status DTO.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum SurvivorStatusDto {
|
|
Active,
|
|
Rescued,
|
|
Lost,
|
|
Deceased,
|
|
FalsePositive,
|
|
}
|
|
|
|
impl From<SurvivorStatus> for SurvivorStatusDto {
|
|
fn from(ss: SurvivorStatus) -> Self {
|
|
match ss {
|
|
SurvivorStatus::Active => SurvivorStatusDto::Active,
|
|
SurvivorStatus::Rescued => SurvivorStatusDto::Rescued,
|
|
SurvivorStatus::Lost => SurvivorStatusDto::Lost,
|
|
SurvivorStatus::Deceased => SurvivorStatusDto::Deceased,
|
|
SurvivorStatus::FalsePositive => SurvivorStatusDto::FalsePositive,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Query Parameters
|
|
// ============================================================================
|
|
|
|
/// Query parameters for listing events.
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ListEventsQuery {
|
|
/// Filter by status
|
|
pub status: Option<EventStatusDto>,
|
|
/// Filter by disaster type
|
|
pub event_type: Option<DisasterTypeDto>,
|
|
/// Page number (0-indexed)
|
|
#[serde(default)]
|
|
pub page: usize,
|
|
/// Page size (default 20, max 100)
|
|
#[serde(default = "default_page_size")]
|
|
pub page_size: usize,
|
|
}
|
|
|
|
fn default_page_size() -> usize { 20 }
|
|
|
|
/// Query parameters for listing survivors.
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ListSurvivorsQuery {
|
|
/// Filter by triage status
|
|
pub triage_status: Option<TriageStatusDto>,
|
|
/// Filter by zone ID
|
|
pub zone_id: Option<Uuid>,
|
|
/// Filter by minimum confidence
|
|
pub min_confidence: Option<f64>,
|
|
/// Include only deteriorating
|
|
#[serde(default)]
|
|
pub deteriorating_only: bool,
|
|
}
|
|
|
|
/// Query parameters for listing alerts.
|
|
#[derive(Debug, Clone, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ListAlertsQuery {
|
|
/// Filter by priority
|
|
pub priority: Option<PriorityDto>,
|
|
/// Filter by status
|
|
pub status: Option<AlertStatusDto>,
|
|
/// Only pending alerts
|
|
#[serde(default)]
|
|
pub pending_only: bool,
|
|
/// Only active alerts
|
|
#[serde(default)]
|
|
pub active_only: bool,
|
|
}
|
|
|
|
// ============================================================================
|
|
// Scan Control DTOs
|
|
// ============================================================================
|
|
|
|
/// Request to push CSI data into the pipeline.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// ```json
|
|
/// {
|
|
/// "amplitudes": [0.5, 0.6, 0.4, 0.7, 0.3],
|
|
/// "phases": [0.1, -0.2, 0.15, -0.1, 0.05],
|
|
/// "sample_rate": 1000.0
|
|
/// }
|
|
/// ```
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct PushCsiDataRequest {
|
|
/// CSI amplitude samples
|
|
pub amplitudes: Vec<f64>,
|
|
/// CSI phase samples (must be same length as amplitudes)
|
|
pub phases: Vec<f64>,
|
|
/// Sample rate in Hz (optional, defaults to pipeline config)
|
|
#[serde(default)]
|
|
pub sample_rate: Option<f64>,
|
|
}
|
|
|
|
/// Response after pushing CSI data.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct PushCsiDataResponse {
|
|
/// Whether data was accepted
|
|
pub accepted: bool,
|
|
/// Number of samples ingested
|
|
pub samples_ingested: usize,
|
|
/// Current buffer duration in seconds
|
|
pub buffer_duration_secs: f64,
|
|
}
|
|
|
|
/// Scan control action request.
|
|
///
|
|
/// ## Example
|
|
///
|
|
/// ```json
|
|
/// { "action": "start" }
|
|
/// ```
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ScanControlRequest {
|
|
/// Action to perform
|
|
pub action: ScanAction,
|
|
}
|
|
|
|
/// Available scan actions.
|
|
#[derive(Debug, Clone, Copy, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ScanAction {
|
|
/// Start scanning
|
|
Start,
|
|
/// Stop scanning
|
|
Stop,
|
|
/// Pause scanning (retain buffer)
|
|
Pause,
|
|
/// Resume from pause
|
|
Resume,
|
|
/// Clear the CSI data buffer
|
|
ClearBuffer,
|
|
}
|
|
|
|
/// Response for scan control actions.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct ScanControlResponse {
|
|
/// Whether action was performed
|
|
pub success: bool,
|
|
/// Current scan state
|
|
pub state: String,
|
|
/// Description of what happened
|
|
pub message: String,
|
|
}
|
|
|
|
/// Response for pipeline status query.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct PipelineStatusResponse {
|
|
/// Whether scanning is active
|
|
pub scanning: bool,
|
|
/// Current buffer duration in seconds
|
|
pub buffer_duration_secs: f64,
|
|
/// Whether ML pipeline is enabled
|
|
pub ml_enabled: bool,
|
|
/// Whether ML pipeline is ready
|
|
pub ml_ready: bool,
|
|
/// Detection config summary
|
|
pub sample_rate: f64,
|
|
/// Heartbeat detection enabled
|
|
pub heartbeat_enabled: bool,
|
|
/// Minimum confidence threshold
|
|
pub min_confidence: f64,
|
|
}
|
|
|
|
/// Domain events list response.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct DomainEventsResponse {
|
|
/// List of domain events
|
|
pub events: Vec<DomainEventDto>,
|
|
/// Total count
|
|
pub total: usize,
|
|
}
|
|
|
|
/// Serializable domain event for API response.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub struct DomainEventDto {
|
|
/// Event type
|
|
pub event_type: String,
|
|
/// Timestamp
|
|
pub timestamp: DateTime<Utc>,
|
|
/// JSON-serialized event details
|
|
pub details: String,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_event_request_deserialize() {
|
|
let json = r#"{
|
|
"event_type": "Earthquake",
|
|
"latitude": 37.7749,
|
|
"longitude": -122.4194,
|
|
"description": "Test earthquake"
|
|
}"#;
|
|
|
|
let req: CreateEventRequest = serde_json::from_str(json).unwrap();
|
|
assert_eq!(req.event_type, DisasterTypeDto::Earthquake);
|
|
assert!((req.latitude - 37.7749).abs() < 0.0001);
|
|
}
|
|
|
|
#[test]
|
|
fn test_zone_bounds_dto_deserialize() {
|
|
let rect_json = r#"{
|
|
"type": "rectangle",
|
|
"min_x": 0.0,
|
|
"min_y": 0.0,
|
|
"max_x": 10.0,
|
|
"max_y": 10.0
|
|
}"#;
|
|
|
|
let bounds: ZoneBoundsDto = serde_json::from_str(rect_json).unwrap();
|
|
assert!(matches!(bounds, ZoneBoundsDto::Rectangle { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_websocket_message_serialize() {
|
|
let msg = WebSocketMessage::Heartbeat {
|
|
timestamp: Utc::now(),
|
|
};
|
|
let json = serde_json::to_string(&msg).unwrap();
|
|
assert!(json.contains("\"type\":\"heartbeat\""));
|
|
}
|
|
}
|