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

This commit is contained in:
2025-10-18 18:06:37 -04:00
parent 8d04eb5594
commit e6aba90ee5
15 changed files with 567 additions and 575 deletions
+2 -2
View File
@@ -167,7 +167,7 @@ def cli_inventory():
print("=" * 80)
print("\nSimulated Wristbands:")
simulated = config.get_simulated_bands() or [] # Add "or []"
simulated = config.get_simulated_bands() or []
if simulated:
for band in simulated:
print(f" 🟢 {band['band_id']:20} | Profile: {band['profile']}")
@@ -175,7 +175,7 @@ def cli_inventory():
print(" (none configured)")
print("\nReal Wristbands (Hardware):")
real = config.get_real_bands() or [] # Add "or []"
real = config.get_real_bands() or []
if real:
for band in real:
print(f" 🔵 {band['band_id']:20} | BLE: {band['ble_address']}")
+87 -108
View File
@@ -1,19 +1,16 @@
"""
VitalLink Main System Runner
Runs the complete system with real and/or simulated wristbands
Automatically assigns bands when patients check in via kiosk
"""
import asyncio
import aiohttp
from wristband_manager import WristbandManager
import time
import struct
from wristband_manager import WristbandManager, WristbandType
from config_system import WristbandConfig
import sys
# ============================================================================
# MAIN SYSTEM
# ============================================================================
class VitalLinkSystem:
"""Main system orchestrator"""
@@ -26,38 +23,31 @@ class VitalLinkSystem:
self.monitoring_task = None
async def initialize(self):
"""Initialize the system"""
print("\n" + "=" * 80)
print("VitalLink System Initialization")
print("=" * 80 + "\n")
# Check backend availability
backend_ok = await self.check_backend()
if not backend_ok:
print("\n⚠️ Warning: Backend not running. System will wait for backend...")
# Scan for real wristbands if configured
if self.config.get("auto_scan_ble", False):
timeout = self.config.get("scan_timeout", 10.0)
await self.manager.scan_for_real_bands(timeout)
# Load configured real wristbands
for band_config in self.config.get_real_bands() or []:
self.manager.add_real_band(
band_config["band_id"], band_config["ble_address"]
)
# Load configured simulated wristbands
for band_config in self.config.get_simulated_bands() or []:
self.manager.add_simulated_band(
band_config["band_id"], band_config.get("profile", "stable")
)
# Show inventory
self.manager.print_inventory()
async def check_backend(self):
"""Check if backend is running"""
try:
async with aiohttp.ClientSession() as session:
async with session.get(
@@ -66,14 +56,90 @@ class VitalLinkSystem:
if resp.status == 200:
print(f"✓ Backend is running at {self.backend_url}")
return True
except Exception as e:
except:
print(f"❌ Backend not reachable at {self.backend_url}")
return False
return False
def _decode_packet_for_display(self, packet: bytes) -> dict:
if len(packet) != 16:
return {}
PKT_STRUCT = struct.Struct("<B H I B B B h H B B")
(
ver,
seq,
ts_ms,
flags,
hr_bpm,
spo2,
skin_c_x100,
act_rms_x100,
checksum,
rfu,
) = PKT_STRUCT.unpack(packet)
return {
"version": ver,
"sequence": seq,
"timestamp_ms": ts_ms,
"flags": {
"raw": flags,
"motion_artifact": bool(flags & (1 << 0)),
"low_battery": bool(flags & (1 << 1)),
"sensor_fault": bool(flags & (1 << 2)),
"alert": bool(flags & (1 << 3)),
"emergency": bool(flags & (1 << 4)),
},
"hr_bpm": hr_bpm,
"spo2": spo2,
"temperature_c": skin_c_x100 / 100.0,
"activity": act_rms_x100 / 100.0,
"checksum": f"0x{checksum:02X}",
"reserved": rfu,
}
async def report_inventory_to_backend(self):
try:
inventory = {"timestamp": time.time(), "wristbands": []}
for band_id, band in self.manager.inventory.items():
band_info = {
"band_id": band.band_id,
"type": band.type.value,
"status": band.status.value,
"patient_id": band.patient_id,
"packet_count": band.packet_count,
"is_monitoring": band_id in self.manager.active_monitoring,
"last_packet": band.last_packet,
}
if band.type == WristbandType.SIMULATED and hasattr(
band, "last_raw_packet"
):
if band.last_raw_packet:
band_info["last_raw_packet"] = {
"hex": band.last_raw_packet.hex(),
"bytes": list(band.last_raw_packet),
"decoded": self._decode_packet_for_display(
band.last_raw_packet
),
}
if hasattr(band, "packet_history"):
band_info["recent_packets"] = band.packet_history[-5:]
inventory["wristbands"].append(band_info)
async with aiohttp.ClientSession() as session:
await session.post(
f"{self.backend_url}/api/wristband-details", json=inventory
)
except:
pass
async def monitor_new_patients(self):
"""Monitor backend for new patient check-ins and auto-assign bands"""
print("\n🔍 Monitoring for new patient check-ins...")
known_patients = set()
@@ -89,11 +155,9 @@ class VitalLinkSystem:
for patient in queue:
patient_id = patient["patient_id"]
# New patient detected
if patient_id not in known_patients:
known_patients.add(patient_id)
# Check if already has a band assigned and monitoring
has_active_band = any(
b.patient_id == patient_id
and b.band_id in self.manager.active_monitoring
@@ -105,7 +169,6 @@ class VitalLinkSystem:
f"\n🆕 New patient detected: {patient_id} ({patient['name']})"
)
# Try to assign a band
band = self.manager.assign_band(
patient_id, prefer_real=prefer_real
)
@@ -114,18 +177,15 @@ class VitalLinkSystem:
print(
f" ✓ Assigned {band.band_id} ({band.type.value})"
)
# Start monitoring
await self.manager.start_monitoring(
band.band_id
)
else:
# No bands available - create a new simulated one on the fly
print(
f" ⚠️ No bands available, creating emergency simulated band..."
)
emergency_band_id = f"VitalLink-EMRG{len(self.manager.inventory):02d}"
emergency_band_id = f"MOCK-EMRG{len(self.manager.inventory):02d}"
band = self.manager.add_simulated_band(
emergency_band_id, "stable"
)
@@ -134,20 +194,15 @@ class VitalLinkSystem:
print(
f" ✓ Created and assigned {emergency_band_id}"
)
await self.manager.start_monitoring(
band.band_id
)
except Exception as e:
# Silently continue if backend temporarily unavailable
except:
pass
# Check every 2 seconds
await asyncio.sleep(2)
async def run(self):
"""Run the main system"""
self.running = True
await self.initialize()
@@ -164,15 +219,12 @@ class VitalLinkSystem:
print("\nPress Ctrl+C to stop\n")
print("=" * 80 + "\n")
# Start monitoring for new patients
self.monitoring_task = asyncio.create_task(self.monitor_new_patients())
try:
# Keep running until interrupted
while self.running:
await asyncio.sleep(10)
# Periodic status update
status = self.manager.get_status()
available = status["status_breakdown"].get("available", 0)
@@ -183,19 +235,18 @@ class VitalLinkSystem:
f"Sim: {status['simulated_bands']}"
)
await self.report_inventory_to_backend()
except KeyboardInterrupt:
print("\n\n⚠️ Shutting down...")
await self.shutdown()
async def shutdown(self):
"""Clean shutdown"""
self.running = False
# Cancel monitoring task
if self.monitoring_task:
self.monitoring_task.cancel()
# Stop all monitoring
print("Stopping all wristband monitoring...")
for band_id in list(self.manager.active_monitoring.keys()):
await self.manager.stop_monitoring(band_id)
@@ -204,82 +255,10 @@ class VitalLinkSystem:
self.manager.print_inventory()
# ============================================================================
# CLI MODES
# ============================================================================
async def interactive_mode():
"""Interactive mode with menu"""
system = VitalLinkSystem()
await system.initialize()
while True:
print("\n" + "=" * 80)
print("VITALLINK INTERACTIVE MODE")
print("=" * 80)
print("\n1. Show inventory")
print("2. Scan for real wristbands")
print("3. Assign band to patient")
print("4. Release band")
print("5. Start auto-monitoring mode")
print("6. Exit")
choice = input("\nSelect option: ")
if choice == "1":
system.manager.print_inventory()
elif choice == "2":
await system.manager.scan_for_real_bands(timeout=10.0)
elif choice == "3":
patient_id = input("Patient ID: ")
prefer_real = input("Prefer real band? (y/n): ").lower() == "y"
band = system.manager.assign_band(patient_id, prefer_real=prefer_real)
if band:
await system.manager.start_monitoring(band.band_id)
elif choice == "4":
band_id = input("Band ID to release: ")
await system.manager.release_band(band_id)
elif choice == "5":
await system.run()
break
elif choice == "6":
print("Goodbye!")
break
# ============================================================================
# MAIN ENTRY POINT
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="VitalLink System")
parser.add_argument(
"--interactive", "-i", action="store_true", help="Run in interactive mode"
)
parser.add_argument(
"--config",
"-c",
default="wristband_config.yaml",
help="Configuration file path",
)
args = parser.parse_args()
try:
if args.interactive:
asyncio.run(interactive_mode())
else:
# Normal automatic mode
system = VitalLinkSystem()
asyncio.run(system.run())
system = VitalLinkSystem()
asyncio.run(system.run())
except KeyboardInterrupt:
print("\n\nExiting...")
sys.exit(0)
+99 -183
View File
@@ -5,6 +5,7 @@ Unified system for managing real and simulated wristbands
import asyncio
import struct
import random
import time
import aiohttp
from typing import Dict, List, Optional, Union
@@ -40,15 +41,15 @@ class WristbandType(Enum):
class WristbandStatus(Enum):
AVAILABLE = "available" # Charged and ready
ASSIGNED = "assigned" # Checked out to patient
IN_USE = "in_use" # Actively sending data
CHARGING = "charging" # On charger
MAINTENANCE = "maintenance" # Needs attention
AVAILABLE = "available"
ASSIGNED = "assigned"
IN_USE = "in_use"
CHARGING = "charging"
MAINTENANCE = "maintenance"
# ============================================================================
# PACKET DECODER (Works with real hardware packets)
# PACKET DECODER
# ============================================================================
@@ -61,13 +62,11 @@ class PacketDecoder:
if len(data) != 16:
return None
# Verify checksum
checksum_calc = sum(data[0:14]) & 0xFF
if checksum_calc != data[14]:
print(f"⚠️ Checksum failed: expected {data[14]}, got {checksum_calc}")
return None
# Unpack according to spec
(
ver,
seq,
@@ -81,11 +80,10 @@ class PacketDecoder:
rfu,
) = PKT_STRUCT.unpack(data)
# Determine tier from flags
tier = "NORMAL"
if flags & (1 << 4): # EMERGENCY bit
if flags & (1 << 4):
tier = "EMERGENCY"
elif flags & (1 << 3): # ALERT bit
elif flags & (1 << 3):
tier = "ALERT"
return {
@@ -108,7 +106,7 @@ class PacketDecoder:
class BaseWristband(ABC):
"""Abstract base class for all wristbands (real and simulated)"""
"""Abstract base class for all wristbands"""
def __init__(self, band_id: str, wristband_type: WristbandType):
self.band_id = band_id
@@ -120,22 +118,18 @@ class BaseWristband(ABC):
@abstractmethod
async def start_monitoring(self):
"""Start receiving data from wristband"""
pass
@abstractmethod
async def stop_monitoring(self):
"""Stop receiving data"""
pass
def assign_to_patient(self, patient_id: str):
"""Assign wristband to a patient"""
self.patient_id = patient_id
self.status = WristbandStatus.ASSIGNED
print(f"{self.band_id} assigned to patient {patient_id}")
def release(self):
"""Release wristband (return to inventory)"""
self.patient_id = None
self.status = WristbandStatus.AVAILABLE
self.packet_count = 0
@@ -143,7 +137,7 @@ class BaseWristband(ABC):
# ============================================================================
# REAL WRISTBAND (Hardware BLE)
# REAL WRISTBAND
# ============================================================================
@@ -157,7 +151,6 @@ class RealWristband(BaseWristband):
self.decoder = PacketDecoder()
async def start_monitoring(self):
"""Connect to real wristband via BLE and start receiving data"""
if not BLE_AVAILABLE:
raise RuntimeError("Bleak library not available")
@@ -171,7 +164,6 @@ class RealWristband(BaseWristband):
await self.client.connect()
print(f"✓ Connected to {self.band_id}")
# Subscribe to notifications
await self.client.start_notify(CHAR_UUID, self._notification_handler)
print(f"✓ Subscribed to notifications from {self.band_id}")
@@ -180,102 +172,13 @@ class RealWristband(BaseWristband):
self.status = WristbandStatus.MAINTENANCE
def _notification_handler(self, sender, data: bytearray):
"""Handle incoming BLE notifications"""
decoded = self.decoder.decode(bytes(data))
if decoded:
self.last_packet = decoded
self.packet_count += 1
# Send to backend
asyncio.create_task(self._send_to_backend(decoded))
async def _send_to_backend(self, decoded: dict):
"""Send decoded data to backend"""
if not self.patient_id:
return
payload = {
"band_id": self.band_id,
"patient_id": self.patient_id,
"timestamp": time.time(),
"ver": decoded["ver"],
"seq": decoded["seq"],
"ts_ms": decoded["ts_ms"],
"tier": decoded["tier"],
"flags": [], # Convert flags to list if needed
"hr_bpm": decoded["hr_bpm"],
"spo2": decoded["spo2"],
"temp_c": decoded["temp_c"],
"activity": decoded["activity"],
}
try:
async with aiohttp.ClientSession() as session:
await session.post(f"{BACKEND_URL}/api/vitals", json=payload)
except:
pass # Silently fail if backend unavailable
async def stop_monitoring(self):
"""Disconnect from wristband"""
if self.client and self.client.is_connected:
await self.client.stop_notify(CHAR_UUID)
await self.client.disconnect()
print(f"✓ Disconnected from {self.band_id}")
self.status = WristbandStatus.AVAILABLE
# ============================================================================
# SIMULATED WRISTBAND (For testing without hardware)
# ============================================================================
class SimulatedWristband(BaseWristband):
"""Simulated wristband for testing"""
def __init__(self, band_id: str, profile: str = "stable"):
super().__init__(band_id, WristbandType.SIMULATED)
self.profile = profile
self.seq = 0
self.running = False
# Import simulator profiles
from wristband_simulator import PATIENT_PROFILES, WristbandSimulator
self.simulator = WristbandSimulator(
band_id, PATIENT_PROFILES.get(profile, PATIENT_PROFILES["stable"]), None
)
async def start_monitoring(self):
"""Start simulated data generation"""
self.status = WristbandStatus.IN_USE
self.running = True
self.simulator.patient_id = self.patient_id
print(f"🟢 Starting simulated wristband {self.band_id} ({self.profile})")
while self.running:
# Generate packet
packet = self.simulator.generate_packet()
# Decode it
decoder = PacketDecoder()
decoded = decoder.decode(packet)
if decoded:
self.last_packet = decoded
self.packet_count += 1
# Send to backend
await self._send_to_backend(decoded)
# Wait based on tier
tier = self.simulator.tier
interval = 1.0 if tier in ["ALERT", "EMERGENCY"] else 60.0
await asyncio.sleep(interval)
async def _send_to_backend(self, decoded: dict):
"""Send to backend"""
if not self.patient_id:
return
@@ -301,7 +204,91 @@ class SimulatedWristband(BaseWristband):
pass
async def stop_monitoring(self):
"""Stop simulation"""
if self.client and self.client.is_connected:
await self.client.stop_notify(CHAR_UUID)
await self.client.disconnect()
print(f"✓ Disconnected from {self.band_id}")
self.status = WristbandStatus.AVAILABLE
# ============================================================================
# SIMULATED WRISTBAND
# ============================================================================
class SimulatedWristband(BaseWristband):
"""Simulated wristband for testing"""
def __init__(self, band_id: str, profile: str = "stable"):
super().__init__(band_id, WristbandType.SIMULATED)
self.profile = profile
self.seq = 0
self.running = False
self.last_raw_packet = None
self.packet_history = []
from wristband_simulator import PATIENT_PROFILES, WristbandSimulator
self.simulator = WristbandSimulator(
band_id, PATIENT_PROFILES.get(profile, PATIENT_PROFILES["stable"]), None
)
async def start_monitoring(self):
self.status = WristbandStatus.IN_USE
self.running = True
self.simulator.patient_id = self.patient_id
print(f"🟢 Starting simulated wristband {self.band_id} ({self.profile})")
while self.running:
packet = self.simulator.generate_packet()
self.last_raw_packet = packet
self.packet_history.append(
{"timestamp": time.time(), "raw": packet.hex(), "bytes": list(packet)}
)
if len(self.packet_history) > 50:
self.packet_history = self.packet_history[-50:]
decoder = PacketDecoder()
decoded = decoder.decode(packet)
if decoded:
self.last_packet = decoded
self.packet_count += 1
await self._send_to_backend(decoded)
tier = self.simulator.tier
interval = 1.0 if tier in ["ALERT", "EMERGENCY"] else 60.0
await asyncio.sleep(interval)
async def _send_to_backend(self, decoded: dict):
if not self.patient_id:
return
payload = {
"band_id": self.band_id,
"patient_id": self.patient_id,
"timestamp": time.time(),
"ver": decoded["ver"],
"seq": decoded["seq"],
"ts_ms": decoded["ts_ms"],
"tier": decoded["tier"],
"flags": [],
"hr_bpm": decoded["hr_bpm"],
"spo2": decoded["spo2"],
"temp_c": decoded["temp_c"],
"activity": decoded["activity"],
}
try:
async with aiohttp.ClientSession() as session:
await session.post(f"{BACKEND_URL}/api/vitals", json=payload)
except:
pass
async def stop_monitoring(self):
self.running = False
self.status = WristbandStatus.AVAILABLE
print(f"✓ Stopped simulated wristband {self.band_id}")
@@ -313,17 +300,15 @@ class SimulatedWristband(BaseWristband):
class WristbandManager:
"""Central manager for all wristbands (real and simulated)"""
"""Central manager for all wristbands"""
def __init__(self):
self.inventory: Dict[str, BaseWristband] = {}
self.active_monitoring: Dict[str, asyncio.Task] = {}
def add_simulated_band(self, band_id: str, profile: str = "stable"):
"""Add a simulated wristband to inventory"""
# Change naming to use MOCK prefix if not already present
if not band_id.startswith("MOCK-"):
band_id = f"MOCK-{band_id.replace('VitalLink-', '')}"
band_id = f"MOCK-{band_id.replace('VitalLink-', '').replace('MOCK-', '')}"
band = SimulatedWristband(band_id, profile)
self.inventory[band_id] = band
@@ -331,7 +316,6 @@ class WristbandManager:
return band
def add_real_band(self, band_id: str, ble_address: str):
"""Add a real wristband to inventory"""
if not BLE_AVAILABLE:
print("❌ Cannot add real band: Bleak not installed")
return None
@@ -342,7 +326,6 @@ class WristbandManager:
return band
async def scan_for_real_bands(self, timeout: float = 10.0):
"""Scan for real wristbands and add them to inventory"""
if not BLE_AVAILABLE:
print("❌ BLE scanning not available: Install bleak")
return []
@@ -352,7 +335,6 @@ class WristbandManager:
found = []
for device in devices:
# Check if device advertises our service UUID
uuids = device.metadata.get("uuids", [])
if any(uuid.lower() == SERVICE_UUID.lower() for uuid in uuids):
band_id = (
@@ -365,7 +347,6 @@ class WristbandManager:
return found
def get_available_bands(self) -> List[BaseWristband]:
"""Get list of available (not assigned) wristbands"""
return [
b for b in self.inventory.values() if b.status == WristbandStatus.AVAILABLE
]
@@ -373,20 +354,15 @@ class WristbandManager:
def assign_band(
self, patient_id: str, prefer_real: bool = False
) -> Optional[BaseWristband]:
"""Assign an available band to a patient"""
available = self.get_available_bands()
if not available:
print("❌ No wristbands available")
return None
# Prefer real bands if requested and available
if prefer_real:
real_bands = [b for b in available if b.type == WristbandType.REAL]
if real_bands:
band = real_bands[0]
else:
band = available[0]
band = real_bands[0] if real_bands else available[0]
else:
band = available[0]
@@ -394,7 +370,6 @@ class WristbandManager:
return band
async def start_monitoring(self, band_id: str):
"""Start monitoring a wristband"""
if band_id not in self.inventory:
print(f"❌ Band {band_id} not in inventory")
return
@@ -404,12 +379,10 @@ class WristbandManager:
print(f"⚠️ {band_id} already being monitored")
return
# Create monitoring task
task = asyncio.create_task(band.start_monitoring())
self.active_monitoring[band_id] = task
async def stop_monitoring(self, band_id: str):
"""Stop monitoring a wristband"""
if band_id in self.active_monitoring:
task = self.active_monitoring[band_id]
task.cancel()
@@ -419,13 +392,11 @@ class WristbandManager:
await self.inventory[band_id].stop_monitoring()
async def release_band(self, band_id: str):
"""Release band back to inventory"""
await self.stop_monitoring(band_id)
if band_id in self.inventory:
self.inventory[band_id].release()
def get_status(self) -> dict:
"""Get overall status"""
status_counts = {}
for status in WristbandStatus:
status_counts[status.value] = sum(
@@ -445,7 +416,6 @@ class WristbandManager:
}
def print_inventory(self):
"""Print current inventory"""
print("\n" + "=" * 80)
print("WRISTBAND INVENTORY")
print("=" * 80)
@@ -471,57 +441,3 @@ class WristbandManager:
f"Active: {status['active_monitoring']}"
)
print("=" * 80 + "\n")
# ============================================================================
# EXAMPLE USAGE
# ============================================================================
async def main():
"""Example usage of wristband manager"""
manager = WristbandManager()
print("VitalLink Wristband Management System")
print("=" * 80)
# Option 1: Scan for real wristbands
# await manager.scan_for_real_bands(timeout=10.0)
# Option 2: Add simulated wristbands
manager.add_simulated_band("VitalLink-SIM1", "stable")
manager.add_simulated_band("VitalLink-SIM2", "mild_anxiety")
manager.add_simulated_band("VitalLink-SIM3", "deteriorating")
# Option 3: Manually add real wristband if you know the address
# manager.add_real_band("VitalLink-REAL1", "D7:91:3F:9A:12:34")
# Show inventory
manager.print_inventory()
# Assign bands to patients
band1 = manager.assign_band("P100001")
band2 = manager.assign_band("P100002")
# Start monitoring
if band1:
await manager.start_monitoring(band1.band_id)
if band2:
await manager.start_monitoring(band2.band_id)
# Monitor for 30 seconds
print("\nMonitoring for 30 seconds...")
await asyncio.sleep(30)
# Stop and release
if band1:
await manager.release_band(band1.band_id)
if band2:
await manager.release_band(band2.band_id)
manager.print_inventory()
if __name__ == "__main__":
asyncio.run(main())