diff --git a/vitallink/backend/server.py b/vitallink/backend/server.py index 2ddbf0e..0751b7b 100644 --- a/vitallink/backend/server.py +++ b/vitallink/backend/server.py @@ -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) diff --git a/vitallink/frontend/dashboard/src/App.jsx b/vitallink/frontend/dashboard/src/App.jsx index 5d9d4c3..56218c1 100644 --- a/vitallink/frontend/dashboard/src/App.jsx +++ b/vitallink/frontend/dashboard/src/App.jsx @@ -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() { -
-
-
+
+
+
- -
+
-
- {filteredPatients.map((patient, index) => ( -
-
-
-
-
- #{index + 1} -
-
-

{patient.name}

-
- {patient.patient_id} - - {patient.band_id} -
- {patient.symptoms && patient.symptoms.length > 0 && ( -
- {patient.symptoms.map(symptom => ( - - {symptom} - - ))} +
+ {activeTab === 'patients' ? ( + <> +
+
+ + + + +
+
+ +
+ {filteredPatients.map((patient, index) => ( +
+
+
+
+
+ #{index + 1}
+
+

{patient.name}

+
+ {patient.patient_id} + + {patient.band_id} +
+ {patient.symptoms && patient.symptoms.length > 0 && ( +
+ {patient.symptoms.map(symptom => ( + + {symptom} + + ))} +
+ )} +
+
+ +
+
+ {getTierIcon(patient.tier)} + {patient.tier} +
+ +
+
+ +
+
+
+ + Heart Rate +
+

+ {patient.last_hr} +

+

bpm

+
+ +
+
+ + SpO₂ +
+

+ {patient.last_spo2} +

+

%

+
+ +
+
+ + Temperature +
+

+ {patient.last_temp.toFixed(1)} +

+

°C

+
+ +
+
+ + Wait Time +
+

+ {patient.wait_time_minutes} +

+

minutes

+
+
+
+
+ ))} +
+ + {filteredPatients.length === 0 && ( +
+ +

No patients in this category

+

Patients will appear here as they check in

+
+ )} + + ) : ( +
+
+

Wristband Inventory

+ +
+ {wristbands.map(band => ( +
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' + }`} + > +
+ + {band.type === 'real' ? '🔵' : '🟢'} {band.band_id} + + + {band.status.toUpperCase().replace('_', ' ')} + +
+ + {band.patient_id && ( +
+ Patient: {band.patient_id} +
+ )} + +
+ Packets: {band.packet_count} + {band.is_monitoring && ( + ● LIVE )}
- -
-
- {getTierIcon(patient.tier)} - {patient.tier} -
- -
-
- -
-
-
- - Heart Rate -
-

- {patient.last_hr} -

-

bpm

-
- -
-
- - SpO₂ -
-

- {patient.last_spo2} -

-

%

-
- -
-
- - Temperature -
-

- {patient.last_temp.toFixed(1)} -

-

°C

-
- -
-
- - Wait Time -
-

- {patient.wait_time_minutes} -

-

minutes

-
-
+ ))}
-
- ))} -
- {filteredPatients.length === 0 && ( -
- -

No patients in this category

-

Patients will appear here as they check in

+ {wristbands.length === 0 && ( +

No wristbands configured

+ )} +
+ + {selectedWristband && selectedWristband.last_raw_packet && ( +
+
+

+ Packet Details: {selectedWristband.band_id} + {selectedWristband.type === 'simulated' && ( + MOCK + )} +

+ +
+ +
+

Raw Packet (16 bytes, Hex):

+
+ {selectedWristband.last_raw_packet.hex.toUpperCase().match(/.{1,2}/g).join(' ')} +
+

+ Format: [ver][seq][timestamp][flags][hr][spo2][temp_x100][activity_x100][checksum][rfu] +

+
+ + {selectedWristband.last_raw_packet.decoded && ( + <> +
+

Decoded Fields:

+
+
+
Version
+
0x{selectedWristband.last_raw_packet.decoded.version.toString(16).padStart(2, '0')}
+
+
+
Sequence #
+
{selectedWristband.last_raw_packet.decoded.sequence}
+
+
+
Timestamp (ms since boot)
+
{selectedWristband.last_raw_packet.decoded.timestamp_ms.toLocaleString()}
+
+
+
Heart Rate
+
{selectedWristband.last_raw_packet.decoded.hr_bpm}
+
bpm
+
+
+
SpO₂
+
{selectedWristband.last_raw_packet.decoded.spo2}
+
%
+
+
+
Temperature
+
{selectedWristband.last_raw_packet.decoded.temperature_c.toFixed(2)}
+
°C
+
+
+
Activity
+
{selectedWristband.last_raw_packet.decoded.activity.toFixed(2)}
+
RMS
+
+
+
Checksum
+
{selectedWristband.last_raw_packet.decoded.checksum}
+
+
+
Flags (raw)
+
0x{selectedWristband.last_raw_packet.decoded.flags.raw.toString(16).padStart(2, '0')}
+
+
+
+ + {selectedWristband.last_raw_packet.decoded.flags && ( +
+

Status Flags:

+
+ {selectedWristband.last_raw_packet.decoded.flags.emergency && ( + + 🚨 Bit 4: Emergency + + )} + {selectedWristband.last_raw_packet.decoded.flags.alert && ( + + ⚠️ Bit 3: Alert + + )} + {selectedWristband.last_raw_packet.decoded.flags.sensor_fault && ( + + ⚙️ Bit 2: Sensor Fault + + )} + {selectedWristband.last_raw_packet.decoded.flags.low_battery && ( + + 🔋 Bit 1: Low Battery + + )} + {selectedWristband.last_raw_packet.decoded.flags.motion_artifact && ( + + 👋 Bit 0: Motion Artifact + + )} + {!Object.values(selectedWristband.last_raw_packet.decoded.flags).some(v => v === true) && ( + + ✓ No flags set (all normal) + + )} +
+
+ )} + + )} +
+ )}
)}
diff --git a/vitallink/logs/all_pids.txt b/vitallink/logs/all_pids.txt index 514aaf9..210ed40 100644 --- a/vitallink/logs/all_pids.txt +++ b/vitallink/logs/all_pids.txt @@ -1,4 +1,4 @@ -106155 -106168 -106176 -106211 +117391 +117404 +117411 +117454 diff --git a/vitallink/logs/backend.log b/vitallink/logs/backend.log index c2d4246..5763bbb 100644 Binary files a/vitallink/logs/backend.log and b/vitallink/logs/backend.log differ diff --git a/vitallink/logs/backend.pid b/vitallink/logs/backend.pid index daca52c..d4ae7b0 100644 --- a/vitallink/logs/backend.pid +++ b/vitallink/logs/backend.pid @@ -1 +1 @@ -106155 +117391 diff --git a/vitallink/logs/dashboard.log b/vitallink/logs/dashboard.log index 894996b..a8335c6 100644 --- a/vitallink/logs/dashboard.log +++ b/vitallink/logs/dashboard.log @@ -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 diff --git a/vitallink/logs/dashboard.pid b/vitallink/logs/dashboard.pid index b823426..726a08b 100644 --- a/vitallink/logs/dashboard.pid +++ b/vitallink/logs/dashboard.pid @@ -1 +1 @@ -106176 +117411 diff --git a/vitallink/logs/kiosk.log b/vitallink/logs/kiosk.log index 2085fbe..2b527e7 100644 --- a/vitallink/logs/kiosk.log +++ b/vitallink/logs/kiosk.log @@ -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 diff --git a/vitallink/logs/kiosk.pid b/vitallink/logs/kiosk.pid index 72b47a8..4c75e94 100644 --- a/vitallink/logs/kiosk.pid +++ b/vitallink/logs/kiosk.pid @@ -1 +1 @@ -106211 +117454 diff --git a/vitallink/logs/wristbands.pid b/vitallink/logs/wristbands.pid index 06dc9c8..eb4532d 100644 --- a/vitallink/logs/wristbands.pid +++ b/vitallink/logs/wristbands.pid @@ -1 +1 @@ -106168 +117404 diff --git a/vitallink/simulator/__pycache__/config_system.cpython-39.pyc b/vitallink/simulator/__pycache__/config_system.cpython-39.pyc index 5090c61..66fca50 100644 Binary files a/vitallink/simulator/__pycache__/config_system.cpython-39.pyc and b/vitallink/simulator/__pycache__/config_system.cpython-39.pyc differ diff --git a/vitallink/simulator/__pycache__/wristband_manager.cpython-39.pyc b/vitallink/simulator/__pycache__/wristband_manager.cpython-39.pyc index 0d0b43a..7835e18 100644 Binary files a/vitallink/simulator/__pycache__/wristband_manager.cpython-39.pyc and b/vitallink/simulator/__pycache__/wristband_manager.cpython-39.pyc differ diff --git a/vitallink/simulator/config_system.py b/vitallink/simulator/config_system.py index 6597eb4..5c54850 100644 --- a/vitallink/simulator/config_system.py +++ b/vitallink/simulator/config_system.py @@ -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']}") diff --git a/vitallink/simulator/main_runner.py b/vitallink/simulator/main_runner.py index e0486eb..a298c38 100644 --- a/vitallink/simulator/main_runner.py +++ b/vitallink/simulator/main_runner.py @@ -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(" 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())