feat: implement Contec CMS7000PLUS TCP server and dashboard routing with deployment configuration
This commit is contained in:
@@ -24,6 +24,11 @@ ENV/
|
|||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
|
# Scratch / debug
|
||||||
|
scratch/
|
||||||
|
captures/
|
||||||
|
*.bin
|
||||||
|
|
||||||
# IDE / OS
|
# IDE / OS
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
web: uvicorn main:app --host 0.0.0.0 --port $PORT
|
||||||
@@ -41,3 +41,4 @@ When the application is running, it starts a web server on port `8000`. You can
|
|||||||
- `GET /api/devices`
|
- `GET /api/devices`
|
||||||
- `GET /api/latest-readings`
|
- `GET /api/latest-readings`
|
||||||
- `GET /api/transmission-logs`
|
- `GET /api/transmission-logs`
|
||||||
|
# ContectFiveParaMonitor
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ async def handle_contec_client(reader: asyncio.StreamReader, writer: asyncio.Str
|
|||||||
break
|
break
|
||||||
|
|
||||||
buffer += data
|
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)})")
|
logger.debug(f"Received {len(data)} bytes from {client_ip}:{client_port} on port {port} (buf={len(buffer)})")
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -4,7 +4,6 @@ sqlalchemy
|
|||||||
pydantic
|
pydantic
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
httpx
|
httpx
|
||||||
pyinstaller
|
gunicorn
|
||||||
rich
|
rich
|
||||||
pyserial
|
|
||||||
websockets
|
websockets
|
||||||
|
|||||||
@@ -185,3 +185,87 @@ def get_network_info():
|
|||||||
f"6. The dashboard will show data automatically"
|
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)}")
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
python-3.12.0
|
||||||
Reference in New Issue
Block a user