diff --git a/fiveparaminte-main/contec_parser.py b/fiveparaminte-main/contec_parser.py index 9a35125..4bd4de8 100644 --- a/fiveparaminte-main/contec_parser.py +++ b/fiveparaminte-main/contec_parser.py @@ -287,6 +287,7 @@ def _parse_45_byte_packet(data: bytes, vitals_dict: dict) -> bool: if "present_fields" not in vitals_dict: vitals_dict["present_fields"] = [] + logger.debug("DEBUG_NIBP_PKT len=%d nibp_hex=%s", len(data), data[8:24].hex()) nibp_sys = read_u16_le(data, 15) nibp_dia = read_u16_le(data, 17) nibp_map = read_u16_le(data, 19) @@ -371,42 +372,26 @@ def _parse_spo2_waveform_packet(data: bytes, vitals_dict: dict) -> bool: if "pleth_wave" not in vitals_dict["present_fields"]: vitals_dict["present_fields"].append("pleth_wave") - found = False - # Check SpO2 at standard offset 264 spo2_val = read_u16_le(data, 264) - # If 264 is disconnected sentinel, probe is NOT connected / off-finger - if spo2_val in (None, 65535, 32767, 255, 9999, 0): - vitals_dict["spo2"] = None - vitals_dict["heart_rate"] = None - if "spo2" not in vitals_dict["present_fields"]: - vitals_dict["present_fields"].append("spo2") - if "heart_rate" not in vitals_dict["present_fields"]: - vitals_dict["present_fields"].append("heart_rate") - return True - - if 50 <= spo2_val <= 100: + if spo2_val is not None and 50 <= spo2_val <= 100: vitals_dict["spo2"] = float(spo2_val) if "spo2" not in vitals_dict["present_fields"]: vitals_dict["present_fields"].append("spo2") - found = True - # ECG HR is at offset 266 in the 286-byte SpO2 waveform packet footer. - # Offset 268 is the SpO2 HIGH ALARM LIMIT (always 100) — NOT pulse rate! - # So we read ECG HR at 266 only. + # Pulse Rate / ECG HR is at offset 266 in the footer ecg_hr = read_u16_le(data, 266) - - logger.info( - f"SpO2 pkt: SpO2[264]={spo2_val} ECG_HR[266]={ecg_hr} alarm_hi[268]={read_u16_le(data,268)} alarm_lo[270]={read_u16_le(data,270) if len(data)>=272 else 'N/A'}" - ) - if ecg_hr is not None and ecg_hr not in (9999, 65535, 32767, 0) and 20 <= ecg_hr <= 300: vitals_dict["heart_rate"] = float(ecg_hr) if "heart_rate" not in vitals_dict["present_fields"]: vitals_dict["present_fields"].append("heart_rate") - return found + logger.info( + f"SpO2 pkt: SpO2[264]={spo2_val} ECG_HR[266]={ecg_hr} alarm_hi[268]={read_u16_le(data,268)} alarm_lo[270]={read_u16_le(data,270) if len(data)>=272 else 'N/A'}" + ) + + return True def _parse_286_byte_packet(data: bytes, vitals_dict: dict) -> bool: @@ -442,13 +427,13 @@ def _parse_989_byte_packet(data: bytes, vitals_dict: dict) -> bool: if "ecg_wave" not in vitals_dict["present_fields"]: vitals_dict["present_fields"].append("ecg_wave") + logger.debug("DEBUG_ECG_PKT len=%d tail_hex=%s", len(data), data[890:989].hex()) hr_val = read_u16_le(data, 904) - if hr_val is not None and hr_val not in (65535, 9999, 0) and 20 <= hr_val <= 300: - # Only use ECG HR if SpO2-probe PR has NOT already set heart_rate. - # SpO2 PR (from the pleth packet) matches the monitor display and wins. - if "heart_rate" not in vitals_dict.get("present_fields", []): + if hr_val is not None and hr_val not in (65535, 32767, 9999, 0) and 20 <= hr_val <= 300: + if vitals_dict.get("heart_rate") is None: vitals_dict["heart_rate"] = float(hr_val) - vitals_dict.setdefault("present_fields", []).append("heart_rate") + if "heart_rate" not in vitals_dict.setdefault("present_fields", []): + vitals_dict["present_fields"].append("heart_rate") return True @@ -486,9 +471,7 @@ def _find_vitals_block_in_stream(data: bytes, client_ip: str) -> Optional[Normal elif pkt_len == 989: if _parse_989_byte_packet(pkt_data, vitals_dict): found_any = True - elif 100 < pkt_len < 600: - if _parse_spo2_waveform_packet(pkt_data, vitals_dict): - found_any = True + # Handled specifically by known packet sizes above i += max(pkt_len, 4) else: diff --git a/fiveparaminte-main/database.py b/fiveparaminte-main/database.py index 770383a..12e4ae4 100644 --- a/fiveparaminte-main/database.py +++ b/fiveparaminte-main/database.py @@ -3,9 +3,11 @@ from sqlalchemy.pool import NullPool from sqlalchemy.orm import sessionmaker, declarative_base from config import settings +connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {} + engine = create_engine( settings.database_url, - connect_args={"check_same_thread": False}, + connect_args=connect_args, poolclass=NullPool ) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/fiveparaminte-main/main.py b/fiveparaminte-main/main.py index 6d8c61c..146b0b4 100644 --- a/fiveparaminte-main/main.py +++ b/fiveparaminte-main/main.py @@ -210,6 +210,13 @@ def serve_dashboard_root(): from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory="static"), name="static") +from fastapi.responses import Response +@app.get("/favicon.ico", include_in_schema=False) +async def favicon(): + return Response(status_code=204, media_type="image/x-icon") + + + # === WebSocket endpoint for live vitals === @app.websocket("/ws/vitals") diff --git a/fiveparaminte-main/routers/dashboard.py b/fiveparaminte-main/routers/dashboard.py index 2507e26..0dc9f82 100644 --- a/fiveparaminte-main/routers/dashboard.py +++ b/fiveparaminte-main/routers/dashboard.py @@ -202,14 +202,14 @@ def list_patients(db: Session = Depends(get_db)): except Exception as e: logger.debug(f"Could not read from device_cache: {e}") - # Fallback or cloud/Render mode: query DB strictly within recent time window + # Fallback or cloud/Render mode: query DB if merged_vitals is None: patient_filters = or_( VitalReading.patient_id == p.mrn, VitalReading.patient_id == p.patient_id, (VitalReading.device_id == p.device_id) if p.device_id else False ) - # Find readings within the live cutoff window (last 25 seconds) + # 1. Try to find readings within the live cutoff window (last 25 seconds) recent_readings = ( db.query(VitalReading) .filter(patient_filters, VitalReading.timestamp >= live_cutoff) @@ -217,8 +217,20 @@ def list_patients(db: Session = Depends(get_db)): .limit(20) .all() ) + # 2. Option B: If no live reading in last 25s, retain latest recorded readings from DB + if not recent_readings: + recent_readings = ( + db.query(VitalReading) + .filter(patient_filters) + .order_by(VitalReading.timestamp.desc()) + .limit(20) + .all() + ) if recent_readings: + ts_iso = recent_readings[0].timestamp.isoformat() if recent_readings[0].timestamp else None + if ts_iso and not ts_iso.endswith('Z') and '+' not in ts_iso: + ts_iso += 'Z' merged_vitals = { "heart_rate": None, "spo2": None, @@ -229,7 +241,7 @@ def list_patients(db: Session = Depends(get_db)): "temperature": None, "temperature2": None, "temperature_diff": None, - "timestamp": recent_readings[0].timestamp.isoformat() if recent_readings[0].timestamp else None, + "timestamp": ts_iso, } for r in reversed(recent_readings): if r.heart_rate is not None: merged_vitals["heart_rate"] = r.heart_rate @@ -238,24 +250,32 @@ def list_patients(db: Session = Depends(get_db)): if r.temperature is not None: merged_vitals["temperature"] = r.temperature if getattr(r, 'temperature2', None) is not None: merged_vitals["temperature2"] = r.temperature2 if getattr(r, 'temperature_diff', None) is not None: merged_vitals["temperature_diff"] = r.temperature_diff + if r.systolic_bp is not None: merged_vitals["systolic_bp"] = r.systolic_bp + if r.diastolic_bp is not None: merged_vitals["diastolic_bp"] = r.diastolic_bp + if r.map_bp is not None: merged_vitals["map_bp"] = r.map_bp + + # Check NIBP separately if not already populated + if not merged_vitals or merged_vitals.get("systolic_bp") is None: + r_bp = ( + db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp, VitalReading.timestamp) + .filter(patient_filters, VitalReading.systolic_bp != None) + .order_by(VitalReading.timestamp.desc()) + .first() + ) + if r_bp: + if merged_vitals is None: + ts_bp = r_bp[3].isoformat() if r_bp[3] else None + if ts_bp and not ts_bp.endswith('Z') and '+' not in ts_bp: + ts_bp += 'Z' + merged_vitals = { + "heart_rate": None, "spo2": None, "respiratory_rate": None, + "temperature": None, "temperature2": None, "temperature_diff": None, + "timestamp": ts_bp + } + merged_vitals["systolic_bp"] = r_bp[0] + merged_vitals["diastolic_bp"] = r_bp[1] + merged_vitals["map_bp"] = r_bp[2] - # Check NIBP separately within 15 minute window - r_bp = ( - db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp, VitalReading.timestamp) - .filter(patient_filters, VitalReading.systolic_bp != None, VitalReading.timestamp >= nibp_cutoff) - .order_by(VitalReading.timestamp.desc()) - .first() - ) - if r_bp: - if merged_vitals is None: - merged_vitals = { - "heart_rate": None, "spo2": None, "respiratory_rate": None, - "temperature": None, "temperature2": None, "temperature_diff": None, - "timestamp": r_bp[3].isoformat() if r_bp[3] else None - } - merged_vitals["systolic_bp"] = r_bp[0] - merged_vitals["diastolic_bp"] = r_bp[1] - merged_vitals["map_bp"] = r_bp[2] device = db.query(Device).filter(Device.device_id == p.device_id).first() if p.device_id else None diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html index d06a6d6..7d8a19e 100644 --- a/fiveparaminte-main/static/dashboard.html +++ b/fiveparaminte-main/static/dashboard.html @@ -8,6 +8,17 @@ + + +