diff --git a/fiveparaminte-main/.gitignore b/fiveparaminte-main/.gitignore index e4715eb..a9bc693 100644 --- a/fiveparaminte-main/.gitignore +++ b/fiveparaminte-main/.gitignore @@ -24,6 +24,11 @@ ENV/ env.bak/ venv.bak/ +# Scratch / debug +scratch/ +captures/ +*.bin + # IDE / OS .idea/ .vscode/ diff --git a/fiveparaminte-main/Procfile b/fiveparaminte-main/Procfile new file mode 100644 index 0000000..0e04840 --- /dev/null +++ b/fiveparaminte-main/Procfile @@ -0,0 +1 @@ +web: uvicorn main:app --host 0.0.0.0 --port $PORT diff --git a/fiveparaminte-main/README.md b/fiveparaminte-main/README.md index 1f18e1d..fd73963 100644 --- a/fiveparaminte-main/README.md +++ b/fiveparaminte-main/README.md @@ -41,3 +41,4 @@ When the application is running, it starts a web server on port `8000`. You can - `GET /api/devices` - `GET /api/latest-readings` - `GET /api/transmission-logs` +# ContectFiveParaMonitor diff --git a/fiveparaminte-main/contec_server.py b/fiveparaminte-main/contec_server.py index 389aa9a..675f4b6 100644 --- a/fiveparaminte-main/contec_server.py +++ b/fiveparaminte-main/contec_server.py @@ -65,9 +65,6 @@ async def handle_contec_client(reader: asyncio.StreamReader, writer: asyncio.Str break buffer += data - # Temporary debug capture of raw binary stream - with open("/home/prathiyuman/Prathiyuman/ContecMonitor/fiveparaminte-main/scratch/raw_contec_stream.bin", "ab") as f: - f.write(data) logger.debug(f"Received {len(data)} bytes from {client_ip}:{client_port} on port {port} (buf={len(buffer)})") # --------------------------------------------------------------- diff --git a/fiveparaminte-main/render.yaml b/fiveparaminte-main/render.yaml new file mode 100644 index 0000000..0158f78 --- /dev/null +++ b/fiveparaminte-main/render.yaml @@ -0,0 +1,15 @@ +services: + - type: web + name: contec-monitor-api + runtime: python + buildCommand: pip install -r requirements.txt + startCommand: uvicorn main:app --host 0.0.0.0 --port $PORT + envVars: + - key: PORT + value: 10000 + - key: PYTHON_VERSION + value: 3.12.0 + - key: TARGET_API_URL + sync: false + - key: API_TOKEN + sync: false diff --git a/fiveparaminte-main/requirements.txt b/fiveparaminte-main/requirements.txt index 637ff8a..b76f540 100644 --- a/fiveparaminte-main/requirements.txt +++ b/fiveparaminte-main/requirements.txt @@ -4,7 +4,6 @@ sqlalchemy pydantic pydantic-settings httpx -pyinstaller +gunicorn rich -pyserial websockets diff --git a/fiveparaminte-main/routers/dashboard.py b/fiveparaminte-main/routers/dashboard.py index 8be7957..01bdf90 100644 --- a/fiveparaminte-main/routers/dashboard.py +++ b/fiveparaminte-main/routers/dashboard.py @@ -185,3 +185,87 @@ def get_network_info(): f"6. The dashboard will show data automatically" ] } + + +# === Push Vitals Endpoint (for local-to-cloud sync) === +from schemas import NormalizedVitals +from models import Patient + + +@router.post("/push-vitals") +async def push_vitals(vitals: NormalizedVitals, api_key: str = None, db: Session = Depends(get_db)): + """ + Receive vitals pushed from a local instance and store + broadcast them. + + This enables the 'split architecture' where: + - Local instance receives data from physical monitors via TCP + - Local instance POSTs vitals here for cloud storage & dashboard access + + Optional: pass ?api_key=YOUR_KEY for basic authentication. + """ + from config import settings + + # Optional API key check + if settings.api_token and api_key != settings.api_token: + if settings.api_token: # Only enforce if a token is configured + raise HTTPException(status_code=401, detail="Invalid or missing api_key") + + try: + # 1. Device tracking + device = db.query(Device).filter(Device.device_id == vitals.device_id).first() + if not device: + device = Device( + device_id=vitals.device_id, + ip_address=vitals.ip_address or "remote-push", + status="active" + ) + db.add(device) + else: + if vitals.ip_address: + device.ip_address = vitals.ip_address + device.status = "active" + device.last_seen = datetime.now(timezone.utc) + + # 2. Patient tracking + patient = db.query(Patient).filter(Patient.patient_id == vitals.patient_id).first() + if not patient: + patient = Patient(patient_id=vitals.patient_id, name="Unknown") + db.add(patient) + + # 3. Create vital reading + from models import VitalReading as VR + reading = VR( + device_id=vitals.device_id, + patient_id=vitals.patient_id, + timestamp=vitals.timestamp, + heart_rate=vitals.heart_rate, + spo2=vitals.spo2, + systolic_bp=vitals.systolic_bp, + diastolic_bp=vitals.diastolic_bp, + map_bp=vitals.map_bp, + respiratory_rate=vitals.respiratory_rate, + temperature=vitals.temperature, + transmitted=True # Already received = transmitted + ) + db.add(reading) + db.commit() + db.refresh(reading) + + # 4. Broadcast to WebSocket clients + await ws_manager.broadcast_vitals(vitals.model_dump(mode="json")) + + logger.info(f"Push-vitals received for device {vitals.device_id}") + + return { + "status": "ok", + "reading_id": reading.id, + "device_id": vitals.device_id, + "patient_id": vitals.patient_id + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Push-vitals error: {e}") + db.rollback() + raise HTTPException(status_code=500, detail=f"Failed to store vitals: {str(e)}") diff --git a/fiveparaminte-main/runtime.txt b/fiveparaminte-main/runtime.txt new file mode 100644 index 0000000..44f8fbe --- /dev/null +++ b/fiveparaminte-main/runtime.txt @@ -0,0 +1 @@ +python-3.12.0