link start!
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user