import asyncio import logging import multiprocessing import socket from contextlib import asynccontextmanager from functools import partial from fastapi import FastAPI, WebSocket, WebSocketDisconnect import uvicorn from config import settings from database import engine, Base from contec_server import handle_contec_client from background_tasks import retry_failed_transmissions from routers import dashboard from routers.dashboard import get_ws_manager # Setup Logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(settings.log_file), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # Create database tables if they don't exist Base.metadata.create_all(bind=engine) def _make_contec_handler(port: int): """Create a connection handler closure for a specific Contec port.""" async def handler(reader, writer): await handle_contec_client(reader, writer, port) return handler def _get_local_ips(): """Get all local IP addresses for this machine.""" 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 # Also try connecting to an external address to find the primary IP 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 ips if ips else ['Could not detect — check ipconfig'] @asynccontextmanager async def lifespan(app: FastAPI): servers = [] # === Start Contec CMS7000PLUS TCP Servers === logger.info("Starting Contec CMS7000PLUS listeners...") for port in settings.contec_ports: try: contec_server = await asyncio.start_server( _make_contec_handler(port), settings.hl7_host, port ) servers.append(contec_server) logger.info(f" [OK] Contec listener on port {port}") except PermissionError: logger.warning( f" [FAIL] Port {port} - Permission denied. " f"Run as Administrator for ports below 1024." ) except OSError as e: if "already in use" in str(e).lower() or getattr(e, 'errno', 0) == 10048: logger.warning(f" [FAIL] Port {port} - Already in use") else: logger.warning(f" [FAIL] Port {port} - {e}") if servers: active_ports = [] for s in servers: for sock in s.sockets: active_ports.append(sock.getsockname()[1]) logger.info(f" Active listener ports: {active_ports}") else: logger.error("No servers could be started! Check port availability and permissions.") # === Print startup banner === local_ips = _get_local_ips() model = settings.monitor_model print("\n" + "=" * 65) print(f" Contec {model} Patient Monitor - Vital Signs Forwarder") print("=" * 65) print(f" DASHBOARD: http://localhost:8000/api/dashboard") print(f" API Docs: http://localhost:8000/docs") print(f" Contec Ports: {settings.contec_ports}") print(f" Monitor Model: {model}") print(f" API Target: {settings.target_api_url}") print(f" Database: {settings.database_url}") print(f" ---------------------------------------------------------") print(f" YOUR PC's IP ADDRESSES (configure on your monitor):") for ip in local_ips: print(f" -> {ip}") print("=" * 65) print(f" Waiting for {model} connections...") print(f" On your monitor: System Setup > Network > CMS Settings") print(f" Set Server IP = one of the IPs above, Port = 511") print("=" * 65 + "\n") # Startup: Start the background retry task retry_task = asyncio.create_task(retry_failed_transmissions()) yield # Shutdown logger.info("Shutting down servers...") retry_task.cancel() for server in servers: server.close() for server in servers: await server.wait_closed() # Initialize FastAPI app app = FastAPI( title="Contec CMS7000PLUS Vital Signs Forwarder", description="Service to receive vital signs from Contec CMS7000PLUS patient monitors and forward them to a REST API", version="2.0.0", lifespan=lifespan ) # Include dashboard routes app.include_router(dashboard.router, prefix="/api") # === WebSocket endpoint for live vitals === @app.websocket("/ws/vitals") async def websocket_vitals(websocket: WebSocket): manager = get_ws_manager() await manager.connect(websocket) try: while True: # Keep connection alive, listen for any client messages await websocket.receive_text() except WebSocketDisconnect: manager.disconnect(websocket) except Exception: manager.disconnect(websocket) if __name__ == "__main__": # Required for Windows when building with PyInstaller using multiprocessing (uvicorn workers) multiprocessing.freeze_support() logger.info("Initializing Application...") # Run the FastAPI server. # Notice we run on port 8000 for the REST Dashboard, while HL7 is on settings.hl7_port (e.g. 6060) uvicorn.run(app, host="0.0.0.0", port=8000)