2026-07-08 14:14:03 +05:30
|
|
|
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])
|
2026-07-11 14:24:14 +05:30
|
|
|
app.state.active_ports = active_ports
|
2026-07-08 14:14:03 +05:30
|
|
|
logger.info(f" Active listener ports: {active_ports}")
|
|
|
|
|
else:
|
2026-07-11 14:24:14 +05:30
|
|
|
app.state.active_ports = []
|
2026-07-08 14:14:03 +05:30
|
|
|
logger.error("No servers could be started! Check port availability and permissions.")
|
|
|
|
|
|
|
|
|
|
# === Print startup banner ===
|
2026-07-11 14:24:14 +05:30
|
|
|
import os
|
|
|
|
|
port = int(os.environ.get("PORT", 8000))
|
2026-07-08 14:14:03 +05:30
|
|
|
local_ips = _get_local_ips()
|
|
|
|
|
model = settings.monitor_model
|
|
|
|
|
print("\n" + "=" * 65)
|
|
|
|
|
print(f" Contec {model} Patient Monitor - Vital Signs Forwarder")
|
|
|
|
|
print("=" * 65)
|
2026-07-11 14:24:14 +05:30
|
|
|
print(f" DASHBOARD: http://localhost:{port}/api/dashboard")
|
|
|
|
|
print(f" API Docs: http://localhost:{port}/docs")
|
2026-07-08 14:14:03 +05:30
|
|
|
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")
|
2026-07-11 14:24:14 +05:30
|
|
|
print(f" Set Server IP = one of the IPs above, Port = {settings.contec_ports[0] if settings.contec_ports else 511}")
|
2026-07-08 14:14:03 +05:30
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-13 14:59:37 +05:30
|
|
|
# Enable CORS for cross-origin app integrations (e.g. Electron apps, local dev servers)
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-08 14:14:03 +05:30
|
|
|
# Include dashboard routes
|
|
|
|
|
app.include_router(dashboard.router, prefix="/api")
|
|
|
|
|
|
2026-07-13 14:59:37 +05:30
|
|
|
# Serve static folder
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
|
|
2026-07-08 14:14:03 +05:30
|
|
|
|
|
|
|
|
# === 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.
|
2026-07-11 14:24:14 +05:30
|
|
|
# Notice we run on port 8000 (or PORT env var) for the REST Dashboard, while HL7 is on settings.hl7_port (e.g. 6060)
|
|
|
|
|
import os
|
|
|
|
|
port = int(os.environ.get("PORT", 8000))
|
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=port)
|