added wristband viewing inside of the control software for real and mock wristbands
This commit is contained in:
parent
8d04eb5594
commit
e6aba90ee5
@ -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)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
106155
|
||||
106168
|
||||
106176
|
||||
106211
|
||||
117391
|
||||
117404
|
||||
117411
|
||||
117454
|
||||
|
||||
Binary file not shown.
@ -1 +1 @@
|
||||
106155
|
||||
117391
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1 +1 @@
|
||||
106176
|
||||
117411
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1 +1 @@
|
||||
106211
|
||||
117454
|
||||
|
||||
@ -1 +1 @@
|
||||
106168
|
||||
117404
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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']}")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user