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 @@
+
+
+