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
|
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)
|
app = FastAPI(title="VitalLink API", version="1.0.0", lifespan=lifespan)
|
||||||
|
|
||||||
# CORS middleware for frontend
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"], # In production, specify your frontend domain
|
allow_origins=["*"],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
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] = {}
|
patients_db: Dict[str, Patient] = {}
|
||||||
@ -110,58 +109,44 @@ available_bands = [
|
|||||||
]
|
]
|
||||||
active_websockets: List[WebSocket] = []
|
active_websockets: List[WebSocket] = []
|
||||||
|
|
||||||
|
# Wristband details cache
|
||||||
|
wristband_details_cache = {}
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PRIORITY ALGORITHM
|
# PRIORITY ALGORITHM
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
def calculate_priority_score(patient: Patient) -> float:
|
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
|
score = 0.0
|
||||||
|
|
||||||
# Tier contribution (largest factor)
|
|
||||||
tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0}
|
tier_scores = {"EMERGENCY": 100, "ALERT": 50, "NORMAL": 0}
|
||||||
score += tier_scores.get(patient.current_tier, 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
|
wait_minutes = (datetime.now() - patient.check_in_time).total_seconds() / 60
|
||||||
if wait_minutes > 30:
|
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:
|
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}
|
severity_scores = {"severe": 20, "moderate": 10, "mild": 5}
|
||||||
score += severity_scores.get(patient.severity, 0)
|
score += severity_scores.get(patient.severity, 0)
|
||||||
|
|
||||||
# Vital signs contribution (if available)
|
|
||||||
if patient.last_vitals:
|
if patient.last_vitals:
|
||||||
hr = patient.last_vitals.get("hr_bpm", 75)
|
hr = patient.last_vitals.get("hr_bpm", 75)
|
||||||
spo2 = patient.last_vitals.get("spo2", 98)
|
spo2 = patient.last_vitals.get("spo2", 98)
|
||||||
temp = patient.last_vitals.get("temp_c", 37.0)
|
temp = patient.last_vitals.get("temp_c", 37.0)
|
||||||
|
|
||||||
# Abnormal HR
|
|
||||||
if hr > 110 or hr < 50:
|
if hr > 110 or hr < 50:
|
||||||
score += 10
|
score += 10
|
||||||
if hr > 140 or hr < 40:
|
if hr > 140 or hr < 40:
|
||||||
score += 30
|
score += 30
|
||||||
|
|
||||||
# Low SpO2 (critical)
|
|
||||||
if spo2 < 92:
|
if spo2 < 92:
|
||||||
score += 15
|
score += 15
|
||||||
if spo2 < 88:
|
if spo2 < 88:
|
||||||
score += 40
|
score += 40
|
||||||
|
|
||||||
# Fever
|
|
||||||
if temp > 38.5:
|
if temp > 38.5:
|
||||||
score += 15
|
score += 15
|
||||||
if temp > 39.5:
|
if temp > 39.5:
|
||||||
@ -177,7 +162,6 @@ def calculate_priority_score(patient: Patient) -> float:
|
|||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root():
|
||||||
"""Root endpoint"""
|
|
||||||
return {
|
return {
|
||||||
"message": "VitalLink Backend API",
|
"message": "VitalLink Backend API",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -188,16 +172,12 @@ async def root():
|
|||||||
|
|
||||||
@app.post("/api/checkin")
|
@app.post("/api/checkin")
|
||||||
async def check_in_patient(data: PatientCheckIn):
|
async def check_in_patient(data: PatientCheckIn):
|
||||||
"""Register a new patient and assign wristband"""
|
|
||||||
|
|
||||||
if not available_bands:
|
if not available_bands:
|
||||||
raise HTTPException(status_code=503, detail="No wristbands available")
|
raise HTTPException(status_code=503, detail="No wristbands available")
|
||||||
|
|
||||||
# Assign IDs
|
|
||||||
patient_id = f"P{len(patients_db) + 100001}"
|
patient_id = f"P{len(patients_db) + 100001}"
|
||||||
band_id = available_bands.pop(0)
|
band_id = available_bands.pop(0)
|
||||||
|
|
||||||
# Create patient record
|
|
||||||
patient = Patient(
|
patient = Patient(
|
||||||
patient_id=patient_id,
|
patient_id=patient_id,
|
||||||
band_id=band_id,
|
band_id=band_id,
|
||||||
@ -212,7 +192,6 @@ async def check_in_patient(data: PatientCheckIn):
|
|||||||
|
|
||||||
patients_db[patient_id] = patient
|
patients_db[patient_id] = patient
|
||||||
|
|
||||||
# Notify connected clients
|
|
||||||
await broadcast_update({"type": "patient_added", "patient": patient.dict()})
|
await broadcast_update({"type": "patient_added", "patient": patient.dict()})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -224,24 +203,19 @@ async def check_in_patient(data: PatientCheckIn):
|
|||||||
|
|
||||||
@app.post("/api/vitals")
|
@app.post("/api/vitals")
|
||||||
async def receive_vitals(data: VitalsData):
|
async def receive_vitals(data: VitalsData):
|
||||||
"""Receive vitals data from base station"""
|
|
||||||
|
|
||||||
patient_id = data.patient_id
|
patient_id = data.patient_id
|
||||||
|
|
||||||
if patient_id not in patients_db:
|
if patient_id not in patients_db:
|
||||||
raise HTTPException(status_code=404, detail="Patient not found")
|
raise HTTPException(status_code=404, detail="Patient not found")
|
||||||
|
|
||||||
# Update patient record
|
|
||||||
patient = patients_db[patient_id]
|
patient = patients_db[patient_id]
|
||||||
patient.current_tier = data.tier
|
patient.current_tier = data.tier
|
||||||
patient.last_vitals = data.dict()
|
patient.last_vitals = data.dict()
|
||||||
|
|
||||||
# Store in history (keep last 1000 readings)
|
|
||||||
vitals_history[patient_id].append(data)
|
vitals_history[patient_id].append(data)
|
||||||
if len(vitals_history[patient_id]) > 1000:
|
if len(vitals_history[patient_id]) > 1000:
|
||||||
vitals_history[patient_id] = vitals_history[patient_id][-1000:]
|
vitals_history[patient_id] = vitals_history[patient_id][-1000:]
|
||||||
|
|
||||||
# Broadcast to connected clients
|
|
||||||
await broadcast_update(
|
await broadcast_update(
|
||||||
{"type": "vitals_update", "patient_id": patient_id, "vitals": data.dict()}
|
{"type": "vitals_update", "patient_id": patient_id, "vitals": data.dict()}
|
||||||
)
|
)
|
||||||
@ -251,11 +225,8 @@ async def receive_vitals(data: VitalsData):
|
|||||||
|
|
||||||
@app.get("/api/queue")
|
@app.get("/api/queue")
|
||||||
async def get_queue():
|
async def get_queue():
|
||||||
"""Get prioritized queue of active patients"""
|
|
||||||
|
|
||||||
active_patients = [p for p in patients_db.values() if p.is_active]
|
active_patients = [p for p in patients_db.values() if p.is_active]
|
||||||
|
|
||||||
# Calculate priority and sort
|
|
||||||
queue = []
|
queue = []
|
||||||
for patient in active_patients:
|
for patient in active_patients:
|
||||||
priority_score = calculate_priority_score(patient)
|
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)
|
queue.sort(key=lambda x: x.priority_score, reverse=True)
|
||||||
|
|
||||||
return queue
|
return queue
|
||||||
@ -291,8 +261,6 @@ async def get_queue():
|
|||||||
|
|
||||||
@app.get("/api/patients/{patient_id}")
|
@app.get("/api/patients/{patient_id}")
|
||||||
async def get_patient_details(patient_id: str):
|
async def get_patient_details(patient_id: str):
|
||||||
"""Get detailed information about a specific patient"""
|
|
||||||
|
|
||||||
if patient_id not in patients_db:
|
if patient_id not in patients_db:
|
||||||
raise HTTPException(status_code=404, detail="Patient not found")
|
raise HTTPException(status_code=404, detail="Patient not found")
|
||||||
|
|
||||||
@ -301,25 +269,21 @@ async def get_patient_details(patient_id: str):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"patient": patient.dict(),
|
"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),
|
"priority_score": calculate_priority_score(patient),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/patients/{patient_id}/discharge")
|
@app.post("/api/patients/{patient_id}/discharge")
|
||||||
async def discharge_patient(patient_id: str):
|
async def discharge_patient(patient_id: str):
|
||||||
"""Discharge a patient and return wristband to pool"""
|
|
||||||
|
|
||||||
if patient_id not in patients_db:
|
if patient_id not in patients_db:
|
||||||
raise HTTPException(status_code=404, detail="Patient not found")
|
raise HTTPException(status_code=404, detail="Patient not found")
|
||||||
|
|
||||||
patient = patients_db[patient_id]
|
patient = patients_db[patient_id]
|
||||||
patient.is_active = False
|
patient.is_active = False
|
||||||
|
|
||||||
# Return band to pool
|
|
||||||
available_bands.append(patient.band_id)
|
available_bands.append(patient.band_id)
|
||||||
|
|
||||||
# Notify clients
|
|
||||||
await broadcast_update({"type": "patient_discharged", "patient_id": patient_id})
|
await broadcast_update({"type": "patient_discharged", "patient_id": patient_id})
|
||||||
|
|
||||||
return {"message": "Patient discharged", "band_returned": patient.band_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")
|
@app.get("/api/stats")
|
||||||
async def get_statistics():
|
async def get_statistics():
|
||||||
"""Get overall ER statistics"""
|
|
||||||
|
|
||||||
active_patients = [p for p in patients_db.values() if p.is_active]
|
active_patients = [p for p in patients_db.values() if p.is_active]
|
||||||
|
|
||||||
tier_counts = {"EMERGENCY": 0, "ALERT": 0, "NORMAL": 0}
|
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")
|
@app.websocket("/ws")
|
||||||
async def websocket_endpoint(websocket: WebSocket):
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
"""WebSocket connection for real-time updates to frontend"""
|
|
||||||
|
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
active_websockets.append(websocket)
|
active_websockets.append(websocket)
|
||||||
|
|
||||||
# Send initial data
|
|
||||||
await websocket.send_json(
|
await websocket.send_json(
|
||||||
{"type": "connected", "message": "Connected to VitalLink server"}
|
{"type": "connected", "message": "Connected to VitalLink server"}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# Keep connection alive and listen for client messages
|
|
||||||
data = await websocket.receive_text()
|
data = await websocket.receive_text()
|
||||||
# Could handle client commands here
|
|
||||||
except:
|
except:
|
||||||
active_websockets.remove(websocket)
|
active_websockets.remove(websocket)
|
||||||
|
|
||||||
|
|
||||||
async def broadcast_update(message: dict):
|
async def broadcast_update(message: dict):
|
||||||
"""Broadcast update to all connected WebSocket clients"""
|
|
||||||
|
|
||||||
disconnected = []
|
disconnected = []
|
||||||
for websocket in active_websockets:
|
for websocket in active_websockets:
|
||||||
try:
|
try:
|
||||||
@ -391,7 +365,6 @@ async def broadcast_update(message: dict):
|
|||||||
except:
|
except:
|
||||||
disconnected.append(websocket)
|
disconnected.append(websocket)
|
||||||
|
|
||||||
# Remove disconnected clients
|
|
||||||
for ws in disconnected:
|
for ws in disconnected:
|
||||||
active_websockets.remove(ws)
|
active_websockets.remove(ws)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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';
|
const API_BASE = 'http://localhost:8000';
|
||||||
|
|
||||||
@ -12,9 +14,11 @@ function App() {
|
|||||||
average_wait_minutes: 0
|
average_wait_minutes: 0
|
||||||
});
|
});
|
||||||
const [filter, setFilter] = useState('all');
|
const [filter, setFilter] = useState('all');
|
||||||
|
const [activeTab, setActiveTab] = useState('patients');
|
||||||
|
const [wristbands, setWristbands] = useState([]);
|
||||||
|
const [selectedWristband, setSelectedWristband] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch data from backend
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const queueResponse = await fetch(`${API_BASE}/api/queue`);
|
const queueResponse = await fetch(`${API_BASE}/api/queue`);
|
||||||
@ -23,7 +27,9 @@ function App() {
|
|||||||
const statsResponse = await fetch(`${API_BASE}/api/stats`);
|
const statsResponse = await fetch(`${API_BASE}/api/stats`);
|
||||||
const statsData = await statsResponse.json();
|
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 => ({
|
setPatients(queueData.map(p => ({
|
||||||
patient_id: p.patient_id,
|
patient_id: p.patient_id,
|
||||||
band_id: p.band_id,
|
band_id: p.band_id,
|
||||||
@ -34,97 +40,19 @@ function App() {
|
|||||||
last_hr: p.last_hr,
|
last_hr: p.last_hr,
|
||||||
last_spo2: p.last_spo2,
|
last_spo2: p.last_spo2,
|
||||||
last_temp: p.last_temp,
|
last_temp: p.last_temp,
|
||||||
symptoms: [] // Backend doesn't send symptoms in queue endpoint
|
symptoms: []
|
||||||
})));
|
})));
|
||||||
|
|
||||||
setStats(statsData);
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch from backend:', 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();
|
fetchData();
|
||||||
|
|
||||||
// Poll every 3 seconds for updates
|
|
||||||
const interval = setInterval(fetchData, 3000);
|
const interval = setInterval(fetchData, 3000);
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
@ -173,11 +101,9 @@ function App() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
console.log(`✓ Discharged patient ${patientId}`);
|
console.log(`✓ Discharged patient ${patientId}`);
|
||||||
// Remove from local state
|
|
||||||
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
|
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to discharge patient:', 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));
|
setPatients(prev => prev.filter(p => p.patient_id !== patientId));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -251,153 +177,351 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto p-6">
|
<div className="bg-white border-b shadow-sm">
|
||||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-4 py-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilter('all')}
|
onClick={() => setActiveTab('patients')}
|
||||||
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
|
className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${
|
||||||
filter === 'all'
|
activeTab === 'patients'
|
||||||
? 'bg-blue-600 text-white'
|
? 'bg-blue-100 text-blue-700 border-b-2 border-blue-600'
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
: 'text-gray-600 hover:text-gray-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
All Patients ({patients.length})
|
📋 Patients ({patients.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setFilter('EMERGENCY')}
|
onClick={() => setActiveTab('wristbands')}
|
||||||
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
|
className={`px-6 py-2 rounded-t-lg font-semibold transition-colors ${
|
||||||
filter === 'EMERGENCY'
|
activeTab === 'wristbands'
|
||||||
? 'bg-red-600 text-white'
|
? 'bg-purple-100 text-purple-700 border-b-2 border-purple-600'
|
||||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
: 'text-gray-600 hover:text-gray-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Emergency ({stats.tier_breakdown.EMERGENCY})
|
⌚ Wristbands ({wristbands.length})
|
||||||
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="max-w-7xl mx-auto p-6">
|
||||||
{filteredPatients.map((patient, index) => (
|
{activeTab === 'patients' ? (
|
||||||
<div
|
<>
|
||||||
key={patient.patient_id}
|
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||||
className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
<div className="flex gap-2">
|
||||||
>
|
<button
|
||||||
<div className="p-6">
|
onClick={() => setFilter('all')}
|
||||||
<div className="flex items-start justify-between mb-4">
|
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
|
||||||
<div className="flex items-start gap-4">
|
filter === 'all'
|
||||||
<div className="text-2xl font-bold text-gray-400 min-w-12 text-center pt-1">
|
? 'bg-blue-600 text-white'
|
||||||
#{index + 1}
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
</div>
|
}`}
|
||||||
<div>
|
>
|
||||||
<h3 className="text-xl font-bold text-gray-800 mb-1">{patient.name}</h3>
|
All Patients ({patients.length})
|
||||||
<div className="flex items-center gap-3 text-sm text-gray-600">
|
</button>
|
||||||
<span className="font-mono">{patient.patient_id}</span>
|
<button
|
||||||
<span>•</span>
|
onClick={() => setFilter('EMERGENCY')}
|
||||||
<span className="font-mono">{patient.band_id}</span>
|
className={`px-6 py-2 rounded-lg font-semibold transition-colors ${
|
||||||
</div>
|
filter === 'EMERGENCY'
|
||||||
{patient.symptoms && patient.symptoms.length > 0 && (
|
? 'bg-red-600 text-white'
|
||||||
<div className="flex flex-wrap gap-2 mt-2">
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
{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}
|
Emergency ({stats.tier_breakdown.EMERGENCY})
|
||||||
</span>
|
</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>
|
||||||
|
<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>
|
</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>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filteredPatients.length === 0 && (
|
{wristbands.length === 0 && (
|
||||||
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
|
<p className="text-gray-500 text-center py-8">No wristbands configured</p>
|
||||||
<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>
|
</div>
|
||||||
<p className="text-gray-500">Patients will appear here as they check in</p>
|
|
||||||
|
{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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
106155
|
117391
|
||||||
106168
|
117404
|
||||||
106176
|
117411
|
||||||
106211
|
117454
|
||||||
|
|||||||
Binary file not shown.
@ -1 +1 @@
|
|||||||
106155
|
117391
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
Port 5173 is in use, trying another one...
|
Port 5173 is in use, trying another one...
|
||||||
Port 5174 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/
|
➜ Local: http://localhost:5175/
|
||||||
➜ Network: use --host to expose
|
➜ 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 5174 is in use, trying another one...
|
||||||
Port 5175 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/
|
➜ Local: http://localhost:5176/
|
||||||
➜ Network: use --host to expose
|
➜ 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("=" * 80)
|
||||||
|
|
||||||
print("\nSimulated Wristbands:")
|
print("\nSimulated Wristbands:")
|
||||||
simulated = config.get_simulated_bands() or [] # Add "or []"
|
simulated = config.get_simulated_bands() or []
|
||||||
if simulated:
|
if simulated:
|
||||||
for band in simulated:
|
for band in simulated:
|
||||||
print(f" 🟢 {band['band_id']:20} | Profile: {band['profile']}")
|
print(f" 🟢 {band['band_id']:20} | Profile: {band['profile']}")
|
||||||
@ -175,7 +175,7 @@ def cli_inventory():
|
|||||||
print(" (none configured)")
|
print(" (none configured)")
|
||||||
|
|
||||||
print("\nReal Wristbands (Hardware):")
|
print("\nReal Wristbands (Hardware):")
|
||||||
real = config.get_real_bands() or [] # Add "or []"
|
real = config.get_real_bands() or []
|
||||||
if real:
|
if real:
|
||||||
for band in real:
|
for band in real:
|
||||||
print(f" 🔵 {band['band_id']:20} | BLE: {band['ble_address']}")
|
print(f" 🔵 {band['band_id']:20} | BLE: {band['ble_address']}")
|
||||||
|
|||||||
@ -1,19 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
VitalLink Main System Runner
|
VitalLink Main System Runner
|
||||||
Runs the complete system with real and/or simulated wristbands
|
Runs the complete system with real and/or simulated wristbands
|
||||||
Automatically assigns bands when patients check in via kiosk
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from wristband_manager import WristbandManager
|
import time
|
||||||
|
import struct
|
||||||
|
from wristband_manager import WristbandManager, WristbandType
|
||||||
from config_system import WristbandConfig
|
from config_system import WristbandConfig
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# MAIN SYSTEM
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
class VitalLinkSystem:
|
class VitalLinkSystem:
|
||||||
"""Main system orchestrator"""
|
"""Main system orchestrator"""
|
||||||
@ -26,38 +23,31 @@ class VitalLinkSystem:
|
|||||||
self.monitoring_task = None
|
self.monitoring_task = None
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
"""Initialize the system"""
|
|
||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
print("VitalLink System Initialization")
|
print("VitalLink System Initialization")
|
||||||
print("=" * 80 + "\n")
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
# Check backend availability
|
|
||||||
backend_ok = await self.check_backend()
|
backend_ok = await self.check_backend()
|
||||||
if not backend_ok:
|
if not backend_ok:
|
||||||
print("\n⚠️ Warning: Backend not running. System will wait for backend...")
|
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):
|
if self.config.get("auto_scan_ble", False):
|
||||||
timeout = self.config.get("scan_timeout", 10.0)
|
timeout = self.config.get("scan_timeout", 10.0)
|
||||||
await self.manager.scan_for_real_bands(timeout)
|
await self.manager.scan_for_real_bands(timeout)
|
||||||
|
|
||||||
# Load configured real wristbands
|
|
||||||
for band_config in self.config.get_real_bands() or []:
|
for band_config in self.config.get_real_bands() or []:
|
||||||
self.manager.add_real_band(
|
self.manager.add_real_band(
|
||||||
band_config["band_id"], band_config["ble_address"]
|
band_config["band_id"], band_config["ble_address"]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load configured simulated wristbands
|
|
||||||
for band_config in self.config.get_simulated_bands() or []:
|
for band_config in self.config.get_simulated_bands() or []:
|
||||||
self.manager.add_simulated_band(
|
self.manager.add_simulated_band(
|
||||||
band_config["band_id"], band_config.get("profile", "stable")
|
band_config["band_id"], band_config.get("profile", "stable")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Show inventory
|
|
||||||
self.manager.print_inventory()
|
self.manager.print_inventory()
|
||||||
|
|
||||||
async def check_backend(self):
|
async def check_backend(self):
|
||||||
"""Check if backend is running"""
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(
|
async with session.get(
|
||||||
@ -66,14 +56,90 @@ class VitalLinkSystem:
|
|||||||
if resp.status == 200:
|
if resp.status == 200:
|
||||||
print(f"✓ Backend is running at {self.backend_url}")
|
print(f"✓ Backend is running at {self.backend_url}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except:
|
||||||
print(f"❌ Backend not reachable at {self.backend_url}")
|
print(f"❌ Backend not reachable at {self.backend_url}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
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):
|
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...")
|
print("\n🔍 Monitoring for new patient check-ins...")
|
||||||
|
|
||||||
known_patients = set()
|
known_patients = set()
|
||||||
@ -89,11 +155,9 @@ class VitalLinkSystem:
|
|||||||
for patient in queue:
|
for patient in queue:
|
||||||
patient_id = patient["patient_id"]
|
patient_id = patient["patient_id"]
|
||||||
|
|
||||||
# New patient detected
|
|
||||||
if patient_id not in known_patients:
|
if patient_id not in known_patients:
|
||||||
known_patients.add(patient_id)
|
known_patients.add(patient_id)
|
||||||
|
|
||||||
# Check if already has a band assigned and monitoring
|
|
||||||
has_active_band = any(
|
has_active_band = any(
|
||||||
b.patient_id == patient_id
|
b.patient_id == patient_id
|
||||||
and b.band_id in self.manager.active_monitoring
|
and b.band_id in self.manager.active_monitoring
|
||||||
@ -105,7 +169,6 @@ class VitalLinkSystem:
|
|||||||
f"\n🆕 New patient detected: {patient_id} ({patient['name']})"
|
f"\n🆕 New patient detected: {patient_id} ({patient['name']})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to assign a band
|
|
||||||
band = self.manager.assign_band(
|
band = self.manager.assign_band(
|
||||||
patient_id, prefer_real=prefer_real
|
patient_id, prefer_real=prefer_real
|
||||||
)
|
)
|
||||||
@ -114,18 +177,15 @@ class VitalLinkSystem:
|
|||||||
print(
|
print(
|
||||||
f" ✓ Assigned {band.band_id} ({band.type.value})"
|
f" ✓ Assigned {band.band_id} ({band.type.value})"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Start monitoring
|
|
||||||
await self.manager.start_monitoring(
|
await self.manager.start_monitoring(
|
||||||
band.band_id
|
band.band_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# No bands available - create a new simulated one on the fly
|
|
||||||
print(
|
print(
|
||||||
f" ⚠️ No bands available, creating emergency simulated band..."
|
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(
|
band = self.manager.add_simulated_band(
|
||||||
emergency_band_id, "stable"
|
emergency_band_id, "stable"
|
||||||
)
|
)
|
||||||
@ -134,20 +194,15 @@ class VitalLinkSystem:
|
|||||||
print(
|
print(
|
||||||
f" ✓ Created and assigned {emergency_band_id}"
|
f" ✓ Created and assigned {emergency_band_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.manager.start_monitoring(
|
await self.manager.start_monitoring(
|
||||||
band.band_id
|
band.band_id
|
||||||
)
|
)
|
||||||
|
except:
|
||||||
except Exception as e:
|
|
||||||
# Silently continue if backend temporarily unavailable
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Check every 2 seconds
|
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""Run the main system"""
|
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
||||||
await self.initialize()
|
await self.initialize()
|
||||||
@ -164,15 +219,12 @@ class VitalLinkSystem:
|
|||||||
print("\nPress Ctrl+C to stop\n")
|
print("\nPress Ctrl+C to stop\n")
|
||||||
print("=" * 80 + "\n")
|
print("=" * 80 + "\n")
|
||||||
|
|
||||||
# Start monitoring for new patients
|
|
||||||
self.monitoring_task = asyncio.create_task(self.monitor_new_patients())
|
self.monitoring_task = asyncio.create_task(self.monitor_new_patients())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Keep running until interrupted
|
|
||||||
while self.running:
|
while self.running:
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
# Periodic status update
|
|
||||||
status = self.manager.get_status()
|
status = self.manager.get_status()
|
||||||
available = status["status_breakdown"].get("available", 0)
|
available = status["status_breakdown"].get("available", 0)
|
||||||
|
|
||||||
@ -183,19 +235,18 @@ class VitalLinkSystem:
|
|||||||
f"Sim: {status['simulated_bands']}"
|
f"Sim: {status['simulated_bands']}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self.report_inventory_to_backend()
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\n⚠️ Shutting down...")
|
print("\n\n⚠️ Shutting down...")
|
||||||
await self.shutdown()
|
await self.shutdown()
|
||||||
|
|
||||||
async def shutdown(self):
|
async def shutdown(self):
|
||||||
"""Clean shutdown"""
|
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
# Cancel monitoring task
|
|
||||||
if self.monitoring_task:
|
if self.monitoring_task:
|
||||||
self.monitoring_task.cancel()
|
self.monitoring_task.cancel()
|
||||||
|
|
||||||
# Stop all monitoring
|
|
||||||
print("Stopping all wristband monitoring...")
|
print("Stopping all wristband monitoring...")
|
||||||
for band_id in list(self.manager.active_monitoring.keys()):
|
for band_id in list(self.manager.active_monitoring.keys()):
|
||||||
await self.manager.stop_monitoring(band_id)
|
await self.manager.stop_monitoring(band_id)
|
||||||
@ -204,82 +255,10 @@ class VitalLinkSystem:
|
|||||||
self.manager.print_inventory()
|
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__":
|
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:
|
try:
|
||||||
if args.interactive:
|
system = VitalLinkSystem()
|
||||||
asyncio.run(interactive_mode())
|
asyncio.run(system.run())
|
||||||
else:
|
|
||||||
# Normal automatic mode
|
|
||||||
system = VitalLinkSystem()
|
|
||||||
asyncio.run(system.run())
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\nExiting...")
|
print("\n\nExiting...")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ Unified system for managing real and simulated wristbands
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
|
import random
|
||||||
import time
|
import time
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional, Union
|
||||||
@ -40,15 +41,15 @@ class WristbandType(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class WristbandStatus(Enum):
|
class WristbandStatus(Enum):
|
||||||
AVAILABLE = "available" # Charged and ready
|
AVAILABLE = "available"
|
||||||
ASSIGNED = "assigned" # Checked out to patient
|
ASSIGNED = "assigned"
|
||||||
IN_USE = "in_use" # Actively sending data
|
IN_USE = "in_use"
|
||||||
CHARGING = "charging" # On charger
|
CHARGING = "charging"
|
||||||
MAINTENANCE = "maintenance" # Needs attention
|
MAINTENANCE = "maintenance"
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# PACKET DECODER (Works with real hardware packets)
|
# PACKET DECODER
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
@ -61,13 +62,11 @@ class PacketDecoder:
|
|||||||
if len(data) != 16:
|
if len(data) != 16:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Verify checksum
|
|
||||||
checksum_calc = sum(data[0:14]) & 0xFF
|
checksum_calc = sum(data[0:14]) & 0xFF
|
||||||
if checksum_calc != data[14]:
|
if checksum_calc != data[14]:
|
||||||
print(f"⚠️ Checksum failed: expected {data[14]}, got {checksum_calc}")
|
print(f"⚠️ Checksum failed: expected {data[14]}, got {checksum_calc}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Unpack according to spec
|
|
||||||
(
|
(
|
||||||
ver,
|
ver,
|
||||||
seq,
|
seq,
|
||||||
@ -81,11 +80,10 @@ class PacketDecoder:
|
|||||||
rfu,
|
rfu,
|
||||||
) = PKT_STRUCT.unpack(data)
|
) = PKT_STRUCT.unpack(data)
|
||||||
|
|
||||||
# Determine tier from flags
|
|
||||||
tier = "NORMAL"
|
tier = "NORMAL"
|
||||||
if flags & (1 << 4): # EMERGENCY bit
|
if flags & (1 << 4):
|
||||||
tier = "EMERGENCY"
|
tier = "EMERGENCY"
|
||||||
elif flags & (1 << 3): # ALERT bit
|
elif flags & (1 << 3):
|
||||||
tier = "ALERT"
|
tier = "ALERT"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -108,7 +106,7 @@ class PacketDecoder:
|
|||||||
|
|
||||||
|
|
||||||
class BaseWristband(ABC):
|
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):
|
def __init__(self, band_id: str, wristband_type: WristbandType):
|
||||||
self.band_id = band_id
|
self.band_id = band_id
|
||||||
@ -120,22 +118,18 @@ class BaseWristband(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def start_monitoring(self):
|
async def start_monitoring(self):
|
||||||
"""Start receiving data from wristband"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def stop_monitoring(self):
|
async def stop_monitoring(self):
|
||||||
"""Stop receiving data"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def assign_to_patient(self, patient_id: str):
|
def assign_to_patient(self, patient_id: str):
|
||||||
"""Assign wristband to a patient"""
|
|
||||||
self.patient_id = patient_id
|
self.patient_id = patient_id
|
||||||
self.status = WristbandStatus.ASSIGNED
|
self.status = WristbandStatus.ASSIGNED
|
||||||
print(f"✓ {self.band_id} assigned to patient {patient_id}")
|
print(f"✓ {self.band_id} assigned to patient {patient_id}")
|
||||||
|
|
||||||
def release(self):
|
def release(self):
|
||||||
"""Release wristband (return to inventory)"""
|
|
||||||
self.patient_id = None
|
self.patient_id = None
|
||||||
self.status = WristbandStatus.AVAILABLE
|
self.status = WristbandStatus.AVAILABLE
|
||||||
self.packet_count = 0
|
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()
|
self.decoder = PacketDecoder()
|
||||||
|
|
||||||
async def start_monitoring(self):
|
async def start_monitoring(self):
|
||||||
"""Connect to real wristband via BLE and start receiving data"""
|
|
||||||
if not BLE_AVAILABLE:
|
if not BLE_AVAILABLE:
|
||||||
raise RuntimeError("Bleak library not available")
|
raise RuntimeError("Bleak library not available")
|
||||||
|
|
||||||
@ -171,7 +164,6 @@ class RealWristband(BaseWristband):
|
|||||||
await self.client.connect()
|
await self.client.connect()
|
||||||
print(f"✓ Connected to {self.band_id}")
|
print(f"✓ Connected to {self.band_id}")
|
||||||
|
|
||||||
# Subscribe to notifications
|
|
||||||
await self.client.start_notify(CHAR_UUID, self._notification_handler)
|
await self.client.start_notify(CHAR_UUID, self._notification_handler)
|
||||||
print(f"✓ Subscribed to notifications from {self.band_id}")
|
print(f"✓ Subscribed to notifications from {self.band_id}")
|
||||||
|
|
||||||
@ -180,102 +172,13 @@ class RealWristband(BaseWristband):
|
|||||||
self.status = WristbandStatus.MAINTENANCE
|
self.status = WristbandStatus.MAINTENANCE
|
||||||
|
|
||||||
def _notification_handler(self, sender, data: bytearray):
|
def _notification_handler(self, sender, data: bytearray):
|
||||||
"""Handle incoming BLE notifications"""
|
|
||||||
decoded = self.decoder.decode(bytes(data))
|
decoded = self.decoder.decode(bytes(data))
|
||||||
if decoded:
|
if decoded:
|
||||||
self.last_packet = decoded
|
self.last_packet = decoded
|
||||||
self.packet_count += 1
|
self.packet_count += 1
|
||||||
|
|
||||||
# Send to backend
|
|
||||||
asyncio.create_task(self._send_to_backend(decoded))
|
asyncio.create_task(self._send_to_backend(decoded))
|
||||||
|
|
||||||
async def _send_to_backend(self, decoded: dict):
|
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:
|
if not self.patient_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -301,7 +204,91 @@ class SimulatedWristband(BaseWristband):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
async def stop_monitoring(self):
|
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.running = False
|
||||||
self.status = WristbandStatus.AVAILABLE
|
self.status = WristbandStatus.AVAILABLE
|
||||||
print(f"✓ Stopped simulated wristband {self.band_id}")
|
print(f"✓ Stopped simulated wristband {self.band_id}")
|
||||||
@ -313,17 +300,15 @@ class SimulatedWristband(BaseWristband):
|
|||||||
|
|
||||||
|
|
||||||
class WristbandManager:
|
class WristbandManager:
|
||||||
"""Central manager for all wristbands (real and simulated)"""
|
"""Central manager for all wristbands"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.inventory: Dict[str, BaseWristband] = {}
|
self.inventory: Dict[str, BaseWristband] = {}
|
||||||
self.active_monitoring: Dict[str, asyncio.Task] = {}
|
self.active_monitoring: Dict[str, asyncio.Task] = {}
|
||||||
|
|
||||||
def add_simulated_band(self, band_id: str, profile: str = "stable"):
|
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-"):
|
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)
|
band = SimulatedWristband(band_id, profile)
|
||||||
self.inventory[band_id] = band
|
self.inventory[band_id] = band
|
||||||
@ -331,7 +316,6 @@ class WristbandManager:
|
|||||||
return band
|
return band
|
||||||
|
|
||||||
def add_real_band(self, band_id: str, ble_address: str):
|
def add_real_band(self, band_id: str, ble_address: str):
|
||||||
"""Add a real wristband to inventory"""
|
|
||||||
if not BLE_AVAILABLE:
|
if not BLE_AVAILABLE:
|
||||||
print("❌ Cannot add real band: Bleak not installed")
|
print("❌ Cannot add real band: Bleak not installed")
|
||||||
return None
|
return None
|
||||||
@ -342,7 +326,6 @@ class WristbandManager:
|
|||||||
return band
|
return band
|
||||||
|
|
||||||
async def scan_for_real_bands(self, timeout: float = 10.0):
|
async def scan_for_real_bands(self, timeout: float = 10.0):
|
||||||
"""Scan for real wristbands and add them to inventory"""
|
|
||||||
if not BLE_AVAILABLE:
|
if not BLE_AVAILABLE:
|
||||||
print("❌ BLE scanning not available: Install bleak")
|
print("❌ BLE scanning not available: Install bleak")
|
||||||
return []
|
return []
|
||||||
@ -352,7 +335,6 @@ class WristbandManager:
|
|||||||
|
|
||||||
found = []
|
found = []
|
||||||
for device in devices:
|
for device in devices:
|
||||||
# Check if device advertises our service UUID
|
|
||||||
uuids = device.metadata.get("uuids", [])
|
uuids = device.metadata.get("uuids", [])
|
||||||
if any(uuid.lower() == SERVICE_UUID.lower() for uuid in uuids):
|
if any(uuid.lower() == SERVICE_UUID.lower() for uuid in uuids):
|
||||||
band_id = (
|
band_id = (
|
||||||
@ -365,7 +347,6 @@ class WristbandManager:
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
def get_available_bands(self) -> List[BaseWristband]:
|
def get_available_bands(self) -> List[BaseWristband]:
|
||||||
"""Get list of available (not assigned) wristbands"""
|
|
||||||
return [
|
return [
|
||||||
b for b in self.inventory.values() if b.status == WristbandStatus.AVAILABLE
|
b for b in self.inventory.values() if b.status == WristbandStatus.AVAILABLE
|
||||||
]
|
]
|
||||||
@ -373,20 +354,15 @@ class WristbandManager:
|
|||||||
def assign_band(
|
def assign_band(
|
||||||
self, patient_id: str, prefer_real: bool = False
|
self, patient_id: str, prefer_real: bool = False
|
||||||
) -> Optional[BaseWristband]:
|
) -> Optional[BaseWristband]:
|
||||||
"""Assign an available band to a patient"""
|
|
||||||
available = self.get_available_bands()
|
available = self.get_available_bands()
|
||||||
|
|
||||||
if not available:
|
if not available:
|
||||||
print("❌ No wristbands available")
|
print("❌ No wristbands available")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Prefer real bands if requested and available
|
|
||||||
if prefer_real:
|
if prefer_real:
|
||||||
real_bands = [b for b in available if b.type == WristbandType.REAL]
|
real_bands = [b for b in available if b.type == WristbandType.REAL]
|
||||||
if real_bands:
|
band = real_bands[0] if real_bands else available[0]
|
||||||
band = real_bands[0]
|
|
||||||
else:
|
|
||||||
band = available[0]
|
|
||||||
else:
|
else:
|
||||||
band = available[0]
|
band = available[0]
|
||||||
|
|
||||||
@ -394,7 +370,6 @@ class WristbandManager:
|
|||||||
return band
|
return band
|
||||||
|
|
||||||
async def start_monitoring(self, band_id: str):
|
async def start_monitoring(self, band_id: str):
|
||||||
"""Start monitoring a wristband"""
|
|
||||||
if band_id not in self.inventory:
|
if band_id not in self.inventory:
|
||||||
print(f"❌ Band {band_id} not in inventory")
|
print(f"❌ Band {band_id} not in inventory")
|
||||||
return
|
return
|
||||||
@ -404,12 +379,10 @@ class WristbandManager:
|
|||||||
print(f"⚠️ {band_id} already being monitored")
|
print(f"⚠️ {band_id} already being monitored")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create monitoring task
|
|
||||||
task = asyncio.create_task(band.start_monitoring())
|
task = asyncio.create_task(band.start_monitoring())
|
||||||
self.active_monitoring[band_id] = task
|
self.active_monitoring[band_id] = task
|
||||||
|
|
||||||
async def stop_monitoring(self, band_id: str):
|
async def stop_monitoring(self, band_id: str):
|
||||||
"""Stop monitoring a wristband"""
|
|
||||||
if band_id in self.active_monitoring:
|
if band_id in self.active_monitoring:
|
||||||
task = self.active_monitoring[band_id]
|
task = self.active_monitoring[band_id]
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@ -419,13 +392,11 @@ class WristbandManager:
|
|||||||
await self.inventory[band_id].stop_monitoring()
|
await self.inventory[band_id].stop_monitoring()
|
||||||
|
|
||||||
async def release_band(self, band_id: str):
|
async def release_band(self, band_id: str):
|
||||||
"""Release band back to inventory"""
|
|
||||||
await self.stop_monitoring(band_id)
|
await self.stop_monitoring(band_id)
|
||||||
if band_id in self.inventory:
|
if band_id in self.inventory:
|
||||||
self.inventory[band_id].release()
|
self.inventory[band_id].release()
|
||||||
|
|
||||||
def get_status(self) -> dict:
|
def get_status(self) -> dict:
|
||||||
"""Get overall status"""
|
|
||||||
status_counts = {}
|
status_counts = {}
|
||||||
for status in WristbandStatus:
|
for status in WristbandStatus:
|
||||||
status_counts[status.value] = sum(
|
status_counts[status.value] = sum(
|
||||||
@ -445,7 +416,6 @@ class WristbandManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def print_inventory(self):
|
def print_inventory(self):
|
||||||
"""Print current inventory"""
|
|
||||||
print("\n" + "=" * 80)
|
print("\n" + "=" * 80)
|
||||||
print("WRISTBAND INVENTORY")
|
print("WRISTBAND INVENTORY")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
@ -471,57 +441,3 @@ class WristbandManager:
|
|||||||
f"Active: {status['active_monitoring']}"
|
f"Active: {status['active_monitoring']}"
|
||||||
)
|
)
|
||||||
print("=" * 80 + "\n")
|
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