2025-10-18 13:49:53 -04:00

396 lines
11 KiB
Python

"""
VitalLink Backend API
FastAPI server for managing patients, wristbands, and real-time data
"""
from fastapi import FastAPI, WebSocket, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Dict, Optional
from datetime import datetime, timedelta
import asyncio
import json
from collections import defaultdict
app = FastAPI(title="VitalLink API", version="1.0.0")
# CORS middleware for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify your frontend domain
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ============================================================================
# DATA MODELS
# ============================================================================
class PatientCheckIn(BaseModel):
firstName: str
lastName: str
dob: str
symptoms: List[str]
severity: str
class Patient(BaseModel):
patient_id: str
band_id: str
first_name: str
last_name: str
dob: str
symptoms: List[str]
severity: str
check_in_time: datetime
current_tier: str = "NORMAL"
last_vitals: Optional[dict] = None
is_active: bool = True
class VitalsData(BaseModel):
band_id: str
patient_id: str
timestamp: float
tier: str
hr_bpm: int
spo2: int
temp_c: float
activity: float
flags: List[str]
seq: int
class QueuePosition(BaseModel):
patient_id: str
band_id: str
name: str
tier: str
priority_score: float
wait_time_minutes: int
last_hr: int
last_spo2: int
last_temp: float
# ============================================================================
# IN-MEMORY STORAGE (Replace with database in production)
# ============================================================================
patients_db: Dict[str, Patient] = {}
vitals_history: Dict[str, List[VitalsData]] = defaultdict(list)
available_bands = [
f"VitalLink-{hex(i)[2:].upper().zfill(4)}" for i in range(0x1000, 0x2000)
]
active_websockets: List[WebSocket] = []
# ============================================================================
# PRIORITY ALGORITHM
# ============================================================================
def calculate_priority_score(patient: Patient) -> float:
"""
Calculate dynamic priority score for queue ordering
Higher score = higher priority
Factors:
- Tier (Emergency=100, Alert=50, Normal=0)
- Vital sign trends (worsening = higher)
- Wait time (exponential increase after threshold)
- Initial severity
"""
score = 0.0
# Tier contribution (largest factor)
tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0}
score += tier_scores.get(patient.current_tier, 0)
# Wait time contribution (increases exponentially after 30 min)
wait_minutes = (datetime.now() - patient.check_in_time).total_seconds() / 60
if wait_minutes > 30:
score += (wait_minutes - 30) * 0.5 # 0.5 points per minute over 30
elif wait_minutes > 60:
score += (wait_minutes - 60) * 1.0 # Accelerate after 1 hour
# Initial severity contribution
severity_scores = {"severe": 20, "moderate": 10, "mild": 5}
score += severity_scores.get(patient.severity, 0)
# Vital signs contribution (if available)
if patient.last_vitals:
hr = patient.last_vitals.get("hr_bpm", 75)
spo2 = patient.last_vitals.get("spo2", 98)
temp = patient.last_vitals.get("temp_c", 37.0)
# Abnormal HR
if hr > 110 or hr < 50:
score += 10
if hr > 140 or hr < 40:
score += 30
# Low SpO2 (critical)
if spo2 < 92:
score += 15
if spo2 < 88:
score += 40
# Fever
if temp > 38.5:
score += 15
if temp > 39.5:
score += 25
return score
# ============================================================================
# API ENDPOINTS
# ============================================================================
@app.post("/api/checkin")
async def check_in_patient(data: PatientCheckIn):
"""Register a new patient and assign wristband"""
if not available_bands:
raise HTTPException(status_code=503, detail="No wristbands available")
# Assign IDs
patient_id = f"P{len(patients_db) + 100001}"
band_id = available_bands.pop(0)
# Create patient record
patient = Patient(
patient_id=patient_id,
band_id=band_id,
first_name=data.firstName,
last_name=data.lastName,
dob=data.dob,
symptoms=data.symptoms,
severity=data.severity,
check_in_time=datetime.now(),
current_tier="NORMAL",
)
patients_db[patient_id] = patient
# Notify connected clients
await broadcast_update({"type": "patient_added", "patient": patient.dict()})
return {
"patient_id": patient_id,
"band_id": band_id,
"message": "Check-in successful",
}
@app.post("/api/vitals")
async def receive_vitals(data: VitalsData):
"""Receive vitals data from base station"""
patient_id = data.patient_id
if patient_id not in patients_db:
raise HTTPException(status_code=404, detail="Patient not found")
# Update patient record
patient = patients_db[patient_id]
patient.current_tier = data.tier
patient.last_vitals = data.dict()
# Store in history (keep last 1000 readings)
vitals_history[patient_id].append(data)
if len(vitals_history[patient_id]) > 1000:
vitals_history[patient_id] = vitals_history[patient_id][-1000:]
# Broadcast to connected clients
await broadcast_update(
{"type": "vitals_update", "patient_id": patient_id, "vitals": data.dict()}
)
return {"status": "received"}
@app.get("/api/queue")
async def get_queue():
"""Get prioritized queue of active patients"""
active_patients = [p for p in patients_db.values() if p.is_active]
# Calculate priority and sort
queue = []
for patient in active_patients:
priority_score = calculate_priority_score(patient)
wait_minutes = int(
(datetime.now() - patient.check_in_time).total_seconds() / 60
)
queue.append(
QueuePosition(
patient_id=patient.patient_id,
band_id=patient.band_id,
name=f"{patient.first_name} {patient.last_name}",
tier=patient.current_tier,
priority_score=priority_score,
wait_time_minutes=wait_minutes,
last_hr=patient.last_vitals.get("hr_bpm", 0)
if patient.last_vitals
else 0,
last_spo2=patient.last_vitals.get("spo2", 0)
if patient.last_vitals
else 0,
last_temp=patient.last_vitals.get("temp_c", 0)
if patient.last_vitals
else 0,
)
)
# Sort by priority (highest first)
queue.sort(key=lambda x: x.priority_score, reverse=True)
return queue
@app.get("/api/patients/{patient_id}")
async def get_patient_details(patient_id: str):
"""Get detailed information about a specific patient"""
if patient_id not in patients_db:
raise HTTPException(status_code=404, detail="Patient not found")
patient = patients_db[patient_id]
history = vitals_history.get(patient_id, [])
return {
"patient": patient.dict(),
"vitals_history": [v.dict() for v in history[-50:]], # Last 50 readings
"priority_score": calculate_priority_score(patient),
}
@app.post("/api/patients/{patient_id}/discharge")
async def discharge_patient(patient_id: str):
"""Discharge a patient and return wristband to pool"""
if patient_id not in patients_db:
raise HTTPException(status_code=404, detail="Patient not found")
patient = patients_db[patient_id]
patient.is_active = False
# Return band to pool
available_bands.append(patient.band_id)
# Notify clients
await broadcast_update({"type": "patient_discharged", "patient_id": patient_id})
return {"message": "Patient discharged", "band_returned": patient.band_id}
@app.get("/api/stats")
async def get_statistics():
"""Get overall ER statistics"""
active_patients = [p for p in patients_db.values() if p.is_active]
tier_counts = {"EMERGENCY": 0, "ALERT": 0, "NORMAL": 0}
for patient in active_patients:
tier_counts[patient.current_tier] += 1
total_vitals = sum(len(v) for v in vitals_history.values())
avg_wait = 0
if active_patients:
wait_times = [
(datetime.now() - p.check_in_time).total_seconds() / 60
for p in active_patients
]
avg_wait = sum(wait_times) / len(wait_times)
return {
"total_patients": len(patients_db),
"active_patients": len(active_patients),
"tier_breakdown": tier_counts,
"available_bands": len(available_bands),
"total_vitals_received": total_vitals,
"average_wait_minutes": round(avg_wait, 1),
}
# ============================================================================
# WEBSOCKET FOR REAL-TIME UPDATES
# ============================================================================
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket connection for real-time updates to frontend"""
await websocket.accept()
active_websockets.append(websocket)
# Send initial data
await websocket.send_json(
{"type": "connected", "message": "Connected to VitalLink server"}
)
try:
while True:
# Keep connection alive and listen for client messages
data = await websocket.receive_text()
# Could handle client commands here
except:
active_websockets.remove(websocket)
async def broadcast_update(message: dict):
"""Broadcast update to all connected WebSocket clients"""
disconnected = []
for websocket in active_websockets:
try:
await websocket.send_json(message)
except:
disconnected.append(websocket)
# Remove disconnected clients
for ws in disconnected:
active_websockets.remove(ws)
# ============================================================================
# STARTUP / SHUTDOWN
# ============================================================================
@app.on_event("startup")
async def startup_event():
print("=" * 80)
print("VitalLink Backend API Started")
print("=" * 80)
print("API Documentation: http://localhost:8000/docs")
print("WebSocket Endpoint: ws://localhost:8000/ws")
print("=" * 80)
# ============================================================================
# INTEGRATION WITH SIMULATOR
# ============================================================================
async def simulator_integration_task():
"""
Background task to integrate with wristband simulator
In production, this receives data from actual base station
"""
# This would be replaced with actual base station connection
# For now, shows how to integrate the simulator
pass
# Run with: uvicorn vitalink_backend:app --reload
# Then access at http://localhost:8000