import asyncio import json import logging import socket from typing import List from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from database import get_db from models import Device, VitalReading, TransmissionLog logger = logging.getLogger(__name__) router = APIRouter() # === WebSocket Manager for real-time vitals push === class VitalsWebSocketManager: """Manages WebSocket connections for live vitals streaming.""" def __init__(self): self.active_connections: List[WebSocket] = [] async def connect(self, websocket: WebSocket): await websocket.accept() self.active_connections.append(websocket) logger.info(f"WebSocket client connected ({len(self.active_connections)} active)") def disconnect(self, websocket: WebSocket): if websocket in self.active_connections: self.active_connections.remove(websocket) logger.info(f"WebSocket client disconnected ({len(self.active_connections)} active)") async def broadcast_vitals(self, vitals_data: dict): """Send vitals to all connected WebSocket clients.""" dead = [] for connection in self.active_connections: try: await connection.send_json(vitals_data) except Exception: dead.append(connection) for d in dead: self.disconnect(d) # Global singleton ws_manager = VitalsWebSocketManager() def get_ws_manager() -> VitalsWebSocketManager: return ws_manager # === Dashboard HTML Route === @router.get("/dashboard", response_class=HTMLResponse) def serve_dashboard(): """Serve the vitals monitoring dashboard.""" import os dashboard_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static", "dashboard.html") try: with open(dashboard_path, "r", encoding="utf-8") as f: return HTMLResponse(content=f.read()) except FileNotFoundError: return HTMLResponse(content="
static/dashboard.html is missing
", status_code=404) # === API Endpoints === @router.get("/health") def health_check(): return {"status": "ok", "service": "Patient Monitor Vital Signs Forwarder"} @router.get("/status") def get_system_status(db: Session = Depends(get_db)): total_devices = db.query(Device).count() active_devices = db.query(Device).filter(Device.status == "active").count() total_readings = db.query(VitalReading).count() pending_transmissions = db.query(VitalReading).filter(VitalReading.transmitted == False).count() return { "total_devices": total_devices, "active_devices": active_devices, "total_readings_stored": total_readings, "pending_transmissions": pending_transmissions } @router.get("/devices") def list_devices(db: Session = Depends(get_db)): devices = db.query(Device).all() return devices @router.get("/latest-readings") def get_latest_readings(limit: int = 10, db: Session = Depends(get_db)): readings = db.query(VitalReading).order_by(VitalReading.timestamp.desc()).limit(limit).all() return readings @router.get("/readings") def get_readings(device_id: str = None, patient_id: str = None, limit: int = 50, db: Session = Depends(get_db)): query = db.query(VitalReading) if device_id: query = query.filter(VitalReading.device_id == device_id) if patient_id: query = query.filter(VitalReading.patient_id == patient_id) return query.order_by(VitalReading.timestamp.desc()).limit(limit).all() @router.get("/transmission-logs") def get_transmission_logs(limit: int = 50, db: Session = Depends(get_db)): logs = db.query(TransmissionLog).order_by(TransmissionLog.timestamp.desc()).limit(limit).all() return logs @router.post("/test-api") def test_target_api_connection(): """ Endpoint to trigger a test POST to the target API. """ from config import settings return { "message": "Test triggered", "target_url": settings.target_api_url } @router.get("/network-info") def get_network_info(): """ Returns this PC's local IP addresses. Useful for configuring the CMS7000PLUS monitor's CMS Server IP. """ from config import settings ips = [] try: for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET): ip = info[4][0] if ip not in ips and not ip.startswith('127.'): ips.append(ip) except Exception: pass # Find primary IP primary_ip = None try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(('8.8.8.8', 80)) primary_ip = s.getsockname()[0] s.close() if primary_ip not in ips: ips.insert(0, primary_ip) except Exception: pass return { "hostname": socket.gethostname(), "ip_addresses": ips, "primary_ip": primary_ip or (ips[0] if ips else None), "monitor_model": settings.monitor_model, "contec_ports": settings.contec_ports, "instructions": [ f"1. Connect your {settings.monitor_model} to this PC via Ethernet", f"2. On the monitor: System Setup → Network → CMS Settings", f"3. Set Server IP to: {primary_ip or 'your PC IP'}", f"4. Set Server Port to: {settings.contec_ports[0] if settings.contec_ports else 511}", f"5. Enable the CMS connection", f"6. The dashboard will show data automatically" ] }