added wristband viewing inside of the control software for real and mock wristbands

This commit is contained in:
Raika Furude 2025-10-18 18:06:37 -04:00
parent 8d04eb5594
commit e6aba90ee5
15 changed files with 567 additions and 575 deletions

View File

@ -14,7 +14,7 @@ import json
from collections import defaultdict
# ============================================================================
# LIFESPAN MANAGEMENT (Modern FastAPI Way)
# LIFESPAN MANAGEMENT
# ============================================================================
@ -38,10 +38,9 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="VitalLink API", version="1.0.0", lifespan=lifespan)
# CORS middleware for frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify your frontend domain
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -100,7 +99,7 @@ class QueuePosition(BaseModel):
# ============================================================================
# IN-MEMORY STORAGE (Replace with database in production)
# IN-MEMORY STORAGE
# ============================================================================
patients_db: Dict[str, Patient] = {}
@ -110,58 +109,44 @@ available_bands = [
]
active_websockets: List[WebSocket] = []
# Wristband details cache
wristband_details_cache = {}
# ============================================================================
# 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
score += (wait_minutes - 30) * 0.5
elif wait_minutes > 60:
score += (wait_minutes - 60) * 1.0 # Accelerate after 1 hour
score += (wait_minutes - 60) * 1.0
# 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:
@ -177,7 +162,6 @@ def calculate_priority_score(patient: Patient) -> float:
@app.get("/")
async def root():
"""Root endpoint"""
return {
"message": "VitalLink Backend API",
"version": "1.0.0",
@ -188,16 +172,12 @@ async def root():
@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,
@ -212,7 +192,6 @@ async def check_in_patient(data: PatientCheckIn):
patients_db[patient_id] = patient
# Notify connected clients
await broadcast_update({"type": "patient_added", "patient": patient.dict()})
return {
@ -224,24 +203,19 @@ async def check_in_patient(data: PatientCheckIn):
@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()}
)
@ -251,11 +225,8 @@ async def receive_vitals(data: VitalsData):
@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)
@ -283,7 +254,6 @@ async def get_queue():
)
)
# Sort by priority (highest first)
queue.sort(key=lambda x: x.priority_score, reverse=True)
return queue
@ -291,8 +261,6 @@ async def get_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")
@ -301,25 +269,21 @@ async def get_patient_details(patient_id: str):
return {
"patient": patient.dict(),
"vitals_history": [v.dict() for v in history[-50:]], # Last 50 readings
"vitals_history": [v.dict() for v in history[-50:]],
"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}
@ -327,8 +291,6 @@ async def discharge_patient(patient_id: str):
@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}
@ -356,34 +318,46 @@ async def get_statistics():
# ============================================================================
# WEBSOCKET FOR REAL-TIME UPDATES
# WRISTBAND ENDPOINTS
# ============================================================================
@app.post("/api/wristband-details")
async def update_wristband_details(data: dict):
"""Receive wristband details from wristband system"""
global wristband_details_cache
wristband_details_cache = data
return {"status": "updated"}
@app.get("/api/wristband-details")
async def get_cached_wristband_details():
"""Get cached wristband details"""
return wristband_details_cache
# ============================================================================
# WEBSOCKET
# ============================================================================
@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:
@ -391,7 +365,6 @@ async def broadcast_update(message: dict):
except:
disconnected.append(websocket)
# Remove disconnected clients
for ws in disconnected:
active_websockets.remove(ws)

View File

@ -1,5 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } from 'lucide-react';
import * as LucideIcons from 'lucide-react';
const { Activity, AlertCircle, Clock, Users, Bell, Heart, Thermometer, Wind, CheckCircle, UserX } = LucideIcons;
const API_BASE = 'http://localhost:8000';
@ -12,9 +14,11 @@ function App() {
average_wait_minutes: 0
});
const [filter, setFilter] = useState('all');
const [activeTab, setActiveTab] = useState('patients');
const [wristbands, setWristbands] = useState([]);
const [selectedWristband, setSelectedWristband] = useState(null);
useEffect(() => {
// Fetch data from backend
const fetchData = async () => {
try {
const queueResponse = await fetch(`${API_BASE}/api/queue`);
@ -23,7 +27,9 @@ function App() {
const statsResponse = await fetch(`${API_BASE}/api/stats`);
const statsData = await statsResponse.json();
// Set patients from backend
const wristbandResponse = await fetch(`${API_BASE}/api/wristband-details`);
const wristbandData = await wristbandResponse.json();
setPatients(queueData.map(p => ({
patient_id: p.patient_id,
band_id: p.band_id,
@ -34,97 +40,19 @@ function App() {
last_hr: p.last_hr,
last_spo2: p.last_spo2,
last_temp: p.last_temp,
symptoms: [] // Backend doesn't send symptoms in queue endpoint
symptoms: []
})));
setStats(statsData);
setWristbands(wristbandData.wristbands || []);
console.log(`✓ Fetched ${queueData.length} patients from backend`);
console.log(`✓ Fetched ${queueData.length} patients and ${wristbandData.wristbands?.length || 0} wristbands`);
} catch (error) {
console.error('Failed to fetch from backend:', error);
console.log('⚠️ Using mock data as fallback');
generateMockPatients();
}
};
// Mock data fallback function
const generateMockPatients = () => {
const mockPatients = [
{
patient_id: 'P100001',
band_id: 'VitalLink-A1B2',
name: 'John Smith',
tier: 'NORMAL',
priority_score: 15.2,
wait_time_minutes: 22,
last_hr: 76,
last_spo2: 98,
last_temp: 36.8,
symptoms: ['Chest Pain', 'Nausea']
},
{
patient_id: 'P100002',
band_id: 'VitalLink-C3D4',
name: 'Sarah Johnson',
tier: 'ALERT',
priority_score: 68.5,
wait_time_minutes: 45,
last_hr: 118,
last_spo2: 93,
last_temp: 38.4,
symptoms: ['Fever', 'Difficulty Breathing']
},
{
patient_id: 'P100003',
band_id: 'VitalLink-E5F6',
name: 'Michael Chen',
tier: 'EMERGENCY',
priority_score: 142.8,
wait_time_minutes: 8,
last_hr: 148,
last_spo2: 86,
last_temp: 39.7,
symptoms: ['Severe Headache', 'Chest Pain']
},
{
patient_id: 'P100004',
band_id: 'VitalLink-G7H8',
name: 'Emily Davis',
tier: 'NORMAL',
priority_score: 18.0,
wait_time_minutes: 35,
last_hr: 82,
last_spo2: 97,
last_temp: 37.1,
symptoms: ['Abdominal Pain']
},
{
patient_id: 'P100005',
band_id: 'VitalLink-I9J0',
name: 'Robert Williams',
tier: 'ALERT',
priority_score: 72.3,
wait_time_minutes: 52,
last_hr: 124,
last_spo2: 91,
last_temp: 38.8,
symptoms: ['Fever', 'Dizziness']
}
];
setPatients(mockPatients);
setStats({
total_patients: 5,
active_patients: 5,
tier_breakdown: { EMERGENCY: 1, ALERT: 2, NORMAL: 2 },
average_wait_minutes: 32.4
});
};
// Initial fetch
fetchData();
// Poll every 3 seconds for updates
const interval = setInterval(fetchData, 3000);
return () => clearInterval(interval);
@ -173,11 +101,9 @@ function App() {
method: 'POST',
});
console.log(`✓ Discharged patient ${patientId}`);
// Remove from local state
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
} catch (error) {
console.error('Failed to discharge patient:', error);
// Still remove from UI even if backend fails
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
}
};
@ -251,153 +177,351 @@ function App() {
</div>
</div>
<div className="max-w-7xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="flex gap-2">
<div className="bg-white border-b shadow-sm">
<div className="max-w-7xl mx-auto px-6">
<div className="flex gap-4 py-4">
<button
onClick={() => setFilter('all')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'all'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
onClick={() => setActiveTab('patients')}
className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${
activeTab === 'patients'
? 'bg-blue-100 text-blue-700 border-b-2 border-blue-600'
: 'text-gray-600 hover:text-gray-800'
}`}
>
All Patients ({patients.length})
📋 Patients ({patients.length})
</button>
<button
onClick={() => setFilter('EMERGENCY')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'EMERGENCY'
? 'bg-red-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
onClick={() => setActiveTab('wristbands')}
className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${
activeTab === 'wristbands'
? 'bg-purple-100 text-purple-700 border-b-2 border-purple-600'
: 'text-gray-600 hover:text-gray-800'
}`}
>
Emergency ({stats.tier_breakdown.EMERGENCY})
</button>
<button
onClick={() => setFilter('ALERT')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'ALERT'
? 'bg-yellow-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Alert ({stats.tier_breakdown.ALERT})
</button>
<button
onClick={() => setFilter('NORMAL')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'NORMAL'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Stable ({stats.tier_breakdown.NORMAL})
Wristbands ({wristbands.length})
</button>
</div>
</div>
</div>
<div className="space-y-4">
{filteredPatients.map((patient, index) => (
<div
key={patient.patient_id}
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4">
<div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1">
#{index + 1}
</div>
<div>
<h3 className="text-xl font-bold text-gray-800 mb-1">{patient.name}</h3>
<div className="flex items-center gap-3 text-sm text-gray-600">
<span className="font-mono">{patient.patient_id}</span>
<span></span>
<span className="font-mono">{patient.band_id}</span>
</div>
{patient.symptoms && patient.symptoms.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{patient.symptoms.map(symptom => (
<span key={symptom} className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium">
{symptom}
</span>
))}
<div className="max-w-7xl mx-auto p-6">
{activeTab === 'patients' ? (
<>
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
<div className="flex gap-2">
<button
onClick={() => setFilter('all')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'all'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
All Patients ({patients.length})
</button>
<button
onClick={() => setFilter('EMERGENCY')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'EMERGENCY'
? 'bg-red-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Emergency ({stats.tier_breakdown.EMERGENCY})
</button>
<button
onClick={() => setFilter('ALERT')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'ALERT'
? 'bg-yellow-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Alert ({stats.tier_breakdown.ALERT})
</button>
<button
onClick={() => setFilter('NORMAL')}
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
filter === 'NORMAL'
? 'bg-green-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Stable ({stats.tier_breakdown.NORMAL})
</button>
</div>
</div>
<div className="space-y-4">
{filteredPatients.map((patient, index) => (
<div
key={patient.patient_id}
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4">
<div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1">
#{index + 1}
</div>
<div>
<h3 className="text-xl font-bold text-gray-800 mb-1">{patient.name}</h3>
<div className="flex items-center gap-3 text-sm text-gray-600">
<span className="font-mono">{patient.patient_id}</span>
<span></span>
<span className="font-mono">{patient.band_id}</span>
</div>
{patient.symptoms && patient.symptoms.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{patient.symptoms.map(symptom => (
<span key={symptom} className="bg-gray-100 text-gray-700 px-3 py-1 rounded-full text-xs font-medium">
{symptom}
</span>
))}
</div>
)}
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-4 py-2 rounded-lg border-2 flex items-center gap-2 font-semibold ${getTierColor(patient.tier)}`}>
{getTierIcon(patient.tier)}
{patient.tier}
</div>
<button
onClick={() => handleDischarge(patient.patient_id)}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2 font-semibold"
>
<UserX className="w-4 h-4" />
Discharge
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-4 pt-4 border-t">
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Heart className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Heart Rate</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('hr', patient.last_hr)}`}>
{patient.last_hr}
</p>
<p className="text-xs text-gray-500 mt-1">bpm</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Wind className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">SpO₂</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('spo2', patient.last_spo2)}`}>
{patient.last_spo2}
</p>
<p className="text-xs text-gray-500 mt-1">%</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Thermometer className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Temperature</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('temp', patient.last_temp)}`}>
{patient.last_temp.toFixed(1)}
</p>
<p className="text-xs text-gray-500 mt-1">°C</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Wait Time</span>
</div>
<p className="text-2xl font-bold text-gray-700">
{patient.wait_time_minutes}
</p>
<p className="text-xs text-gray-500 mt-1">minutes</p>
</div>
</div>
</div>
</div>
))}
</div>
{filteredPatients.length === 0 && (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<Activity className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-600 mb-2">No patients in this category</h3>
<p className="text-gray-500">Patients will appear here as they check in</p>
</div>
)}
</>
) : (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow-sm p-6">
<h2 className="text-2xl font-bold mb-4">Wristband Inventory</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{wristbands.map(band => (
<div
key={band.band_id}
onClick={() => setSelectedWristband(band)}
className={`p-4 rounded-lg border-2 cursor-pointer transition-all ${
band.status === 'in_use'
? 'bg-blue-50 border-blue-300 hover:border-blue-400'
: 'bg-gray-50 border-gray-300 hover:border-gray-400'
}`}
>
<div className="flex items-center justify-between mb-2">
<span className="font-mono font-bold text-lg">
{band.type === 'real' ? '🔵' : '🟢'} {band.band_id}
</span>
<span className={`px-2 py-1 rounded text-xs font-semibold ${
band.status === 'in_use' ? 'bg-blue-600 text-white' :
band.status === 'available' ? 'bg-green-600 text-white' :
'bg-gray-400 text-white'
}`}>
{band.status.toUpperCase().replace('_', ' ')}
</span>
</div>
{band.patient_id && (
<div className="text-sm text-gray-600 mb-2">
Patient: <span className="font-mono font-semibold">{band.patient_id}</span>
</div>
)}
<div className="text-xs text-gray-500 flex justify-between">
<span>Packets: {band.packet_count}</span>
{band.is_monitoring && (
<span className="text-green-600 font-semibold"> LIVE</span>
)}
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-4 py-2 rounded-lg border-2 flex items-center gap-2 font-semibold ${getTierColor(patient.tier)}`}>
{getTierIcon(patient.tier)}
{patient.tier}
</div>
<button
onClick={() => handleDischarge(patient.patient_id)}
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors flex items-center gap-2 font-semibold"
>
<UserX className="w-4 h-4" />
Discharge
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-4 pt-4 border-t">
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Heart className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Heart Rate</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('hr', patient.last_hr)}`}>
{patient.last_hr}
</p>
<p className="text-xs text-gray-500 mt-1">bpm</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Wind className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">SpO₂</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('spo2', patient.last_spo2)}`}>
{patient.last_spo2}
</p>
<p className="text-xs text-gray-500 mt-1">%</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Thermometer className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Temperature</span>
</div>
<p className={`text-2xl font-bold ${getVitalStatus('temp', patient.last_temp)}`}>
{patient.last_temp.toFixed(1)}
</p>
<p className="text-xs text-gray-500 mt-1">°C</p>
</div>
<div className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-1">
<Clock className="w-4 h-4 text-gray-600" />
<span className="text-xs text-gray-600 font-medium">Wait Time</span>
</div>
<p className="text-2xl font-bold text-gray-700">
{patient.wait_time_minutes}
</p>
<p className="text-xs text-gray-500 mt-1">minutes</p>
</div>
</div>
))}
</div>
</div>
))}
</div>
{filteredPatients.length === 0 && (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<Activity className="w-16 h-16 text-gray-300 mx-auto mb-4" />
<h3 className="text-xl font-semibold text-gray-600 mb-2">No patients in this category</h3>
<p className="text-gray-500">Patients will appear here as they check in</p>
{wristbands.length === 0 && (
<p className="text-gray-500 text-center py-8">No wristbands configured</p>
)}
</div>
{selectedWristband && selectedWristband.last_raw_packet && (
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-xl font-bold flex items-center gap-2">
<span>Packet Details: {selectedWristband.band_id}</span>
{selectedWristband.type === 'simulated' && (
<span className="text-sm bg-green-100 text-green-800 px-2 py-1 rounded">MOCK</span>
)}
</h3>
<button
onClick={() => setSelectedWristband(null)}
className="text-gray-500 hover:text-gray-700 text-2xl font-bold"
>
</button>
</div>
<div className="mb-4">
<h4 className="font-semibold text-sm text-gray-600 mb-2">Raw Packet (16 bytes, Hex):</h4>
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm overflow-x-auto">
{selectedWristband.last_raw_packet.hex.toUpperCase().match(/.{1,2}/g).join(' ')}
</div>
<p className="text-xs text-gray-500 mt-1">
Format: [ver][seq][timestamp][flags][hr][spo2][temp_x100][activity_x100][checksum][rfu]
</p>
</div>
{selectedWristband.last_raw_packet.decoded && (
<>
<div className="mb-4">
<h4 className="font-semibold text-sm text-gray-600 mb-3">Decoded Fields:</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Version</div>
<div className="font-mono text-lg font-bold">0x{selectedWristband.last_raw_packet.decoded.version.toString(16).padStart(2, '0')}</div>
</div>
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Sequence #</div>
<div className="font-mono text-lg font-bold">{selectedWristband.last_raw_packet.decoded.sequence}</div>
</div>
<div className="bg-gray-50 p-3 rounded border col-span-2">
<div className="text-xs text-gray-600 mb-1">Timestamp (ms since boot)</div>
<div className="font-mono text-lg font-bold">{selectedWristband.last_raw_packet.decoded.timestamp_ms.toLocaleString()}</div>
</div>
<div className="bg-blue-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Heart Rate</div>
<div className="font-mono text-2xl font-bold text-blue-600">{selectedWristband.last_raw_packet.decoded.hr_bpm}</div>
<div className="text-xs text-gray-500">bpm</div>
</div>
<div className="bg-green-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">SpO₂</div>
<div className="font-mono text-2xl font-bold text-green-600">{selectedWristband.last_raw_packet.decoded.spo2}</div>
<div className="text-xs text-gray-500">%</div>
</div>
<div className="bg-orange-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Temperature</div>
<div className="font-mono text-2xl font-bold text-orange-600">{selectedWristband.last_raw_packet.decoded.temperature_c.toFixed(2)}</div>
<div className="text-xs text-gray-500">°C</div>
</div>
<div className="bg-purple-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Activity</div>
<div className="font-mono text-2xl font-bold text-purple-600">{selectedWristband.last_raw_packet.decoded.activity.toFixed(2)}</div>
<div className="text-xs text-gray-500">RMS</div>
</div>
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Checksum</div>
<div className="font-mono text-lg font-bold">{selectedWristband.last_raw_packet.decoded.checksum}</div>
</div>
<div className="bg-gray-50 p-3 rounded border">
<div className="text-xs text-gray-600 mb-1">Flags (raw)</div>
<div className="font-mono text-lg font-bold">0x{selectedWristband.last_raw_packet.decoded.flags.raw.toString(16).padStart(2, '0')}</div>
</div>
</div>
</div>
{selectedWristband.last_raw_packet.decoded.flags && (
<div className="mt-4">
<h4 className="font-semibold text-sm text-gray-600 mb-2">Status Flags:</h4>
<div className="flex flex-wrap gap-2">
{selectedWristband.last_raw_packet.decoded.flags.emergency && (
<span className="px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm font-semibold border border-red-300">
🚨 Bit 4: Emergency
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.alert && (
<span className="px-3 py-1 bg-yellow-100 text-yellow-800 rounded-full text-sm font-semibold border border-yellow-300">
Bit 3: Alert
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.sensor_fault && (
<span className="px-3 py-1 bg-purple-100 text-purple-800 rounded-full text-sm font-semibold border border-purple-300">
Bit 2: Sensor Fault
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.low_battery && (
<span className="px-3 py-1 bg-orange-100 text-orange-800 rounded-full text-sm font-semibold border border-orange-300">
🔋 Bit 1: Low Battery
</span>
)}
{selectedWristband.last_raw_packet.decoded.flags.motion_artifact && (
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-semibold border border-blue-300">
👋 Bit 0: Motion Artifact
</span>
)}
{!Object.values(selectedWristband.last_raw_packet.decoded.flags).some(v => v === true) && (
<span className="px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-sm font-semibold">
No flags set (all normal)
</span>
)}
</div>
</div>
)}
</>
)}
</div>
)}
</div>
)}
</div>

View File

@ -1,4 +1,4 @@
106155
106168
106176
106211
117391
117404
117411
117454

Binary file not shown.

View File

@ -1 +1 @@
106155
117391

View File

@ -5,7 +5,7 @@
Port 5173 is in use, trying another one...
Port 5174 is in use, trying another one...
VITE v7.1.10 ready in 103 ms
VITE v7.1.10 ready in 101 ms
➜ Local: http://localhost:5175/
➜ Network: use --host to expose

View File

@ -1 +1 @@
106176
117411

View File

@ -6,7 +6,7 @@ Port 5173 is in use, trying another one...
Port 5174 is in use, trying another one...
Port 5175 is in use, trying another one...
VITE v7.1.10 ready in 103 ms
VITE v7.1.10 ready in 102 ms
➜ Local: http://localhost:5176/
➜ Network: use --host to expose

View File

@ -1 +1 @@
106211
117454

View File

@ -1 +1 @@
106168
117404

View File

@ -167,7 +167,7 @@ def cli_inventory():
print("=" * 80)
print("\nSimulated Wristbands:")
simulated = config.get_simulated_bands() or [] # Add "or []"
simulated = config.get_simulated_bands() or []
if simulated:
for band in simulated:
print(f" 🟢 {band['band_id']:20} | Profile: {band['profile']}")
@ -175,7 +175,7 @@ def cli_inventory():
print(" (none configured)")
print("\nReal Wristbands (Hardware):")
real = config.get_real_bands() or [] # Add "or []"
real = config.get_real_bands() or []
if real:
for band in real:
print(f" 🔵 {band['band_id']:20} | BLE: {band['ble_address']}")

View File

@ -1,19 +1,16 @@
"""
VitalLink Main System Runner
Runs the complete system with real and/or simulated wristbands
Automatically assigns bands when patients check in via kiosk
"""
import asyncio
import aiohttp
from wristband_manager import WristbandManager
import time
import struct
from wristband_manager import WristbandManager, WristbandType
from config_system import WristbandConfig
import sys
# ============================================================================
# MAIN SYSTEM
# ============================================================================
class VitalLinkSystem:
"""Main system orchestrator"""
@ -26,38 +23,31 @@ class VitalLinkSystem:
self.monitoring_task = None
async def initialize(self):
"""Initialize the system"""
print("\n" + "=" * 80)
print("VitalLink System Initialization")
print("=" * 80 + "\n")
# Check backend availability
backend_ok = await self.check_backend()
if not backend_ok:
print("\n⚠️ Warning: Backend not running. System will wait for backend...")
# Scan for real wristbands if configured
if self.config.get("auto_scan_ble", False):
timeout = self.config.get("scan_timeout", 10.0)
await self.manager.scan_for_real_bands(timeout)
# Load configured real wristbands
for band_config in self.config.get_real_bands() or []:
self.manager.add_real_band(
band_config["band_id"], band_config["ble_address"]
)
# Load configured simulated wristbands
for band_config in self.config.get_simulated_bands() or []:
self.manager.add_simulated_band(
band_config["band_id"], band_config.get("profile", "stable")
)
# Show inventory
self.manager.print_inventory()
async def check_backend(self):
"""Check if backend is running"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(
@ -66,14 +56,90 @@ class VitalLinkSystem:
if resp.status == 200:
print(f"✓ Backend is running at {self.backend_url}")
return True
except Exception as e:
except:
print(f"❌ Backend not reachable at {self.backend_url}")
return False
return False
def _decode_packet_for_display(self, packet: bytes) -> dict:
if len(packet) != 16:
return {}
PKT_STRUCT = struct.Struct("<B H I B B B h H B B")
(
ver,
seq,
ts_ms,
flags,
hr_bpm,
spo2,
skin_c_x100,
act_rms_x100,
checksum,
rfu,
) = PKT_STRUCT.unpack(packet)
return {
"version": ver,
"sequence": seq,
"timestamp_ms": ts_ms,
"flags": {
"raw": flags,
"motion_artifact": bool(flags & (1 << 0)),
"low_battery": bool(flags & (1 << 1)),
"sensor_fault": bool(flags & (1 << 2)),
"alert": bool(flags & (1 << 3)),
"emergency": bool(flags & (1 << 4)),
},
"hr_bpm": hr_bpm,
"spo2": spo2,
"temperature_c": skin_c_x100 / 100.0,
"activity": act_rms_x100 / 100.0,
"checksum": f"0x{checksum:02X}",
"reserved": rfu,
}
async def report_inventory_to_backend(self):
try:
inventory = {"timestamp": time.time(), "wristbands": []}
for band_id, band in self.manager.inventory.items():
band_info = {
"band_id": band.band_id,
"type": band.type.value,
"status": band.status.value,
"patient_id": band.patient_id,
"packet_count": band.packet_count,
"is_monitoring": band_id in self.manager.active_monitoring,
"last_packet": band.last_packet,
}
if band.type == WristbandType.SIMULATED and hasattr(
band, "last_raw_packet"
):
if band.last_raw_packet:
band_info["last_raw_packet"] = {
"hex": band.last_raw_packet.hex(),
"bytes": list(band.last_raw_packet),
"decoded": self._decode_packet_for_display(
band.last_raw_packet
),
}
if hasattr(band, "packet_history"):
band_info["recent_packets"] = band.packet_history[-5:]
inventory["wristbands"].append(band_info)
async with aiohttp.ClientSession() as session:
await session.post(
f"{self.backend_url}/api/wristband-details", json=inventory
)
except:
pass
async def monitor_new_patients(self):
"""Monitor backend for new patient check-ins and auto-assign bands"""
print("\n🔍 Monitoring for new patient check-ins...")
known_patients = set()
@ -89,11 +155,9 @@ class VitalLinkSystem:
for patient in queue:
patient_id = patient["patient_id"]
# New patient detected
if patient_id not in known_patients:
known_patients.add(patient_id)
# Check if already has a band assigned and monitoring
has_active_band = any(
b.patient_id == patient_id
and b.band_id in self.manager.active_monitoring
@ -105,7 +169,6 @@ class VitalLinkSystem:
f"\n🆕 New patient detected: {patient_id} ({patient['name']})"
)
# Try to assign a band
band = self.manager.assign_band(
patient_id, prefer_real=prefer_real
)
@ -114,18 +177,15 @@ class VitalLinkSystem:
print(
f" ✓ Assigned {band.band_id} ({band.type.value})"
)
# Start monitoring
await self.manager.start_monitoring(
band.band_id
)
else:
# No bands available - create a new simulated one on the fly
print(
f" ⚠️ No bands available, creating emergency simulated band..."
)
emergency_band_id = f"VitalLink-EMRG{len(self.manager.inventory):02d}"
emergency_band_id = f"MOCK-EMRG{len(self.manager.inventory):02d}"
band = self.manager.add_simulated_band(
emergency_band_id, "stable"
)
@ -134,20 +194,15 @@ class VitalLinkSystem:
print(
f" ✓ Created and assigned {emergency_band_id}"
)
await self.manager.start_monitoring(
band.band_id
)
except Exception as e:
# Silently continue if backend temporarily unavailable
except:
pass
# Check every 2 seconds
await asyncio.sleep(2)
async def run(self):
"""Run the main system"""
self.running = True
await self.initialize()
@ -164,15 +219,12 @@ class VitalLinkSystem:
print("\nPress Ctrl+C to stop\n")
print("=" * 80 + "\n")
# Start monitoring for new patients
self.monitoring_task = asyncio.create_task(self.monitor_new_patients())
try:
# Keep running until interrupted
while self.running:
await asyncio.sleep(10)
# Periodic status update
status = self.manager.get_status()
available = status["status_breakdown"].get("available", 0)
@ -183,19 +235,18 @@ class VitalLinkSystem:
f"Sim: {status['simulated_bands']}"
)
await self.report_inventory_to_backend()
except KeyboardInterrupt:
print("\n\n⚠️ Shutting down...")
await self.shutdown()
async def shutdown(self):
"""Clean shutdown"""
self.running = False
# Cancel monitoring task
if self.monitoring_task:
self.monitoring_task.cancel()
# Stop all monitoring
print("Stopping all wristband monitoring...")
for band_id in list(self.manager.active_monitoring.keys()):
await self.manager.stop_monitoring(band_id)
@ -204,82 +255,10 @@ class VitalLinkSystem:
self.manager.print_inventory()
# ============================================================================
# CLI MODES
# ============================================================================
async def interactive_mode():
"""Interactive mode with menu"""
system = VitalLinkSystem()
await system.initialize()
while True:
print("\n" + "=" * 80)
print("VITALLINK INTERACTIVE MODE")
print("=" * 80)
print("\n1. Show inventory")
print("2. Scan for real wristbands")
print("3. Assign band to patient")
print("4. Release band")
print("5. Start auto-monitoring mode")
print("6. Exit")
choice = input("\nSelect option: ")
if choice == "1":
system.manager.print_inventory()
elif choice == "2":
await system.manager.scan_for_real_bands(timeout=10.0)
elif choice == "3":
patient_id = input("Patient ID: ")
prefer_real = input("Prefer real band? (y/n): ").lower() == "y"
band = system.manager.assign_band(patient_id, prefer_real=prefer_real)
if band:
await system.manager.start_monitoring(band.band_id)
elif choice == "4":
band_id = input("Band ID to release: ")
await system.manager.release_band(band_id)
elif choice == "5":
await system.run()
break
elif choice == "6":
print("Goodbye!")
break
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="VitalLink System")
parser.add_argument(
"--interactive", "-i", action="store_true", help="Run in interactive mode"
)
parser.add_argument(
"--config",
"-c",
default="wristband_config.yaml",
help="Configuration file path",
)
args = parser.parse_args()
try:
if args.interactive:
asyncio.run(interactive_mode())
else:
# Normal automatic mode
system = VitalLinkSystem()
asyncio.run(system.run())
system = VitalLinkSystem()
asyncio.run(system.run())
except KeyboardInterrupt:
print("\n\nExiting...")
sys.exit(0)

View File

@ -5,6 +5,7 @@ Unified system for managing real and simulated wristbands
import asyncio
import struct
import random
import time
import aiohttp
from typing import Dict, List, Optional, Union
@ -40,15 +41,15 @@ class WristbandType(Enum):
class WristbandStatus(Enum):
AVAILABLE = "available" # Charged and ready
ASSIGNED = "assigned" # Checked out to patient
IN_USE = "in_use" # Actively sending data
CHARGING = "charging" # On charger
MAINTENANCE = "maintenance" # Needs attention
AVAILABLE = "available"
ASSIGNED = "assigned"
IN_USE = "in_use"
CHARGING = "charging"
MAINTENANCE = "maintenance"
# ============================================================================
# PACKET DECODER (Works with real hardware packets)
# PACKET DECODER
# ============================================================================
@ -61,13 +62,11 @@ class PacketDecoder:
if len(data) != 16:
return None
# Verify checksum
checksum_calc = sum(data[0:14]) & 0xFF
if checksum_calc != data[14]:
print(f"⚠️ Checksum failed: expected {data[14]}, got {checksum_calc}")
return None
# Unpack according to spec
(
ver,
seq,
@ -81,11 +80,10 @@ class PacketDecoder:
rfu,
) = PKT_STRUCT.unpack(data)
# Determine tier from flags
tier = "NORMAL"
if flags & (1 << 4): # EMERGENCY bit
if flags & (1 << 4):
tier = "EMERGENCY"
elif flags & (1 << 3): # ALERT bit
elif flags & (1 << 3):
tier = "ALERT"
return {
@ -108,7 +106,7 @@ class PacketDecoder:
class BaseWristband(ABC):
"""Abstract base class for all wristbands (real and simulated)"""
"""Abstract base class for all wristbands"""
def __init__(self, band_id: str, wristband_type: WristbandType):
self.band_id = band_id
@ -120,22 +118,18 @@ class BaseWristband(ABC):
@abstractmethod
async def start_monitoring(self):
"""Start receiving data from wristband"""
pass
@abstractmethod
async def stop_monitoring(self):
"""Stop receiving data"""
pass
def assign_to_patient(self, patient_id: str):
"""Assign wristband to a patient"""
self.patient_id = patient_id
self.status = WristbandStatus.ASSIGNED
print(f"{self.band_id} assigned to patient {patient_id}")
def release(self):
"""Release wristband (return to inventory)"""
self.patient_id = None
self.status = WristbandStatus.AVAILABLE
self.packet_count = 0
@ -143,7 +137,7 @@ class BaseWristband(ABC):
# ============================================================================
# REAL WRISTBAND (Hardware BLE)
# REAL WRISTBAND
# ============================================================================
@ -157,7 +151,6 @@ class RealWristband(BaseWristband):
self.decoder = PacketDecoder()
async def start_monitoring(self):
"""Connect to real wristband via BLE and start receiving data"""
if not BLE_AVAILABLE:
raise RuntimeError("Bleak library not available")
@ -171,7 +164,6 @@ class RealWristband(BaseWristband):
await self.client.connect()
print(f"✓ Connected to {self.band_id}")
# Subscribe to notifications
await self.client.start_notify(CHAR_UUID, self._notification_handler)
print(f"✓ Subscribed to notifications from {self.band_id}")
@ -180,102 +172,13 @@ class RealWristband(BaseWristband):
self.status = WristbandStatus.MAINTENANCE
def _notification_handler(self, sender, data: bytearray):
"""Handle incoming BLE notifications"""
decoded = self.decoder.decode(bytes(data))
if decoded:
self.last_packet = decoded
self.packet_count += 1
# Send to backend
asyncio.create_task(self._send_to_backend(decoded))
async def _send_to_backend(self, decoded: dict):
"""Send decoded data to backend"""
if not self.patient_id:
return
payload = {
"band_id": self.band_id,
"patient_id": self.patient_id,
"timestamp": time.time(),
"ver": decoded["ver"],
"seq": decoded["seq"],
"ts_ms": decoded["ts_ms"],
"tier": decoded["tier"],
"flags": [], # Convert flags to list if needed
"hr_bpm": decoded["hr_bpm"],
"spo2": decoded["spo2"],
"temp_c": decoded["temp_c"],
"activity": decoded["activity"],
}
try:
async with aiohttp.ClientSession() as session:
await session.post(f"{BACKEND_URL}/api/vitals", json=payload)
except:
pass # Silently fail if backend unavailable
async def stop_monitoring(self):
"""Disconnect from wristband"""
if self.client and self.client.is_connected:
await self.client.stop_notify(CHAR_UUID)
await self.client.disconnect()
print(f"✓ Disconnected from {self.band_id}")
self.status = WristbandStatus.AVAILABLE
# ============================================================================
# SIMULATED WRISTBAND (For testing without hardware)
# ============================================================================
class SimulatedWristband(BaseWristband):
"""Simulated wristband for testing"""
def __init__(self, band_id: str, profile: str = "stable"):
super().__init__(band_id, WristbandType.SIMULATED)
self.profile = profile
self.seq = 0
self.running = False
# Import simulator profiles
from wristband_simulator import PATIENT_PROFILES, WristbandSimulator
self.simulator = WristbandSimulator(
band_id, PATIENT_PROFILES.get(profile, PATIENT_PROFILES["stable"]), None
)
async def start_monitoring(self):
"""Start simulated data generation"""
self.status = WristbandStatus.IN_USE
self.running = True
self.simulator.patient_id = self.patient_id
print(f"🟢 Starting simulated wristband {self.band_id} ({self.profile})")
while self.running:
# Generate packet
packet = self.simulator.generate_packet()
# Decode it
decoder = PacketDecoder()
decoded = decoder.decode(packet)
if decoded:
self.last_packet = decoded
self.packet_count += 1
# Send to backend
await self._send_to_backend(decoded)
# Wait based on tier
tier = self.simulator.tier
interval = 1.0 if tier in ["ALERT", "EMERGENCY"] else 60.0
await asyncio.sleep(interval)
async def _send_to_backend(self, decoded: dict):
"""Send to backend"""
if not self.patient_id:
return
@ -301,7 +204,91 @@ class SimulatedWristband(BaseWristband):
pass
async def stop_monitoring(self):
"""Stop simulation"""
if self.client and self.client.is_connected:
await self.client.stop_notify(CHAR_UUID)
await self.client.disconnect()
print(f"✓ Disconnected from {self.band_id}")
self.status = WristbandStatus.AVAILABLE
# ============================================================================
# SIMULATED WRISTBAND
# ============================================================================
class SimulatedWristband(BaseWristband):
"""Simulated wristband for testing"""
def __init__(self, band_id: str, profile: str = "stable"):
super().__init__(band_id, WristbandType.SIMULATED)
self.profile = profile
self.seq = 0
self.running = False
self.last_raw_packet = None
self.packet_history = []
from wristband_simulator import PATIENT_PROFILES, WristbandSimulator
self.simulator = WristbandSimulator(
band_id, PATIENT_PROFILES.get(profile, PATIENT_PROFILES["stable"]), None
)
async def start_monitoring(self):
self.status = WristbandStatus.IN_USE
self.running = True
self.simulator.patient_id = self.patient_id
print(f"🟢 Starting simulated wristband {self.band_id} ({self.profile})")
while self.running:
packet = self.simulator.generate_packet()
self.last_raw_packet = packet
self.packet_history.append(
{"timestamp": time.time(), "raw": packet.hex(), "bytes": list(packet)}
)
if len(self.packet_history) > 50:
self.packet_history = self.packet_history[-50:]
decoder = PacketDecoder()
decoded = decoder.decode(packet)
if decoded:
self.last_packet = decoded
self.packet_count += 1
await self._send_to_backend(decoded)
tier = self.simulator.tier
interval = 1.0 if tier in ["ALERT", "EMERGENCY"] else 60.0
await asyncio.sleep(interval)
async def _send_to_backend(self, decoded: dict):
if not self.patient_id:
return
payload = {
"band_id": self.band_id,
"patient_id": self.patient_id,
"timestamp": time.time(),
"ver": decoded["ver"],
"seq": decoded["seq"],
"ts_ms": decoded["ts_ms"],
"tier": decoded["tier"],
"flags": [],
"hr_bpm": decoded["hr_bpm"],
"spo2": decoded["spo2"],
"temp_c": decoded["temp_c"],
"activity": decoded["activity"],
}
try:
async with aiohttp.ClientSession() as session:
await session.post(f"{BACKEND_URL}/api/vitals", json=payload)
except:
pass
async def stop_monitoring(self):
self.running = False
self.status = WristbandStatus.AVAILABLE
print(f"✓ Stopped simulated wristband {self.band_id}")
@ -313,17 +300,15 @@ class SimulatedWristband(BaseWristband):
class WristbandManager:
"""Central manager for all wristbands (real and simulated)"""
"""Central manager for all wristbands"""
def __init__(self):
self.inventory: Dict[str, BaseWristband] = {}
self.active_monitoring: Dict[str, asyncio.Task] = {}
def add_simulated_band(self, band_id: str, profile: str = "stable"):
"""Add a simulated wristband to inventory"""
# Change naming to use MOCK prefix if not already present
if not band_id.startswith("MOCK-"):
band_id = f"MOCK-{band_id.replace('VitalLink-', '')}"
band_id = f"MOCK-{band_id.replace('VitalLink-', '').replace('MOCK-', '')}"
band = SimulatedWristband(band_id, profile)
self.inventory[band_id] = band
@ -331,7 +316,6 @@ class WristbandManager:
return band
def add_real_band(self, band_id: str, ble_address: str):
"""Add a real wristband to inventory"""
if not BLE_AVAILABLE:
print("❌ Cannot add real band: Bleak not installed")
return None
@ -342,7 +326,6 @@ class WristbandManager:
return band
async def scan_for_real_bands(self, timeout: float = 10.0):
"""Scan for real wristbands and add them to inventory"""
if not BLE_AVAILABLE:
print("❌ BLE scanning not available: Install bleak")
return []
@ -352,7 +335,6 @@ class WristbandManager:
found = []
for device in devices:
# Check if device advertises our service UUID
uuids = device.metadata.get("uuids", [])
if any(uuid.lower() == SERVICE_UUID.lower() for uuid in uuids):
band_id = (
@ -365,7 +347,6 @@ class WristbandManager:
return found
def get_available_bands(self) -> List[BaseWristband]:
"""Get list of available (not assigned) wristbands"""
return [
b for b in self.inventory.values() if b.status == WristbandStatus.AVAILABLE
]
@ -373,20 +354,15 @@ class WristbandManager:
def assign_band(
self, patient_id: str, prefer_real: bool = False
) -> Optional[BaseWristband]:
"""Assign an available band to a patient"""
available = self.get_available_bands()
if not available:
print("❌ No wristbands available")
return None
# Prefer real bands if requested and available
if prefer_real:
real_bands = [b for b in available if b.type == WristbandType.REAL]
if real_bands:
band = real_bands[0]
else:
band = available[0]
band = real_bands[0] if real_bands else available[0]
else:
band = available[0]
@ -394,7 +370,6 @@ class WristbandManager:
return band
async def start_monitoring(self, band_id: str):
"""Start monitoring a wristband"""
if band_id not in self.inventory:
print(f"❌ Band {band_id} not in inventory")
return
@ -404,12 +379,10 @@ class WristbandManager:
print(f"⚠️ {band_id} already being monitored")
return
# Create monitoring task
task = asyncio.create_task(band.start_monitoring())
self.active_monitoring[band_id] = task
async def stop_monitoring(self, band_id: str):
"""Stop monitoring a wristband"""
if band_id in self.active_monitoring:
task = self.active_monitoring[band_id]
task.cancel()
@ -419,13 +392,11 @@ class WristbandManager:
await self.inventory[band_id].stop_monitoring()
async def release_band(self, band_id: str):
"""Release band back to inventory"""
await self.stop_monitoring(band_id)
if band_id in self.inventory:
self.inventory[band_id].release()
def get_status(self) -> dict:
"""Get overall status"""
status_counts = {}
for status in WristbandStatus:
status_counts[status.value] = sum(
@ -445,7 +416,6 @@ class WristbandManager:
}
def print_inventory(self):
"""Print current inventory"""
print("\n" + "=" * 80)
print("WRISTBAND INVENTORY")
print("=" * 80)
@ -471,57 +441,3 @@ class WristbandManager:
f"Active: {status['active_monitoring']}"
)
print("=" * 80 + "\n")
# ============================================================================
# EXAMPLE USAGE
# ============================================================================
async def main():
"""Example usage of wristband manager"""
manager = WristbandManager()
print("VitalLink Wristband Management System")
print("=" * 80)
# Option 1: Scan for real wristbands
# await manager.scan_for_real_bands(timeout=10.0)
# Option 2: Add simulated wristbands
manager.add_simulated_band("VitalLink-SIM1", "stable")
manager.add_simulated_band("VitalLink-SIM2", "mild_anxiety")
manager.add_simulated_band("VitalLink-SIM3", "deteriorating")
# Option 3: Manually add real wristband if you know the address
# manager.add_real_band("VitalLink-REAL1", "D7:91:3F:9A:12:34")
# Show inventory
manager.print_inventory()
# Assign bands to patients
band1 = manager.assign_band("P100001")
band2 = manager.assign_band("P100002")
# Start monitoring
if band1:
await manager.start_monitoring(band1.band_id)
if band2:
await manager.start_monitoring(band2.band_id)
# Monitor for 30 seconds
print("\nMonitoring for 30 seconds...")
await asyncio.sleep(30)
# Stop and release
if band1:
await manager.release_band(band1.band_id)
if band2:
await manager.release_band(band2.band_id)
manager.print_inventory()
if __name__ == "__main__":
asyncio.run(main())