diff --git a/api_client.py b/api_client.py
index 12fd250..a07825f 100644
--- a/api_client.py
+++ b/api_client.py
@@ -79,3 +79,36 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
logger.warning(f"Error forwarding to Render Cloud: {type(e).__name__} ({err_msg})")
return primary_success or render_success
+
+
+async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) -> bool:
+ """
+ Synchronizes patient demographics assignment to Render Cloud Server.
+ Ensures local and remote dashboards always show the exact same patient.
+ """
+ import os
+ is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "")
+ if is_on_render:
+ return True
+
+ client = get_http_client()
+ render_assign_url = "https://contectfiveparamonitor.onrender.com/api/patient/assign"
+ payload = {
+ "device_id": device_id,
+ "mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "",
+ "name": patient_data.get("name") or patient_data.get("patient_name") or "Unknown",
+ "dob": patient_data.get("dob") or patient_data.get("patient_dob"),
+ "gender": patient_data.get("gender") or patient_data.get("patient_gender"),
+ "bed_id": patient_data.get("bed_id"),
+ }
+ try:
+ url_with_key = render_assign_url
+ if settings.api_token:
+ url_with_key = f"{render_assign_url}?api_key={settings.api_token}"
+ resp = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"})
+ resp.raise_for_status()
+ logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Render Cloud for {device_id}")
+ return True
+ except Exception as e:
+ logger.warning(f"Could not sync patient assignment to Render Cloud: {type(e).__name__} ({e})")
+ return False
diff --git a/contec_parser.py b/contec_parser.py
index 37b17e6..4bd4de8 100644
--- a/contec_parser.py
+++ b/contec_parser.py
@@ -165,6 +165,10 @@ def parse_contec_hl7_text(raw_text: str, client_ip: str) -> Optional[NormalizedV
if len(fields) > 8 and fields[8]:
vitals_dict["patient_gender"] = fields[8].strip().upper()
+ # PID-35: Patient Category (ADULT / PEDIATRIC / NEONATAL)
+ if len(fields) > 35 and fields[35]:
+ vitals_dict["patient_type"] = fields[35].strip()
+
elif seg_name == "PV1":
# PV1-3: Assigned Patient Location (Room^Bed^...)
if len(fields) > 3 and fields[3]:
@@ -174,6 +178,10 @@ def parse_contec_hl7_text(raw_text: str, client_ip: str) -> Optional[NormalizedV
if bed:
vitals_dict["bed_id"] = bed
+ # PV1-18: Patient Type
+ if len(fields) > 18 and fields[18]:
+ vitals_dict["patient_type"] = fields[18].strip()
+
elif seg_name == "OBX":
if len(fields) > 5:
obs_val = fields[5].strip()
@@ -279,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)
@@ -363,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:
@@ -434,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
@@ -478,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/contec_server.py b/contec_server.py
index 54a4f44..0a43f8a 100644
--- a/contec_server.py
+++ b/contec_server.py
@@ -58,25 +58,58 @@ TELEMETRY_WORDS = {
}
+def is_telemetry_word(s_lower: str) -> bool:
+ """Check if string matches hardware telemetry or alarm words."""
+ if s_lower in TELEMETRY_WORDS:
+ return True
+ tokens = set(re.findall(r'[a-zA-Z0-9]+', s_lower))
+ alarm_tokens = {
+ 'sensor', 'alarm', 'lead', 'leads', 'searching', 'pulse', 'cuff', 'probe',
+ 'off', 'fall', 'leak', 'motion', 'weak', 'arr', 'asystole', 'vfib', 'vtac',
+ 'pvc', 'brady', 'tachy', 'error', 'ecg', 'spo2', 'nibp', 'resp', 'temp',
+ 'mv', 'bpm', 'mmhg'
+ }
+ if any(tok in alarm_tokens for tok in tokens):
+ return True
+ return False
+
+
def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
"""
- Extracts real patient demographics from Contec CMS8500 / CMS7000 binary packets.
+ Extracts real patient demographics from Contec packets.
Only extracts clean, legitimate patient fields — strictly rejects sensor alarms,
waveform noise, and hardware telemetry.
"""
+ patient_type = None
+ data_upper = data.upper()
+ if b'PEDIATRIC' in data_upper or b'PED' in data_upper:
+ patient_type = 'Pediatric'
+ elif b'NEONATE' in data_upper or b'NEONATAL' in data_upper or b'NEO' in data_upper:
+ patient_type = 'Neonatal'
+ elif b'ADULT' in data_upper or b'ADU' in data_upper:
+ patient_type = 'Adult'
+
+ # Strictly reject binary waveform/measurement packets from generating false patient records.
+ # Patient MRN/Name extraction requires an authentic HL7 message with PID or MSH.
+ if b'PID|' not in data and b'MSH|' not in data:
+ if patient_type:
+ return {"patient_type": patient_type}
+ return None
+
clean = []
- for m in re.finditer(rb'[a-zA-Z0-9_\-\.]{2,}', data):
+ # Match strings of printable alphanumeric and reasonable characters, allowing single spaces
+ for m in re.finditer(rb'[a-zA-Z0-9_\-\.]{2,}(?:\s+[a-zA-Z0-9_\-\.]{2,})*', data):
s = m.group(0).decode('latin1', errors='ignore').strip()
if not s or len(s) < 2:
continue
s_lower = s.lower()
- if any(kw in s_lower for kw in TELEMETRY_WORDS):
+ if is_telemetry_word(s_lower):
continue
if len(set(s)) == 1:
continue
if set(s).issubset({'.', '-', '_', '/', '\\'}):
continue
- if len(s) > 8 and re.match(r'^[A-Za-z]+$', s):
+ if len(s) > 6 and re.match(r'^[A-Za-z0-9]+$', s):
diffs = [abs(ord(s[i]) - ord(s[i-1])) for i in range(1, len(s))]
if sum(1 for d in diffs if d <= 2) / len(diffs) > 0.6:
continue
@@ -84,7 +117,7 @@ def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
continue
clean.append(s)
- if not clean:
+ if not clean and not patient_type:
return None
name = None
@@ -95,9 +128,8 @@ def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
for s in clean:
s_up = s.upper()
s_low = s.lower()
- if s_low in TELEMETRY_WORDS:
- continue
+ # Check gender first
if s_up in ('MALE', 'FEMALE') and not gender:
gender = 'Male' if s_up == 'MALE' else 'Female'
continue
@@ -105,52 +137,75 @@ def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
gender = 'Male' if s_up == 'M' else 'Female'
continue
- # Date of birth: 4-digit birth year between 1920 and 2024, or YYYY-MM-DD
- if re.match(r'^(19\d\d|20[01]\d|202[0-4])$', s) and not dob:
+ if is_telemetry_word(s_low):
+ continue
+
+ # Date of birth: 4-digit birth year between 1920 and 2026, or YYYY-MM-DD
+ if re.match(r'^(19\d\d|20[01]\d|202[0-6])$', s) and not dob:
dob = s
continue
elif re.match(r'^\d{4}[-/]\d{2}[-/]\d{2}$', s) and not dob:
dob = s
continue
- # Name candidate: strictly at least 3 letters, has a vowel, not telemetry
- if re.search(r'^[A-Za-z\s\.\-]{3,30}$', s) and re.search(r'[aeiouyAEIOUY]', s) and not name:
- if s_low not in TELEMETRY_WORDS:
+ # Name candidate: contains letters and a vowel, length between 3 and 40
+ if re.search(r'^[A-Za-z\s\.\-]{3,40}$', s) and re.search(r'[aeiouyAEIOUY]', s) and not name:
+ if not is_telemetry_word(s_low):
name = s
continue
- # MRN candidate: numeric or alphanumeric ID, at least 2 chars, not a year, not telemetry
- if not mrn and re.match(r'^[A-Za-z0-9\-]{2,15}$', s) and s_low not in TELEMETRY_WORDS:
+ # MRN candidate: numeric or alphanumeric ID, 2..15 chars, not a birth year
+ if not mrn and re.match(r'^[A-Za-z0-9\-]{2,15}$', s) and not is_telemetry_word(s_low):
if not (s.isdigit() and 1920 <= int(s) <= 2030):
+ if s.isdigit() and len(s) > 10:
+ continue
mrn = s
continue
- # Only accept when BOTH real name and real MRN/DOB are present together
- if name and (mrn or dob):
+ # Accept if valid MRN or Name was detected
+ if mrn or name:
+ clean_mrn = mrn or name
+ clean_name = name or f"Patient {clean_mrn}"
return {
- "name": name,
- "mrn": mrn or name,
- "patient_id": mrn or name,
+ "name": clean_name,
+ "mrn": clean_mrn,
+ "patient_id": clean_mrn,
"dob": dob,
"gender": gender,
+ "patient_type": patient_type,
+ }
+ elif patient_type:
+ return {
+ "patient_type": patient_type,
}
return None
async def apply_auto_demographics(client_ip: str, demo: dict):
from config import settings
+ from api_client import sync_patient_assignment_to_cloud
model = settings.device_models.get(client_ip, settings.monitor_model)
device_id = f"{model}_{client_ip}"
name = demo.get("name")
mrn = demo.get("mrn")
dob = demo.get("dob")
gender = demo.get("gender")
+ patient_type = demo.get("patient_type")
current = device_patient_map.get(device_id, {})
- if current.get("name") == name and current.get("mrn") == mrn and current.get("dob") == dob and current.get("gender") == gender:
+ # If only patient_type updated
+ if not mrn and not name:
+ if patient_type and current.get("patient_type") != patient_type:
+ current["patient_type"] = patient_type
+ device_patient_map[device_id] = current
return
- logger.info(f"*** REAL PATIENT DEMOGRAPHICS DETECTED FROM MACHINE ({client_ip}): Name={name}, MRN={mrn}, DOB={dob}, Gender={gender} ***")
+ if (current.get("name") == name and current.get("mrn") == mrn and
+ current.get("dob") == dob and current.get("gender") == gender and
+ current.get("patient_type") == patient_type):
+ return
+
+ logger.info(f"*** REAL PATIENT DEMOGRAPHICS DETECTED FROM MACHINE ({client_ip}): Name={name}, MRN={mrn}, DOB={dob}, Gender={gender}, Type={patient_type} ***")
bed_id = "1" if "192.168.100.106" in client_ip else "2"
p_data = {
@@ -159,7 +214,8 @@ async def apply_auto_demographics(client_ip: str, demo: dict):
"name": name,
"gender": gender,
"dob": dob,
- "bed_id": bed_id
+ "bed_id": bed_id,
+ "patient_type": patient_type or current.get("patient_type"),
}
device_patient_map[device_id] = p_data
@@ -218,11 +274,15 @@ async def apply_auto_demographics(client_ip: str, demo: dict):
"gender": gender,
"patient_dob": dob,
"dob": dob,
- "bed_id": bed_id
+ "bed_id": bed_id,
+ "patient_type": p_data.get("patient_type"),
})
except Exception as e:
logger.debug(f"Failed to broadcast auto patient: {e}")
+ # Forward to Render Cloud Server so remote is kept in sync
+ asyncio.create_task(sync_patient_assignment_to_cloud(p_data, device_id))
+
async def handle_contec_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int):
"""
@@ -251,8 +311,13 @@ async def handle_contec_client(reader: asyncio.StreamReader, writer: asyncio.Str
logger.info(f"Contec client {client_ip}:{client_port} on port {port} sent EOF (0 bytes)")
break
- if client_ip != "127.0.0.1":
- logger.info(f"TCP READ from {client_ip}: {len(data)} bytes, hex: {data[:24].hex()}")
+ # Dynamically extract and apply patient demographics sent from the machine
+ try:
+ demo = extract_demographics_from_packet(data, client_ip)
+ if demo:
+ await apply_auto_demographics(client_ip, demo)
+ except Exception as ex:
+ logger.debug(f"Demographics extraction error: {ex}")
buffer += data
@@ -376,6 +441,7 @@ def get_assigned_patient(device_id: str):
"gender": patient.gender,
"dob": patient.dob,
"bed_id": patient.bed_id,
+ "patient_type": getattr(patient, "patient_type", None),
}
device_patient_map[device_id] = p_data
return p_data
@@ -396,6 +462,7 @@ def set_assigned_patient(device_id: str, patient_data: dict):
cached.patient_gender = patient_data.get("gender")
cached.patient_dob = patient_data.get("dob")
cached.bed_id = patient_data.get("bed_id")
+ cached.patient_type = patient_data.get("patient_type")
async def process_vitals(vitals):
@@ -416,6 +483,7 @@ async def process_vitals(vitals):
vitals.patient_gender = assigned.get("gender")
vitals.patient_dob = assigned.get("dob")
vitals.bed_id = assigned.get("bed_id")
+ vitals.patient_type = assigned.get("patient_type")
elif vitals.patient_name and vitals.patient_name != "Unknown":
# Monitor sent fresh demographics via HL7 PID segment!
device_patient_map[device_id] = {
@@ -425,6 +493,7 @@ async def process_vitals(vitals):
"gender": vitals.patient_gender,
"dob": vitals.patient_dob,
"bed_id": vitals.bed_id,
+ "patient_type": getattr(vitals, "patient_type", None),
}
# Check if incoming packet has fresh waveforms
@@ -453,14 +522,12 @@ async def process_vitals(vitals):
if value is not None:
active_fields.append(field)
- # Update cache & record timestamps
+ # Update cache & record timestamps for present values
for field in active_fields:
val = getattr(vitals, field, None)
- setattr(cached, field, val)
if val is not None:
+ setattr(cached, field, val)
device_field_timestamps[device_id][field] = now
- else:
- device_field_timestamps[device_id].pop(field, None)
# Clean up stale fields:
# Live vitals (HR, SpO2, Temp, RR) have a 60-second window because monitor cycles take ~24s
@@ -506,6 +573,7 @@ async def process_vitals(vitals):
vitals_dict["gender"] = vitals.patient_gender
vitals_dict["dob"] = vitals.patient_dob
vitals_dict["bed_id"] = vitals.bed_id
+ vitals_dict["patient_type"] = getattr(vitals, "patient_type", None)
await ws_manager.broadcast_vitals(vitals_dict)
except Exception as e:
logger.debug(f"WebSocket broadcast failed: {e}")
@@ -569,6 +637,7 @@ async def _persist_and_forward_bg(vitals, should_persist):
gender=vitals.patient_gender,
dob=vitals.patient_dob,
bed_id=vitals.bed_id,
+ patient_type=getattr(vitals, "patient_type", None),
device_id=vitals.device_id
)
db.add(patient)
@@ -584,6 +653,8 @@ async def _persist_and_forward_bg(vitals, should_persist):
patient.dob = vitals.patient_dob
if vitals.bed_id:
patient.bed_id = vitals.bed_id
+ if getattr(vitals, "patient_type", None):
+ patient.patient_type = vitals.patient_type
if vitals.device_id:
patient.device_id = vitals.device_id
diff --git a/fiveparaminte-main/api_client.py b/fiveparaminte-main/api_client.py
index 12fd250..a07825f 100644
--- a/fiveparaminte-main/api_client.py
+++ b/fiveparaminte-main/api_client.py
@@ -79,3 +79,36 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
logger.warning(f"Error forwarding to Render Cloud: {type(e).__name__} ({err_msg})")
return primary_success or render_success
+
+
+async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) -> bool:
+ """
+ Synchronizes patient demographics assignment to Render Cloud Server.
+ Ensures local and remote dashboards always show the exact same patient.
+ """
+ import os
+ is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "")
+ if is_on_render:
+ return True
+
+ client = get_http_client()
+ render_assign_url = "https://contectfiveparamonitor.onrender.com/api/patient/assign"
+ payload = {
+ "device_id": device_id,
+ "mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "",
+ "name": patient_data.get("name") or patient_data.get("patient_name") or "Unknown",
+ "dob": patient_data.get("dob") or patient_data.get("patient_dob"),
+ "gender": patient_data.get("gender") or patient_data.get("patient_gender"),
+ "bed_id": patient_data.get("bed_id"),
+ }
+ try:
+ url_with_key = render_assign_url
+ if settings.api_token:
+ url_with_key = f"{render_assign_url}?api_key={settings.api_token}"
+ resp = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"})
+ resp.raise_for_status()
+ logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Render Cloud for {device_id}")
+ return True
+ except Exception as e:
+ logger.warning(f"Could not sync patient assignment to Render Cloud: {type(e).__name__} ({e})")
+ return False
diff --git a/fiveparaminte-main/contec_parser.py b/fiveparaminte-main/contec_parser.py
index 37b17e6..9a35125 100644
--- a/fiveparaminte-main/contec_parser.py
+++ b/fiveparaminte-main/contec_parser.py
@@ -165,6 +165,10 @@ def parse_contec_hl7_text(raw_text: str, client_ip: str) -> Optional[NormalizedV
if len(fields) > 8 and fields[8]:
vitals_dict["patient_gender"] = fields[8].strip().upper()
+ # PID-35: Patient Category (ADULT / PEDIATRIC / NEONATAL)
+ if len(fields) > 35 and fields[35]:
+ vitals_dict["patient_type"] = fields[35].strip()
+
elif seg_name == "PV1":
# PV1-3: Assigned Patient Location (Room^Bed^...)
if len(fields) > 3 and fields[3]:
@@ -174,6 +178,10 @@ def parse_contec_hl7_text(raw_text: str, client_ip: str) -> Optional[NormalizedV
if bed:
vitals_dict["bed_id"] = bed
+ # PV1-18: Patient Type
+ if len(fields) > 18 and fields[18]:
+ vitals_dict["patient_type"] = fields[18].strip()
+
elif seg_name == "OBX":
if len(fields) > 5:
obs_val = fields[5].strip()
diff --git a/fiveparaminte-main/contec_server.py b/fiveparaminte-main/contec_server.py
index 54a4f44..6ced765 100644
--- a/fiveparaminte-main/contec_server.py
+++ b/fiveparaminte-main/contec_server.py
@@ -58,25 +58,58 @@ TELEMETRY_WORDS = {
}
+def is_telemetry_word(s_lower: str) -> bool:
+ """Check if string matches hardware telemetry or alarm words."""
+ if s_lower in TELEMETRY_WORDS:
+ return True
+ tokens = set(re.findall(r'[a-zA-Z0-9]+', s_lower))
+ alarm_tokens = {
+ 'sensor', 'alarm', 'lead', 'leads', 'searching', 'pulse', 'cuff', 'probe',
+ 'off', 'fall', 'leak', 'motion', 'weak', 'arr', 'asystole', 'vfib', 'vtac',
+ 'pvc', 'brady', 'tachy', 'error', 'ecg', 'spo2', 'nibp', 'resp', 'temp',
+ 'mv', 'bpm', 'mmhg'
+ }
+ if any(tok in alarm_tokens for tok in tokens):
+ return True
+ return False
+
+
def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
"""
- Extracts real patient demographics from Contec CMS8500 / CMS7000 binary packets.
+ Extracts real patient demographics from Contec packets.
Only extracts clean, legitimate patient fields — strictly rejects sensor alarms,
waveform noise, and hardware telemetry.
"""
+ patient_type = None
+ data_upper = data.upper()
+ if b'PEDIATRIC' in data_upper or b'PED' in data_upper:
+ patient_type = 'Pediatric'
+ elif b'NEONATE' in data_upper or b'NEONATAL' in data_upper or b'NEO' in data_upper:
+ patient_type = 'Neonatal'
+ elif b'ADULT' in data_upper or b'ADU' in data_upper:
+ patient_type = 'Adult'
+
+ # Strictly reject binary waveform/measurement packets from generating false patient records.
+ # Patient MRN/Name extraction requires an authentic HL7 message with PID or MSH.
+ if b'PID|' not in data and b'MSH|' not in data:
+ if patient_type:
+ return {"patient_type": patient_type}
+ return None
+
clean = []
- for m in re.finditer(rb'[a-zA-Z0-9_\-\.]{2,}', data):
+ # Match strings of printable alphanumeric and reasonable characters, allowing single spaces
+ for m in re.finditer(rb'[a-zA-Z0-9_\-\.]{2,}(?:\s+[a-zA-Z0-9_\-\.]{2,})*', data):
s = m.group(0).decode('latin1', errors='ignore').strip()
if not s or len(s) < 2:
continue
s_lower = s.lower()
- if any(kw in s_lower for kw in TELEMETRY_WORDS):
+ if is_telemetry_word(s_lower):
continue
if len(set(s)) == 1:
continue
if set(s).issubset({'.', '-', '_', '/', '\\'}):
continue
- if len(s) > 8 and re.match(r'^[A-Za-z]+$', s):
+ if len(s) > 6 and re.match(r'^[A-Za-z0-9]+$', s):
diffs = [abs(ord(s[i]) - ord(s[i-1])) for i in range(1, len(s))]
if sum(1 for d in diffs if d <= 2) / len(diffs) > 0.6:
continue
@@ -84,7 +117,7 @@ def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
continue
clean.append(s)
- if not clean:
+ if not clean and not patient_type:
return None
name = None
@@ -95,9 +128,8 @@ def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
for s in clean:
s_up = s.upper()
s_low = s.lower()
- if s_low in TELEMETRY_WORDS:
- continue
+ # Check gender first
if s_up in ('MALE', 'FEMALE') and not gender:
gender = 'Male' if s_up == 'MALE' else 'Female'
continue
@@ -105,52 +137,75 @@ def extract_demographics_from_packet(data: bytes, client_ip: str) -> dict:
gender = 'Male' if s_up == 'M' else 'Female'
continue
- # Date of birth: 4-digit birth year between 1920 and 2024, or YYYY-MM-DD
- if re.match(r'^(19\d\d|20[01]\d|202[0-4])$', s) and not dob:
+ if is_telemetry_word(s_low):
+ continue
+
+ # Date of birth: 4-digit birth year between 1920 and 2026, or YYYY-MM-DD
+ if re.match(r'^(19\d\d|20[01]\d|202[0-6])$', s) and not dob:
dob = s
continue
elif re.match(r'^\d{4}[-/]\d{2}[-/]\d{2}$', s) and not dob:
dob = s
continue
- # Name candidate: strictly at least 3 letters, has a vowel, not telemetry
- if re.search(r'^[A-Za-z\s\.\-]{3,30}$', s) and re.search(r'[aeiouyAEIOUY]', s) and not name:
- if s_low not in TELEMETRY_WORDS:
+ # Name candidate: contains letters and a vowel, length between 3 and 40
+ if re.search(r'^[A-Za-z\s\.\-]{3,40}$', s) and re.search(r'[aeiouyAEIOUY]', s) and not name:
+ if not is_telemetry_word(s_low):
name = s
continue
- # MRN candidate: numeric or alphanumeric ID, at least 2 chars, not a year, not telemetry
- if not mrn and re.match(r'^[A-Za-z0-9\-]{2,15}$', s) and s_low not in TELEMETRY_WORDS:
+ # MRN candidate: numeric or alphanumeric ID, 2..15 chars, not a birth year
+ if not mrn and re.match(r'^[A-Za-z0-9\-]{2,15}$', s) and not is_telemetry_word(s_low):
if not (s.isdigit() and 1920 <= int(s) <= 2030):
+ if s.isdigit() and len(s) > 10:
+ continue
mrn = s
continue
- # Only accept when BOTH real name and real MRN/DOB are present together
- if name and (mrn or dob):
+ # Accept if valid MRN or Name was detected
+ if mrn or name:
+ clean_mrn = mrn or name
+ clean_name = name or f"Patient {clean_mrn}"
return {
- "name": name,
- "mrn": mrn or name,
- "patient_id": mrn or name,
+ "name": clean_name,
+ "mrn": clean_mrn,
+ "patient_id": clean_mrn,
"dob": dob,
"gender": gender,
+ "patient_type": patient_type,
+ }
+ elif patient_type:
+ return {
+ "patient_type": patient_type,
}
return None
async def apply_auto_demographics(client_ip: str, demo: dict):
from config import settings
+ from api_client import sync_patient_assignment_to_cloud
model = settings.device_models.get(client_ip, settings.monitor_model)
device_id = f"{model}_{client_ip}"
name = demo.get("name")
mrn = demo.get("mrn")
dob = demo.get("dob")
gender = demo.get("gender")
+ patient_type = demo.get("patient_type")
current = device_patient_map.get(device_id, {})
- if current.get("name") == name and current.get("mrn") == mrn and current.get("dob") == dob and current.get("gender") == gender:
+ # If only patient_type updated
+ if not mrn and not name:
+ if patient_type and current.get("patient_type") != patient_type:
+ current["patient_type"] = patient_type
+ device_patient_map[device_id] = current
return
- logger.info(f"*** REAL PATIENT DEMOGRAPHICS DETECTED FROM MACHINE ({client_ip}): Name={name}, MRN={mrn}, DOB={dob}, Gender={gender} ***")
+ if (current.get("name") == name and current.get("mrn") == mrn and
+ current.get("dob") == dob and current.get("gender") == gender and
+ current.get("patient_type") == patient_type):
+ return
+
+ logger.info(f"*** REAL PATIENT DEMOGRAPHICS DETECTED FROM MACHINE ({client_ip}): Name={name}, MRN={mrn}, DOB={dob}, Gender={gender}, Type={patient_type} ***")
bed_id = "1" if "192.168.100.106" in client_ip else "2"
p_data = {
@@ -159,7 +214,8 @@ async def apply_auto_demographics(client_ip: str, demo: dict):
"name": name,
"gender": gender,
"dob": dob,
- "bed_id": bed_id
+ "bed_id": bed_id,
+ "patient_type": patient_type or current.get("patient_type"),
}
device_patient_map[device_id] = p_data
@@ -218,11 +274,15 @@ async def apply_auto_demographics(client_ip: str, demo: dict):
"gender": gender,
"patient_dob": dob,
"dob": dob,
- "bed_id": bed_id
+ "bed_id": bed_id,
+ "patient_type": p_data.get("patient_type"),
})
except Exception as e:
logger.debug(f"Failed to broadcast auto patient: {e}")
+ # Forward to Render Cloud Server so remote is kept in sync
+ asyncio.create_task(sync_patient_assignment_to_cloud(p_data, device_id))
+
async def handle_contec_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, port: int):
"""
@@ -251,8 +311,13 @@ async def handle_contec_client(reader: asyncio.StreamReader, writer: asyncio.Str
logger.info(f"Contec client {client_ip}:{client_port} on port {port} sent EOF (0 bytes)")
break
- if client_ip != "127.0.0.1":
- logger.info(f"TCP READ from {client_ip}: {len(data)} bytes, hex: {data[:24].hex()}")
+ # Dynamically extract and apply patient demographics sent from the machine
+ try:
+ demo = extract_demographics_from_packet(data, client_ip)
+ if demo:
+ await apply_auto_demographics(client_ip, demo)
+ except Exception as ex:
+ logger.debug(f"Demographics extraction error: {ex}")
buffer += data
@@ -376,6 +441,7 @@ def get_assigned_patient(device_id: str):
"gender": patient.gender,
"dob": patient.dob,
"bed_id": patient.bed_id,
+ "patient_type": getattr(patient, "patient_type", None),
}
device_patient_map[device_id] = p_data
return p_data
@@ -396,6 +462,7 @@ def set_assigned_patient(device_id: str, patient_data: dict):
cached.patient_gender = patient_data.get("gender")
cached.patient_dob = patient_data.get("dob")
cached.bed_id = patient_data.get("bed_id")
+ cached.patient_type = patient_data.get("patient_type")
async def process_vitals(vitals):
@@ -416,6 +483,7 @@ async def process_vitals(vitals):
vitals.patient_gender = assigned.get("gender")
vitals.patient_dob = assigned.get("dob")
vitals.bed_id = assigned.get("bed_id")
+ vitals.patient_type = assigned.get("patient_type")
elif vitals.patient_name and vitals.patient_name != "Unknown":
# Monitor sent fresh demographics via HL7 PID segment!
device_patient_map[device_id] = {
@@ -425,6 +493,7 @@ async def process_vitals(vitals):
"gender": vitals.patient_gender,
"dob": vitals.patient_dob,
"bed_id": vitals.bed_id,
+ "patient_type": getattr(vitals, "patient_type", None),
}
# Check if incoming packet has fresh waveforms
@@ -506,6 +575,7 @@ async def process_vitals(vitals):
vitals_dict["gender"] = vitals.patient_gender
vitals_dict["dob"] = vitals.patient_dob
vitals_dict["bed_id"] = vitals.bed_id
+ vitals_dict["patient_type"] = getattr(vitals, "patient_type", None)
await ws_manager.broadcast_vitals(vitals_dict)
except Exception as e:
logger.debug(f"WebSocket broadcast failed: {e}")
@@ -569,6 +639,7 @@ async def _persist_and_forward_bg(vitals, should_persist):
gender=vitals.patient_gender,
dob=vitals.patient_dob,
bed_id=vitals.bed_id,
+ patient_type=getattr(vitals, "patient_type", None),
device_id=vitals.device_id
)
db.add(patient)
@@ -584,6 +655,8 @@ async def _persist_and_forward_bg(vitals, should_persist):
patient.dob = vitals.patient_dob
if vitals.bed_id:
patient.bed_id = vitals.bed_id
+ if getattr(vitals, "patient_type", None):
+ patient.patient_type = vitals.patient_type
if vitals.device_id:
patient.device_id = vitals.device_id
diff --git a/fiveparaminte-main/main.py b/fiveparaminte-main/main.py
index 8683ee7..8e3d9c9 100644
--- a/fiveparaminte-main/main.py
+++ b/fiveparaminte-main/main.py
@@ -62,6 +62,22 @@ def _get_local_ips():
@asynccontextmanager
async def lifespan(app: FastAPI):
+ # Protect against Windows IOCP Proactor crashes (WinError 64 / 121 / 10054 on accept)
+ import sys
+ if sys.platform == "win32":
+ try:
+ loop = asyncio.get_running_loop()
+ def silence_windows_iocp_errors(loop, context):
+ msg = str(context.get("message", ""))
+ exc = context.get("exception")
+ winerr = getattr(exc, "winerror", None)
+ if winerr in (64, 121, 10054) or "Accept failed on a socket" in msg or "The specified network name is no longer available" in msg:
+ return
+ loop.default_exception_handler(context)
+ loop.set_exception_handler(silence_windows_iocp_errors)
+ except Exception:
+ pass
+
servers = []
# === Start Contec CMS7000PLUS TCP Servers ===
diff --git a/fiveparaminte-main/models.py b/fiveparaminte-main/models.py
index b03be40..153f87b 100644
--- a/fiveparaminte-main/models.py
+++ b/fiveparaminte-main/models.py
@@ -18,8 +18,9 @@ class Patient(Base):
name = Column(String, nullable=True) # full name from PID-5
gender = Column(String, nullable=True) # M/F/U from PID-8
dob = Column(String, nullable=True) # YYYYMMDD from PID-7
- bed_id = Column(String, nullable=True) # bed from PV1-3
- device_id = Column(String, nullable=True, index=True) # assigned machine / device_id
+ bed_id = Column(String, nullable=True) # bed from PV1-3
+ patient_type = Column(String, nullable=True) # Adult / Pediatric / Neonatal
+ device_id = Column(String, nullable=True, index=True) # assigned machine / device_id
class VitalReading(Base):
__tablename__ = "vital_readings"
diff --git a/fiveparaminte-main/routers/dashboard.py b/fiveparaminte-main/routers/dashboard.py
index 9b41a48..e9bd0e8 100644
--- a/fiveparaminte-main/routers/dashboard.py
+++ b/fiveparaminte-main/routers/dashboard.py
@@ -24,6 +24,7 @@ class PatientAssignRequest(BaseModel):
dob: Optional[str] = None
gender: Optional[str] = None
bed_id: Optional[str] = None
+ patient_type: Optional[str] = None
# === WebSocket Manager for real-time vitals push ===
class VitalsWebSocketManager:
@@ -164,8 +165,8 @@ def list_patients(db: Session = Depends(get_db)):
patients = db.query(Patient).filter(Patient.patient_id != "UNKNOWN", Patient.mrn != "UNKNOWN").all()
results = []
for p in patients:
- # Find latest vitals for this patient or this patient's assigned device
- latest_reading = (
+ # Find latest non-null vitals across recent readings for this patient or device
+ recent_readings = (
db.query(VitalReading)
.filter(
or_(
@@ -175,9 +176,60 @@ def list_patients(db: Session = Depends(get_db)):
)
)
.order_by(VitalReading.timestamp.desc())
- .first()
+ .limit(100)
+ .all()
)
-
+
+ merged_vitals = None
+ if recent_readings:
+ merged_vitals = {
+ "heart_rate": None,
+ "spo2": None,
+ "systolic_bp": None,
+ "diastolic_bp": None,
+ "map_bp": None,
+ "respiratory_rate": None,
+ "temperature": None,
+ "temperature2": None,
+ "temperature_diff": None,
+ "timestamp": recent_readings[0].timestamp.isoformat() if recent_readings[0].timestamp else None,
+ }
+ for r in reversed(recent_readings):
+ if r.heart_rate is not None: merged_vitals["heart_rate"] = r.heart_rate
+ if r.spo2 is not None: merged_vitals["spo2"] = r.spo2
+ 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
+ if r.respiratory_rate is not None: merged_vitals["respiratory_rate"] = r.respiratory_rate
+ 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
+
+ # Backfill any parameters (like SpO2 or NIBP) that were measured earlier
+ 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
+ )
+ if merged_vitals["spo2"] is None:
+ r_spo2 = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_spo2: merged_vitals["spo2"] = r_spo2[0]
+ if merged_vitals["heart_rate"] is None:
+ r_hr = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_hr: merged_vitals["heart_rate"] = r_hr[0]
+ if merged_vitals["systolic_bp"] is None:
+ r_bp = db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp).filter(patient_filters, VitalReading.systolic_bp != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_bp:
+ merged_vitals["systolic_bp"] = r_bp[0]
+ merged_vitals["diastolic_bp"] = r_bp[1]
+ merged_vitals["map_bp"] = r_bp[2]
+ if merged_vitals["respiratory_rate"] is None:
+ r_rr = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_rr: merged_vitals["respiratory_rate"] = r_rr[0]
+ if merged_vitals["temperature"] is None:
+ r_temp = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_temp: merged_vitals["temperature"] = r_temp[0]
+
device = db.query(Device).filter(Device.device_id == p.device_id).first() if p.device_id else None
results.append({
@@ -191,18 +243,7 @@ def list_patients(db: Session = Depends(get_db)):
"device_id": p.device_id,
"device_status": device.status if device else "unassigned",
"device_ip": device.ip_address if device else None,
- "latest_vitals": {
- "heart_rate": latest_reading.heart_rate if latest_reading else None,
- "spo2": latest_reading.spo2 if latest_reading else None,
- "systolic_bp": latest_reading.systolic_bp if latest_reading else None,
- "diastolic_bp": latest_reading.diastolic_bp if latest_reading else None,
- "map_bp": latest_reading.map_bp if latest_reading else None,
- "respiratory_rate": latest_reading.respiratory_rate if latest_reading else None,
- "temperature": latest_reading.temperature if latest_reading else None,
- "temperature2": getattr(latest_reading, 'temperature2', None) if latest_reading else None,
- "temperature_diff": getattr(latest_reading, 'temperature_diff', None) if latest_reading else None,
- "timestamp": latest_reading.timestamp.isoformat() if (latest_reading and latest_reading.timestamp) else None,
- } if latest_reading else None
+ "latest_vitals": merged_vitals
})
return results
@@ -250,6 +291,8 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
})
except Exception:
pass
+ from api_client import sync_patient_assignment_to_cloud
+ asyncio.create_task(sync_patient_assignment_to_cloud({"mrn": "UNKNOWN", "name": "Unassigned Patient"}, device_id))
return {
"status": "success",
"message": f"Device {device_id} unassigned successfully"
@@ -271,6 +314,7 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
gender=req.gender,
dob=req.dob,
bed_id=req.bed_id,
+ patient_type=req.patient_type,
device_id=device_id
)
db.add(patient)
@@ -285,6 +329,8 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
patient.dob = req.dob
if req.bed_id is not None:
patient.bed_id = req.bed_id
+ if req.patient_type is not None:
+ patient.patient_type = req.patient_type
patient.device_id = device_id
# 3. Associate recent readings for this device that were marked "UNKNOWN"
@@ -304,6 +350,7 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
"gender": patient.gender,
"dob": patient.dob,
"bed_id": patient.bed_id,
+ "patient_type": patient.patient_type,
}
set_assigned_patient(device_id, p_data)
@@ -321,10 +368,14 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
"patient_dob": patient.dob,
"dob": patient.dob,
"bed_id": patient.bed_id,
+ "patient_type": patient.patient_type,
})
except Exception as e:
logger.debug(f"Broadcast of patient_assigned failed: {e}")
+ from api_client import sync_patient_assignment_to_cloud
+ asyncio.create_task(sync_patient_assignment_to_cloud(p_data, device_id))
+
return {
"status": "success",
"message": f"Patient {patient.name} ({mrn}) assigned to device {device_id}",
@@ -460,6 +511,7 @@ def get_network_info():
primary_ip = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.settimeout(0.5)
s.connect(('8.8.8.8', 80))
primary_ip = s.getsockname()[0]
s.close()
@@ -578,6 +630,7 @@ async def push_vitals(vitals: NormalizedVitals, api_key: Optional[str] = None, d
vitals_dict["dob"] = vitals.patient_dob or (patient.dob if patient else None)
vitals_dict["patient_dob"] = vitals_dict["dob"]
vitals_dict["bed_id"] = vitals.bed_id or (patient.bed_id if patient else None)
+ vitals_dict["patient_type"] = getattr(vitals, "patient_type", None)
await ws_manager.broadcast_vitals(vitals_dict)
diff --git a/fiveparaminte-main/schemas.py b/fiveparaminte-main/schemas.py
index 0cd2556..3b76183 100644
--- a/fiveparaminte-main/schemas.py
+++ b/fiveparaminte-main/schemas.py
@@ -25,6 +25,7 @@ class NormalizedVitals(BaseModel):
patient_gender: Optional[str] = None # PID-8: M / F / U
patient_dob: Optional[str] = None # PID-7: YYYYMMDD
bed_id: Optional[str] = None # PV1-3: bed / room ID
+ patient_type: Optional[str] = None # Patient category: Adult / Pediatric / Neonatal
class DeviceStatus(BaseModel):
device_id: str
diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html
index 35e1d46..ff53a1f 100644
--- a/fiveparaminte-main/static/dashboard.html
+++ b/fiveparaminte-main/static/dashboard.html
@@ -2192,10 +2192,10 @@
🏥 CONTEC CMS7000PLUS
- BED 01
- ADULT
- SHAHIL
- MRN: 09091102
+ BED --
+ ADULT
+ --
+ MRN: --
PACE: NO
● TELEMETRY NORMAL
@@ -2592,26 +2592,30 @@
};
if (!dev) return res;
- const pMrn = String(dev?.mrn || dev?.patient_id || '').toLowerCase().trim();
- const pName = String(dev?.patient_name || dev?.name || '').toLowerCase().trim();
+ let pMrn = String(dev?.mrn || dev?.patient_id || '').toLowerCase().trim();
+ if ((!pMrn || pMrn === 'unknown') && dev?.device_id) {
+ const pat = Object.values(patientRegistry).find(p => p.device_id === dev.device_id);
+ if (pat) pMrn = String(pat.mrn || pat.patient_id || '').toLowerCase().trim();
+ }
+ let pName = String(dev?.patient_name || dev?.name || '').toLowerCase().trim();
+ if ((!pName || pName === 'unknown') && dev?.device_id) {
+ const pat = Object.values(patientRegistry).find(p => p.device_id === dev.device_id);
+ if (pat) pName = String(pat.name || '').toLowerCase().trim();
+ }
const pDob = String(dev?.patient_dob || dev?.dob || '').toLowerCase().trim();
const pGender = String(dev?.patient_gender || dev?.gender || '').toLowerCase().trim();
// 1. MRN verification
if (targetMrn) {
const tm = targetMrn.toLowerCase().trim();
- if (pMrn && (pMrn === tm || pMrn.includes(tm) || tm.includes(pMrn))) {
+ const cleanTm = tm.replace(/^0+/, '');
+ const cleanMrn = pMrn.replace(/^0+/, '');
+ if (pMrn && (pMrn === tm || (cleanTm.length >= 2 && cleanTm === cleanMrn))) {
res.mrnMatched = true;
- } else {
- const numT = parseInt(tm, 10);
- const numMrn = parseInt(pMrn, 10);
- if (!isNaN(numT) && !isNaN(numMrn) && numT === numMrn && tm.length >= 3 && pMrn.length >= 3) {
- res.mrnMatched = true;
- }
}
}
- // 2. Name verification (supports "Shahil", "Shahil Kumar", "Kumar Shahil")
+ // 2. Name verification (supports exact, partial or token-based matching)
if (targetName) {
const tn = targetName.toLowerCase().trim();
const targetTokens = normalizeNameTokens(targetName);
@@ -2645,7 +2649,16 @@
}
}
- res.allMatched = res.mrnMatched && res.nameMatched && res.dobMatched && res.genderMatched;
+ // Match positively on MRN, or on Name (with DOB if supplied)
+ if (targetMrn && res.mrnMatched) {
+ res.allMatched = true;
+ } else if (targetName && res.nameMatched && (!targetDob || res.dobMatched)) {
+ res.allMatched = true;
+ } else if (!targetMrn && !targetName) {
+ res.allMatched = res.dobMatched && res.genderMatched;
+ } else {
+ res.allMatched = false;
+ }
return res;
}
@@ -2682,6 +2695,32 @@
bed_id: null
};
+ function clearMachineVitals() {
+ machVitals.heart_rate = null;
+ machVitals.spo2 = null;
+ machVitals.systolic_bp = null;
+ machVitals.diastolic_bp = null;
+ machVitals.map_bp = null;
+ machVitals.respiratory_rate = null;
+ machVitals.temperature = null;
+ machVitals.temperature2 = null;
+ machVitals.temperature_diff = null;
+ machVitals.device_id = null;
+ machVitals.patient_name = targetName || null;
+ machVitals.patient_id = targetMrn || null;
+ machVitals.bed_id = null;
+ machVitals.patient_type = null;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+
+ updateMachineDisplay();
+ }
+
let machSweepX = 0;
let machPrevY_ecg = null;
let machPrevY_spo2 = null;
@@ -2711,13 +2750,16 @@
window.addEventListener('resize', resizeMachineCanvases);
if (targetMrn) {
+ machVitals.patient_id = targetMrn;
const elMrn = document.getElementById('machMrnPill');
if (elMrn) elMrn.textContent = `MRN: ${targetMrn}`;
}
if (targetName) {
+ machVitals.patient_name = targetName;
const elName = document.getElementById('machPatientName');
if (elName) elName.textContent = targetName.toUpperCase();
}
+ updateMachineDisplay();
// Load real patient telemetry from DB
fetchMachineTelemetry();
@@ -3021,45 +3063,136 @@
// === Load Real Machine Telemetry from Database & API ===
async function fetchMachineTelemetry() {
try {
- const targetId = targetMrn || targetName || '09091102';
-
- // 1. Demographics from registered patient list
- const patResp = await fetch(`${API_BASE}/patients`);
- if (patResp.ok) {
- const patients = await patResp.json();
- const p = patients.find(pt =>
- (targetMrn && (pt.mrn === targetMrn || pt.patient_id === targetMrn)) ||
- (targetName && pt.name && pt.name.toLowerCase().includes(targetName.toLowerCase()))
- ) || (targetMrn ? null : patients[0]);
+ if (hasTargetFilter) {
+ // Strict Target Patient Mode: NEVER fallback to another patient or transmitting device!
+ let p = null;
+ const patResp = await fetch(`${API_BASE}/patients`);
+ if (patResp.ok) {
+ const patients = await patResp.json();
+ if (targetMrn) {
+ const cleanTm = targetMrn.toLowerCase().trim().replace(/^0+/, '');
+ p = patients.find(pt => {
+ const m = String(pt.mrn || pt.patient_id || '').toLowerCase().trim();
+ return m === targetMrn.toLowerCase().trim() || (cleanTm.length >= 2 && m.replace(/^0+/, '') === cleanTm);
+ });
+ } else if (targetName) {
+ p = patients.find(pt => pt.name && pt.name.toLowerCase().includes(targetName.toLowerCase()));
+ }
+ }
if (p) {
- machVitals.patient_name = p.name || targetName || 'SHAHIL';
- machVitals.patient_id = p.mrn || p.patient_id || targetMrn || '09091102';
- machVitals.bed_id = p.bed_id || '1';
- machVitals.device_id = p.device_id || 'CMS8500_192.168.100.106';
+ machVitals.patient_name = p.name || targetName || null;
+ machVitals.patient_id = p.mrn || p.patient_id || targetMrn || null;
+ machVitals.bed_id = p.bed_id || null;
+ machVitals.device_id = p.device_id || '';
const elName = document.getElementById('machPatientName');
- if (elName) elName.textContent = machVitals.patient_name.toUpperCase();
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
const elMrn = document.getElementById('machMrnPill');
- if (elMrn) elMrn.textContent = `MRN: ${machVitals.patient_id}`;
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
const elBed = document.getElementById('machBedPill');
- if (elBed) elBed.textContent = `BED ${machVitals.bed_id}`;
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
if (p.latest_vitals) {
applyVitalsToMachine(p.latest_vitals);
return;
}
}
+
+ // Query latest readings directly for target patient
+ const targetId = targetMrn || targetName;
+ if (targetId) {
+ const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
+ if (readResp.ok) {
+ const readings = await readResp.json();
+ if (readings && readings.length > 0) {
+ const merged = {};
+ for (let i = readings.length - 1; i >= 0; i--) {
+ const r = readings[i];
+ for (const k in r) {
+ if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
+ }
+ }
+ applyVitalsToMachine(merged);
+ return;
+ }
+ }
+ }
+
+ // If no readings or machine data exists for target patient, strictly clear all vitals to '--' / LEAD OFF / flat line
+ clearMachineVitals();
+ return;
}
- // 2. Fallback: Fetch latest single reading directly from DB (limit=1: NO FAKE DATA)
- const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=1`);
- if (readResp.ok) {
- const readings = await readResp.json();
- if (readings && readings.length > 0) {
- applyVitalsToMachine(readings[0]);
- } else {
- applyVitalsToMachine(null);
+ // General mode (no target filter):
+ // Determine target patient: active device patient
+ let targetId = null;
+ if (activeDeviceId && connectedDevices[activeDeviceId]) {
+ const activeDev = connectedDevices[activeDeviceId];
+ targetId = activeDev.patient_id || activeDev.mrn;
+ }
+
+ // 1. Demographics from registered patient list
+ const patResp = await fetch(`${API_BASE}/patients`);
+ if (patResp.ok) {
+ const patients = await patResp.json();
+ let p = null;
+ if (activeDeviceId) {
+ p = patients.find(pt => pt.device_id === activeDeviceId);
+ }
+ if (!p && patients.length > 0) {
+ // Find patient corresponding to a live transmitting device
+ p = patients.find(pt => pt.device_id && connectedDevices[pt.device_id]?._liveData) ||
+ patients.find(pt => pt.device_id && pt.device_id === activeDeviceId);
+ }
+
+ if (p) {
+ machVitals.patient_name = p.name || null;
+ machVitals.patient_id = p.mrn || p.patient_id || null;
+ machVitals.bed_id = p.bed_id || null;
+ machVitals.device_id = p.device_id || activeDeviceId || '';
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+
+ if (p.latest_vitals) {
+ applyVitalsToMachine(p.latest_vitals);
+ return;
+ }
+ } else if (activeDeviceId && connectedDevices[activeDeviceId]) {
+ const dev = connectedDevices[activeDeviceId];
+ if (dev.patient_name || dev.name) machVitals.patient_name = dev.patient_name || dev.name;
+ if (dev.mrn || dev.patient_id) machVitals.patient_id = dev.mrn || dev.patient_id;
+ if (dev.bed_id || dev.bedLabel) machVitals.bed_id = dev.bed_id || dev.bedLabel;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+ }
+ }
+
+ // 2. Fallback: Fetch latest readings directly from DB (merge up to 10 recent readings so all active parameters display)
+ if (targetId) {
+ const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
+ if (readResp.ok) {
+ const readings = await readResp.json();
+ if (readings && readings.length > 0) {
+ const merged = {};
+ for (let i = readings.length - 1; i >= 0; i--) {
+ const r = readings[i];
+ for (const k in r) {
+ if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
+ }
+ }
+ applyVitalsToMachine(merged);
+ }
}
}
} catch(e) {
@@ -3071,39 +3204,39 @@
if (!data) return;
// Heart Rate
- if (data.heart_rate !== undefined) {
- machVitals.heart_rate = (data.heart_rate != null && data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
+ if (data.heart_rate !== undefined && data.heart_rate !== null) {
+ machVitals.heart_rate = (data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
}
// SpO2
- if (data.spo2 !== undefined) {
- machVitals.spo2 = (data.spo2 != null && data.spo2 > 0) ? Math.round(data.spo2) : null;
+ if (data.spo2 !== undefined && data.spo2 !== null) {
+ machVitals.spo2 = (data.spo2 > 0) ? Math.round(data.spo2) : null;
}
// Respiration Rate
- if (data.respiratory_rate !== undefined) {
- machVitals.respiratory_rate = (data.respiratory_rate != null && data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
+ if (data.respiratory_rate !== undefined && data.respiratory_rate !== null) {
+ machVitals.respiratory_rate = (data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
}
// NIBP
- if (data.systolic_bp !== undefined) {
- machVitals.systolic_bp = (data.systolic_bp != null && data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
+ if (data.systolic_bp !== undefined && data.systolic_bp !== null) {
+ machVitals.systolic_bp = (data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
}
- if (data.diastolic_bp !== undefined) {
- machVitals.diastolic_bp = (data.diastolic_bp != null && data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
+ if (data.diastolic_bp !== undefined && data.diastolic_bp !== null) {
+ machVitals.diastolic_bp = (data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
}
- if (data.map_bp !== undefined) {
- machVitals.map_bp = (data.map_bp != null && data.map_bp > 0) ? Math.round(data.map_bp) : null;
+ if (data.map_bp !== undefined && data.map_bp !== null) {
+ machVitals.map_bp = (data.map_bp > 0) ? Math.round(data.map_bp) : null;
}
// Temperature 1 (T1)
- if (data.temperature !== undefined) {
- machVitals.temperature = (data.temperature != null && data.temperature > 0) ? Number(data.temperature) : null;
+ if (data.temperature !== undefined && data.temperature !== null) {
+ machVitals.temperature = (data.temperature > 0) ? Number(data.temperature) : null;
}
// Temperature 2 (T2)
- if (data.temperature2 !== undefined) {
- machVitals.temperature2 = (data.temperature2 != null && data.temperature2 > 0) ? Number(data.temperature2) : null;
+ if (data.temperature2 !== undefined && data.temperature2 !== null) {
+ machVitals.temperature2 = (data.temperature2 > 0) ? Number(data.temperature2) : null;
}
// Temperature Difference (TD)
@@ -3111,16 +3244,25 @@
machVitals.temperature_diff = Number(data.temperature_diff);
} else if (machVitals.temperature != null && machVitals.temperature2 != null) {
machVitals.temperature_diff = Math.round(Math.abs(machVitals.temperature - machVitals.temperature2) * 10) / 10;
- } else {
- machVitals.temperature_diff = null;
}
- if (data.patient_name && data.patient_name !== 'Unknown') machVitals.patient_name = data.patient_name;
- if (data.name && data.name !== 'Unknown') machVitals.patient_name = data.name;
- if (data.mrn && data.mrn !== 'UNKNOWN') machVitals.patient_id = data.mrn;
- if (data.patient_id && data.patient_id !== 'UNKNOWN') machVitals.patient_id = data.patient_id;
+ if (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') machVitals.patient_name = data.patient_name;
+ if (data.name && data.name !== 'Unknown' && data.name !== 'UNKNOWN') machVitals.patient_name = data.name;
+ if (data.mrn && data.mrn !== 'UNKNOWN' && data.mrn !== 'UNASSIGNED') machVitals.patient_id = data.mrn;
+ if (data.patient_id && data.patient_id !== 'UNKNOWN' && data.patient_id !== 'UNASSIGNED') machVitals.patient_id = data.patient_id;
if (data.bed_id) machVitals.bed_id = data.bed_id;
if (data.device_id) machVitals.device_id = data.device_id;
+ if (data.patient_type) machVitals.patient_type = data.patient_type;
+
+ // Ensure top machine pills reflect current patient directly from machine
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+ const elCat = document.getElementById('machCategoryPill');
+ if (elCat && machVitals.patient_type) elCat.textContent = machVitals.patient_type.toUpperCase();
updateMachineDisplay();
}
@@ -3638,6 +3780,22 @@
function selectDevice(devId) {
activeDeviceId = devId;
setViewMode('single');
+ if (devId && connectedDevices[devId]) {
+ const dev = connectedDevices[devId];
+ machVitals.patient_name = (dev.patient_name && dev.patient_name !== 'Unknown' && dev.patient_name !== 'UNKNOWN') ? dev.patient_name : (dev.name && dev.name !== 'Unknown' && dev.name !== 'UNKNOWN' ? dev.name : null);
+ machVitals.patient_id = (dev.patient_id && dev.patient_id !== 'UNKNOWN' && dev.patient_id !== 'UNASSIGNED') ? dev.patient_id : ((dev.mrn && dev.mrn !== 'UNKNOWN' && dev.mrn !== 'UNASSIGNED') ? dev.mrn : null);
+ machVitals.bed_id = dev.bed_id || dev.bedLabel || null;
+ machVitals.device_id = devId;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+
+ applyVitalsToMachine(dev);
+ }
}
function updateDeviceTabs() {
@@ -3865,6 +4023,22 @@
bed_id: data.bed_id,
bedLabel: data.bed_id || connectedDevices[devId]?.bedLabel
};
+ if (activeDeviceId === devId || !activeDeviceId) {
+ machVitals.patient_name = (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') ? data.patient_name : (data.name && data.name !== 'Unknown' && data.name !== 'UNKNOWN' ? data.name : null);
+ machVitals.patient_id = (data.mrn && data.mrn !== 'UNKNOWN' && data.mrn !== 'UNASSIGNED') ? data.mrn : ((data.patient_id && data.patient_id !== 'UNKNOWN' && data.patient_id !== 'UNASSIGNED') ? data.patient_id : null);
+ machVitals.bed_id = data.bed_id || null;
+ machVitals.device_id = devId;
+ if (data.patient_type) machVitals.patient_type = data.patient_type;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+ const elCat = document.getElementById('machCategoryPill');
+ if (elCat && machVitals.patient_type) elCat.textContent = machVitals.patient_type.toUpperCase();
+ }
updateDeviceTabs();
if (hasTargetFilter) {
if (matchesTargetPatient(connectedDevices[devId], devId)) {
@@ -3929,16 +4103,13 @@
};
// Real-time sync with Contec Machine Display (Strictly live telemetry, zero fake values)
- if (devId) {
- const isMatch = !hasTargetFilter || matchesTargetPatient(connectedDevices[devId], devId) ||
- (devId.includes('192.168.100.106') && (targetMrn === '09091102' || (targetName && targetName.toLowerCase().includes('shahil'))));
- if (isMatch) {
- applyVitalsToMachine(data);
- }
- }
-
const isTargetMatch = !hasTargetFilter || matchesTargetPatient(connectedDevices[devId], devId);
+ if (devId && isTargetMatch) {
+ if (!activeDeviceId) activeDeviceId = devId;
+ applyVitalsToMachine(connectedDevices[devId]);
+ }
+
if (isTargetMatch) {
if (!activeDeviceId || hasTargetFilter) {
activeDeviceId = devId;
@@ -3949,21 +4120,23 @@
if (isTargetMatch && hasTargetFilter) {
// Target patient matched — force single view and render immediately
- // (don't wait for fetchInitialDevicesAndPatients async race)
if (viewMode !== 'single') setViewMode('single');
renderSingleDevice(connectedDevices[devId]);
+ } else if (hasTargetFilter && !isTargetMatch) {
+ // Incoming data is for ANOTHER patient/device - do NOT show it on target patient view!
+ if (viewMode === 'single') {
+ renderSingleDevice(null);
+ }
} else if (viewMode === 'grid') {
renderMultiBedGrid();
} else if (viewMode === 'single') {
- if (isTargetMatch && activeDeviceId === devId) {
+ if (activeDeviceId === devId) {
renderSingleDevice(connectedDevices[devId]);
- } else if (hasTargetFilter && !activeDeviceId) {
- renderSingleDevice(null);
}
}
}
- function renderSingleDevice(data) {
+ function renderSingleDevice(dev) {
const waitingState = document.getElementById('singleTargetWaitingState');
const patientBanner = document.getElementById('singlePatientBanner');
const vitalsGrid = document.getElementById('singleVitalsGrid');
@@ -3972,67 +4145,52 @@
// Always ensure the ICU Live Monitor is visible in single view
if (icuMon) icuMon.style.display = 'flex';
- if (!data && hasTargetFilter) {
- data = {
- patient_name: targetName || machVitals.patient_name || 'Shahilfaah',
- name: targetName || machVitals.patient_name || 'Shahilfaah',
- patient_id: targetMrn || machVitals.patient_id || '09091102',
- mrn: targetMrn || machVitals.patient_id || '09091102',
- device_id: machVitals.device_id || 'CMS8500_192.168.100.106',
- bed_id: machVitals.bed_id || 'Bed 01',
- bedLabel: machVitals.bed_id || 'Bed 01',
- heart_rate: null,
- spo2: null,
- systolic_bp: null,
- diastolic_bp: null,
- map_bp: null,
- respiratory_rate: null,
- temperature: null
- };
- }
-
- if (hasTargetFilter && !isMinimal) {
- const isMatch = data && matchesTargetPatient(data, data.device_id);
- if (!isMatch) {
- if (waitingState) {
- waitingState.style.display = 'flex';
- const badge = document.getElementById('targetWaitingMrnBadge');
- if (badge) badge.innerHTML = getTargetPatientBadge();
+ let data = dev;
+ if (hasTargetFilter) {
+ if (data && !matchesTargetPatient(data, data.device_id)) {
+ data = null;
+ }
+ if (!data) {
+ const matchDev = Object.values(connectedDevices).find(d => matchesTargetPatient(d, d.device_id));
+ if (matchDev) {
+ data = matchDev;
}
- } else {
- if (waitingState) waitingState.style.display = 'none';
}
- } else {
- if (waitingState) waitingState.style.display = 'none';
}
+ if (hasTargetFilter && !data) {
+ // Target patient has NO connected machine or matching readings
+ clearMachineVitals();
+
+ // Clear standard vital cards
+ updateVitalCard('hr', null, 'heart_rate');
+ updateVitalCard('spo2', null, 'spo2');
+ updateVitalCard('bp', null, 'systolic_bp');
+ updateVitalCard('rr', null, 'respiratory_rate');
+ updateVitalCard('temp', null, 'temperature');
+ updateVitalCard('map', null, 'map_bp');
+
+ if (!isMinimal && waitingState) {
+ waitingState.style.display = 'flex';
+ const badge = document.getElementById('targetWaitingMrnBadge');
+ if (badge) badge.innerHTML = getTargetPatientBadge();
+ }
+ if (!isMinimal) {
+ if (patientBanner) patientBanner.style.display = 'none';
+ if (vitalsGrid) vitalsGrid.style.display = 'none';
+ }
+ return;
+ }
+
+ if (waitingState) waitingState.style.display = 'none';
if (!isMinimal) {
if (patientBanner) patientBanner.style.display = 'flex';
if (vitalsGrid) vitalsGrid.style.display = 'grid';
}
if (!data) return;
- // Sync with ICU Live Monitor
- if (data) {
- if (data.heart_rate != null) icuVitals.heart_rate = data.heart_rate;
- if (data.spo2 != null) icuVitals.spo2 = data.spo2;
- if (data.systolic_bp != null) icuVitals.systolic_bp = data.systolic_bp;
- if (data.diastolic_bp != null) icuVitals.diastolic_bp = data.diastolic_bp;
- if (data.map_bp != null) icuVitals.map_bp = data.map_bp;
- if (data.respiratory_rate != null) icuVitals.respiratory_rate = data.respiratory_rate;
- if (data.temperature != null) icuVitals.temperature = data.temperature;
- if (data.patient_name && data.patient_name !== 'Unknown') icuVitals.patient_name = data.patient_name;
- if (data.mrn || data.patient_id) icuVitals.patient_id = data.mrn || data.patient_id;
- if (data.device_id) icuVitals.device_id = data.device_id;
- if (data.bed_id || data.bedLabel) icuVitals.bed_id = data.bed_id || data.bedLabel;
-
- document.getElementById('icuPatientNameText').textContent = icuVitals.patient_name;
- document.getElementById('icuAvatar').textContent = icuVitals.patient_name.charAt(0).toUpperCase();
- document.getElementById('icuMrnTag').textContent = `MRN: ${icuVitals.patient_id}`;
- document.getElementById('icuBedTag').textContent = `BED: ${icuVitals.bed_id}`;
- document.getElementById('icuDevTag').textContent = `DEV: ${icuVitals.device_id}`;
- updateIcuMonitorReadouts();
- }
+ // Sync with Contec Machine Screen
+ applyVitalsToMachine(data);
// 1. Update Patient Banner
const hasAssigned = data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN' && data.patient_name !== 'Unassigned Patient';
@@ -4182,6 +4340,17 @@
};
closeAssignPatientModal();
+ if (activeDeviceId === device_id || !activeDeviceId) {
+ machVitals.patient_name = name;
+ machVitals.patient_id = mrn;
+ machVitals.bed_id = bed_id;
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = name ? name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = mrn ? `MRN: ${mrn}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = bed_id ? `BED ${bed_id}` : 'BED --';
+ }
updateDeviceTabs();
if (viewMode === 'grid') renderMultiBedGrid();
else if (viewMode === 'single' && activeDeviceId === device_id) {
@@ -4343,12 +4512,18 @@
const updateEl = document.getElementById(`update-${cardKey}`);
const cardEl = document.getElementById(`card-${cardKey}`);
+ if (!valueEl) return;
+
if (value === null || value === undefined) {
valueEl.innerHTML = '---';
valueEl.className = 'vital-value no-data';
- statusEl.textContent = 'No Data';
- statusEl.className = 'status-badge offline';
- cardEl.classList.remove('has-value');
+ if (statusEl) {
+ statusEl.textContent = 'No Data';
+ statusEl.className = 'status-badge offline';
+ }
+ if (cardEl) {
+ cardEl.classList.remove('has-value');
+ }
return;
}
diff --git a/main.py b/main.py
index 8683ee7..8e3d9c9 100644
--- a/main.py
+++ b/main.py
@@ -62,6 +62,22 @@ def _get_local_ips():
@asynccontextmanager
async def lifespan(app: FastAPI):
+ # Protect against Windows IOCP Proactor crashes (WinError 64 / 121 / 10054 on accept)
+ import sys
+ if sys.platform == "win32":
+ try:
+ loop = asyncio.get_running_loop()
+ def silence_windows_iocp_errors(loop, context):
+ msg = str(context.get("message", ""))
+ exc = context.get("exception")
+ winerr = getattr(exc, "winerror", None)
+ if winerr in (64, 121, 10054) or "Accept failed on a socket" in msg or "The specified network name is no longer available" in msg:
+ return
+ loop.default_exception_handler(context)
+ loop.set_exception_handler(silence_windows_iocp_errors)
+ except Exception:
+ pass
+
servers = []
# === Start Contec CMS7000PLUS TCP Servers ===
diff --git a/models.py b/models.py
index b03be40..153f87b 100644
--- a/models.py
+++ b/models.py
@@ -18,8 +18,9 @@ class Patient(Base):
name = Column(String, nullable=True) # full name from PID-5
gender = Column(String, nullable=True) # M/F/U from PID-8
dob = Column(String, nullable=True) # YYYYMMDD from PID-7
- bed_id = Column(String, nullable=True) # bed from PV1-3
- device_id = Column(String, nullable=True, index=True) # assigned machine / device_id
+ bed_id = Column(String, nullable=True) # bed from PV1-3
+ patient_type = Column(String, nullable=True) # Adult / Pediatric / Neonatal
+ device_id = Column(String, nullable=True, index=True) # assigned machine / device_id
class VitalReading(Base):
__tablename__ = "vital_readings"
diff --git a/routers/dashboard.py b/routers/dashboard.py
index 9b41a48..e9bd0e8 100644
--- a/routers/dashboard.py
+++ b/routers/dashboard.py
@@ -24,6 +24,7 @@ class PatientAssignRequest(BaseModel):
dob: Optional[str] = None
gender: Optional[str] = None
bed_id: Optional[str] = None
+ patient_type: Optional[str] = None
# === WebSocket Manager for real-time vitals push ===
class VitalsWebSocketManager:
@@ -164,8 +165,8 @@ def list_patients(db: Session = Depends(get_db)):
patients = db.query(Patient).filter(Patient.patient_id != "UNKNOWN", Patient.mrn != "UNKNOWN").all()
results = []
for p in patients:
- # Find latest vitals for this patient or this patient's assigned device
- latest_reading = (
+ # Find latest non-null vitals across recent readings for this patient or device
+ recent_readings = (
db.query(VitalReading)
.filter(
or_(
@@ -175,9 +176,60 @@ def list_patients(db: Session = Depends(get_db)):
)
)
.order_by(VitalReading.timestamp.desc())
- .first()
+ .limit(100)
+ .all()
)
-
+
+ merged_vitals = None
+ if recent_readings:
+ merged_vitals = {
+ "heart_rate": None,
+ "spo2": None,
+ "systolic_bp": None,
+ "diastolic_bp": None,
+ "map_bp": None,
+ "respiratory_rate": None,
+ "temperature": None,
+ "temperature2": None,
+ "temperature_diff": None,
+ "timestamp": recent_readings[0].timestamp.isoformat() if recent_readings[0].timestamp else None,
+ }
+ for r in reversed(recent_readings):
+ if r.heart_rate is not None: merged_vitals["heart_rate"] = r.heart_rate
+ if r.spo2 is not None: merged_vitals["spo2"] = r.spo2
+ 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
+ if r.respiratory_rate is not None: merged_vitals["respiratory_rate"] = r.respiratory_rate
+ 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
+
+ # Backfill any parameters (like SpO2 or NIBP) that were measured earlier
+ 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
+ )
+ if merged_vitals["spo2"] is None:
+ r_spo2 = db.query(VitalReading.spo2).filter(patient_filters, VitalReading.spo2 != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_spo2: merged_vitals["spo2"] = r_spo2[0]
+ if merged_vitals["heart_rate"] is None:
+ r_hr = db.query(VitalReading.heart_rate).filter(patient_filters, VitalReading.heart_rate != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_hr: merged_vitals["heart_rate"] = r_hr[0]
+ if merged_vitals["systolic_bp"] is None:
+ r_bp = db.query(VitalReading.systolic_bp, VitalReading.diastolic_bp, VitalReading.map_bp).filter(patient_filters, VitalReading.systolic_bp != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_bp:
+ merged_vitals["systolic_bp"] = r_bp[0]
+ merged_vitals["diastolic_bp"] = r_bp[1]
+ merged_vitals["map_bp"] = r_bp[2]
+ if merged_vitals["respiratory_rate"] is None:
+ r_rr = db.query(VitalReading.respiratory_rate).filter(patient_filters, VitalReading.respiratory_rate != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_rr: merged_vitals["respiratory_rate"] = r_rr[0]
+ if merged_vitals["temperature"] is None:
+ r_temp = db.query(VitalReading.temperature).filter(patient_filters, VitalReading.temperature != None).order_by(VitalReading.timestamp.desc()).first()
+ if r_temp: merged_vitals["temperature"] = r_temp[0]
+
device = db.query(Device).filter(Device.device_id == p.device_id).first() if p.device_id else None
results.append({
@@ -191,18 +243,7 @@ def list_patients(db: Session = Depends(get_db)):
"device_id": p.device_id,
"device_status": device.status if device else "unassigned",
"device_ip": device.ip_address if device else None,
- "latest_vitals": {
- "heart_rate": latest_reading.heart_rate if latest_reading else None,
- "spo2": latest_reading.spo2 if latest_reading else None,
- "systolic_bp": latest_reading.systolic_bp if latest_reading else None,
- "diastolic_bp": latest_reading.diastolic_bp if latest_reading else None,
- "map_bp": latest_reading.map_bp if latest_reading else None,
- "respiratory_rate": latest_reading.respiratory_rate if latest_reading else None,
- "temperature": latest_reading.temperature if latest_reading else None,
- "temperature2": getattr(latest_reading, 'temperature2', None) if latest_reading else None,
- "temperature_diff": getattr(latest_reading, 'temperature_diff', None) if latest_reading else None,
- "timestamp": latest_reading.timestamp.isoformat() if (latest_reading and latest_reading.timestamp) else None,
- } if latest_reading else None
+ "latest_vitals": merged_vitals
})
return results
@@ -250,6 +291,8 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
})
except Exception:
pass
+ from api_client import sync_patient_assignment_to_cloud
+ asyncio.create_task(sync_patient_assignment_to_cloud({"mrn": "UNKNOWN", "name": "Unassigned Patient"}, device_id))
return {
"status": "success",
"message": f"Device {device_id} unassigned successfully"
@@ -271,6 +314,7 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
gender=req.gender,
dob=req.dob,
bed_id=req.bed_id,
+ patient_type=req.patient_type,
device_id=device_id
)
db.add(patient)
@@ -285,6 +329,8 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
patient.dob = req.dob
if req.bed_id is not None:
patient.bed_id = req.bed_id
+ if req.patient_type is not None:
+ patient.patient_type = req.patient_type
patient.device_id = device_id
# 3. Associate recent readings for this device that were marked "UNKNOWN"
@@ -304,6 +350,7 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
"gender": patient.gender,
"dob": patient.dob,
"bed_id": patient.bed_id,
+ "patient_type": patient.patient_type,
}
set_assigned_patient(device_id, p_data)
@@ -321,10 +368,14 @@ async def assign_patient(req: PatientAssignRequest, db: Session = Depends(get_db
"patient_dob": patient.dob,
"dob": patient.dob,
"bed_id": patient.bed_id,
+ "patient_type": patient.patient_type,
})
except Exception as e:
logger.debug(f"Broadcast of patient_assigned failed: {e}")
+ from api_client import sync_patient_assignment_to_cloud
+ asyncio.create_task(sync_patient_assignment_to_cloud(p_data, device_id))
+
return {
"status": "success",
"message": f"Patient {patient.name} ({mrn}) assigned to device {device_id}",
@@ -460,6 +511,7 @@ def get_network_info():
primary_ip = None
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ s.settimeout(0.5)
s.connect(('8.8.8.8', 80))
primary_ip = s.getsockname()[0]
s.close()
@@ -578,6 +630,7 @@ async def push_vitals(vitals: NormalizedVitals, api_key: Optional[str] = None, d
vitals_dict["dob"] = vitals.patient_dob or (patient.dob if patient else None)
vitals_dict["patient_dob"] = vitals_dict["dob"]
vitals_dict["bed_id"] = vitals.bed_id or (patient.bed_id if patient else None)
+ vitals_dict["patient_type"] = getattr(vitals, "patient_type", None)
await ws_manager.broadcast_vitals(vitals_dict)
diff --git a/schemas.py b/schemas.py
index 0cd2556..3b76183 100644
--- a/schemas.py
+++ b/schemas.py
@@ -25,6 +25,7 @@ class NormalizedVitals(BaseModel):
patient_gender: Optional[str] = None # PID-8: M / F / U
patient_dob: Optional[str] = None # PID-7: YYYYMMDD
bed_id: Optional[str] = None # PV1-3: bed / room ID
+ patient_type: Optional[str] = None # Patient category: Adult / Pediatric / Neonatal
class DeviceStatus(BaseModel):
device_id: str
diff --git a/static/dashboard.html b/static/dashboard.html
index 35e1d46..ff53a1f 100644
--- a/static/dashboard.html
+++ b/static/dashboard.html
@@ -2192,10 +2192,10 @@
🏥 CONTEC CMS7000PLUS
- BED 01
- ADULT
- SHAHIL
- MRN: 09091102
+ BED --
+ ADULT
+ --
+ MRN: --
PACE: NO
● TELEMETRY NORMAL
@@ -2592,26 +2592,30 @@
};
if (!dev) return res;
- const pMrn = String(dev?.mrn || dev?.patient_id || '').toLowerCase().trim();
- const pName = String(dev?.patient_name || dev?.name || '').toLowerCase().trim();
+ let pMrn = String(dev?.mrn || dev?.patient_id || '').toLowerCase().trim();
+ if ((!pMrn || pMrn === 'unknown') && dev?.device_id) {
+ const pat = Object.values(patientRegistry).find(p => p.device_id === dev.device_id);
+ if (pat) pMrn = String(pat.mrn || pat.patient_id || '').toLowerCase().trim();
+ }
+ let pName = String(dev?.patient_name || dev?.name || '').toLowerCase().trim();
+ if ((!pName || pName === 'unknown') && dev?.device_id) {
+ const pat = Object.values(patientRegistry).find(p => p.device_id === dev.device_id);
+ if (pat) pName = String(pat.name || '').toLowerCase().trim();
+ }
const pDob = String(dev?.patient_dob || dev?.dob || '').toLowerCase().trim();
const pGender = String(dev?.patient_gender || dev?.gender || '').toLowerCase().trim();
// 1. MRN verification
if (targetMrn) {
const tm = targetMrn.toLowerCase().trim();
- if (pMrn && (pMrn === tm || pMrn.includes(tm) || tm.includes(pMrn))) {
+ const cleanTm = tm.replace(/^0+/, '');
+ const cleanMrn = pMrn.replace(/^0+/, '');
+ if (pMrn && (pMrn === tm || (cleanTm.length >= 2 && cleanTm === cleanMrn))) {
res.mrnMatched = true;
- } else {
- const numT = parseInt(tm, 10);
- const numMrn = parseInt(pMrn, 10);
- if (!isNaN(numT) && !isNaN(numMrn) && numT === numMrn && tm.length >= 3 && pMrn.length >= 3) {
- res.mrnMatched = true;
- }
}
}
- // 2. Name verification (supports "Shahil", "Shahil Kumar", "Kumar Shahil")
+ // 2. Name verification (supports exact, partial or token-based matching)
if (targetName) {
const tn = targetName.toLowerCase().trim();
const targetTokens = normalizeNameTokens(targetName);
@@ -2645,7 +2649,16 @@
}
}
- res.allMatched = res.mrnMatched && res.nameMatched && res.dobMatched && res.genderMatched;
+ // Match positively on MRN, or on Name (with DOB if supplied)
+ if (targetMrn && res.mrnMatched) {
+ res.allMatched = true;
+ } else if (targetName && res.nameMatched && (!targetDob || res.dobMatched)) {
+ res.allMatched = true;
+ } else if (!targetMrn && !targetName) {
+ res.allMatched = res.dobMatched && res.genderMatched;
+ } else {
+ res.allMatched = false;
+ }
return res;
}
@@ -2682,6 +2695,32 @@
bed_id: null
};
+ function clearMachineVitals() {
+ machVitals.heart_rate = null;
+ machVitals.spo2 = null;
+ machVitals.systolic_bp = null;
+ machVitals.diastolic_bp = null;
+ machVitals.map_bp = null;
+ machVitals.respiratory_rate = null;
+ machVitals.temperature = null;
+ machVitals.temperature2 = null;
+ machVitals.temperature_diff = null;
+ machVitals.device_id = null;
+ machVitals.patient_name = targetName || null;
+ machVitals.patient_id = targetMrn || null;
+ machVitals.bed_id = null;
+ machVitals.patient_type = null;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+
+ updateMachineDisplay();
+ }
+
let machSweepX = 0;
let machPrevY_ecg = null;
let machPrevY_spo2 = null;
@@ -2711,13 +2750,16 @@
window.addEventListener('resize', resizeMachineCanvases);
if (targetMrn) {
+ machVitals.patient_id = targetMrn;
const elMrn = document.getElementById('machMrnPill');
if (elMrn) elMrn.textContent = `MRN: ${targetMrn}`;
}
if (targetName) {
+ machVitals.patient_name = targetName;
const elName = document.getElementById('machPatientName');
if (elName) elName.textContent = targetName.toUpperCase();
}
+ updateMachineDisplay();
// Load real patient telemetry from DB
fetchMachineTelemetry();
@@ -3021,45 +3063,136 @@
// === Load Real Machine Telemetry from Database & API ===
async function fetchMachineTelemetry() {
try {
- const targetId = targetMrn || targetName || '09091102';
-
- // 1. Demographics from registered patient list
- const patResp = await fetch(`${API_BASE}/patients`);
- if (patResp.ok) {
- const patients = await patResp.json();
- const p = patients.find(pt =>
- (targetMrn && (pt.mrn === targetMrn || pt.patient_id === targetMrn)) ||
- (targetName && pt.name && pt.name.toLowerCase().includes(targetName.toLowerCase()))
- ) || (targetMrn ? null : patients[0]);
+ if (hasTargetFilter) {
+ // Strict Target Patient Mode: NEVER fallback to another patient or transmitting device!
+ let p = null;
+ const patResp = await fetch(`${API_BASE}/patients`);
+ if (patResp.ok) {
+ const patients = await patResp.json();
+ if (targetMrn) {
+ const cleanTm = targetMrn.toLowerCase().trim().replace(/^0+/, '');
+ p = patients.find(pt => {
+ const m = String(pt.mrn || pt.patient_id || '').toLowerCase().trim();
+ return m === targetMrn.toLowerCase().trim() || (cleanTm.length >= 2 && m.replace(/^0+/, '') === cleanTm);
+ });
+ } else if (targetName) {
+ p = patients.find(pt => pt.name && pt.name.toLowerCase().includes(targetName.toLowerCase()));
+ }
+ }
if (p) {
- machVitals.patient_name = p.name || targetName || 'SHAHIL';
- machVitals.patient_id = p.mrn || p.patient_id || targetMrn || '09091102';
- machVitals.bed_id = p.bed_id || '1';
- machVitals.device_id = p.device_id || 'CMS8500_192.168.100.106';
+ machVitals.patient_name = p.name || targetName || null;
+ machVitals.patient_id = p.mrn || p.patient_id || targetMrn || null;
+ machVitals.bed_id = p.bed_id || null;
+ machVitals.device_id = p.device_id || '';
const elName = document.getElementById('machPatientName');
- if (elName) elName.textContent = machVitals.patient_name.toUpperCase();
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
const elMrn = document.getElementById('machMrnPill');
- if (elMrn) elMrn.textContent = `MRN: ${machVitals.patient_id}`;
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
const elBed = document.getElementById('machBedPill');
- if (elBed) elBed.textContent = `BED ${machVitals.bed_id}`;
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
if (p.latest_vitals) {
applyVitalsToMachine(p.latest_vitals);
return;
}
}
+
+ // Query latest readings directly for target patient
+ const targetId = targetMrn || targetName;
+ if (targetId) {
+ const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
+ if (readResp.ok) {
+ const readings = await readResp.json();
+ if (readings && readings.length > 0) {
+ const merged = {};
+ for (let i = readings.length - 1; i >= 0; i--) {
+ const r = readings[i];
+ for (const k in r) {
+ if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
+ }
+ }
+ applyVitalsToMachine(merged);
+ return;
+ }
+ }
+ }
+
+ // If no readings or machine data exists for target patient, strictly clear all vitals to '--' / LEAD OFF / flat line
+ clearMachineVitals();
+ return;
}
- // 2. Fallback: Fetch latest single reading directly from DB (limit=1: NO FAKE DATA)
- const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=1`);
- if (readResp.ok) {
- const readings = await readResp.json();
- if (readings && readings.length > 0) {
- applyVitalsToMachine(readings[0]);
- } else {
- applyVitalsToMachine(null);
+ // General mode (no target filter):
+ // Determine target patient: active device patient
+ let targetId = null;
+ if (activeDeviceId && connectedDevices[activeDeviceId]) {
+ const activeDev = connectedDevices[activeDeviceId];
+ targetId = activeDev.patient_id || activeDev.mrn;
+ }
+
+ // 1. Demographics from registered patient list
+ const patResp = await fetch(`${API_BASE}/patients`);
+ if (patResp.ok) {
+ const patients = await patResp.json();
+ let p = null;
+ if (activeDeviceId) {
+ p = patients.find(pt => pt.device_id === activeDeviceId);
+ }
+ if (!p && patients.length > 0) {
+ // Find patient corresponding to a live transmitting device
+ p = patients.find(pt => pt.device_id && connectedDevices[pt.device_id]?._liveData) ||
+ patients.find(pt => pt.device_id && pt.device_id === activeDeviceId);
+ }
+
+ if (p) {
+ machVitals.patient_name = p.name || null;
+ machVitals.patient_id = p.mrn || p.patient_id || null;
+ machVitals.bed_id = p.bed_id || null;
+ machVitals.device_id = p.device_id || activeDeviceId || '';
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+
+ if (p.latest_vitals) {
+ applyVitalsToMachine(p.latest_vitals);
+ return;
+ }
+ } else if (activeDeviceId && connectedDevices[activeDeviceId]) {
+ const dev = connectedDevices[activeDeviceId];
+ if (dev.patient_name || dev.name) machVitals.patient_name = dev.patient_name || dev.name;
+ if (dev.mrn || dev.patient_id) machVitals.patient_id = dev.mrn || dev.patient_id;
+ if (dev.bed_id || dev.bedLabel) machVitals.bed_id = dev.bed_id || dev.bedLabel;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+ }
+ }
+
+ // 2. Fallback: Fetch latest readings directly from DB (merge up to 10 recent readings so all active parameters display)
+ if (targetId) {
+ const readResp = await fetch(`${API_BASE}/latest-readings?mrn=${encodeURIComponent(targetId)}&limit=10`);
+ if (readResp.ok) {
+ const readings = await readResp.json();
+ if (readings && readings.length > 0) {
+ const merged = {};
+ for (let i = readings.length - 1; i >= 0; i--) {
+ const r = readings[i];
+ for (const k in r) {
+ if (r[k] !== null && r[k] !== undefined) merged[k] = r[k];
+ }
+ }
+ applyVitalsToMachine(merged);
+ }
}
}
} catch(e) {
@@ -3071,39 +3204,39 @@
if (!data) return;
// Heart Rate
- if (data.heart_rate !== undefined) {
- machVitals.heart_rate = (data.heart_rate != null && data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
+ if (data.heart_rate !== undefined && data.heart_rate !== null) {
+ machVitals.heart_rate = (data.heart_rate > 0) ? Math.round(data.heart_rate) : null;
}
// SpO2
- if (data.spo2 !== undefined) {
- machVitals.spo2 = (data.spo2 != null && data.spo2 > 0) ? Math.round(data.spo2) : null;
+ if (data.spo2 !== undefined && data.spo2 !== null) {
+ machVitals.spo2 = (data.spo2 > 0) ? Math.round(data.spo2) : null;
}
// Respiration Rate
- if (data.respiratory_rate !== undefined) {
- machVitals.respiratory_rate = (data.respiratory_rate != null && data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
+ if (data.respiratory_rate !== undefined && data.respiratory_rate !== null) {
+ machVitals.respiratory_rate = (data.respiratory_rate > 0) ? Math.round(data.respiratory_rate) : null;
}
// NIBP
- if (data.systolic_bp !== undefined) {
- machVitals.systolic_bp = (data.systolic_bp != null && data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
+ if (data.systolic_bp !== undefined && data.systolic_bp !== null) {
+ machVitals.systolic_bp = (data.systolic_bp > 0) ? Math.round(data.systolic_bp) : null;
}
- if (data.diastolic_bp !== undefined) {
- machVitals.diastolic_bp = (data.diastolic_bp != null && data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
+ if (data.diastolic_bp !== undefined && data.diastolic_bp !== null) {
+ machVitals.diastolic_bp = (data.diastolic_bp > 0) ? Math.round(data.diastolic_bp) : null;
}
- if (data.map_bp !== undefined) {
- machVitals.map_bp = (data.map_bp != null && data.map_bp > 0) ? Math.round(data.map_bp) : null;
+ if (data.map_bp !== undefined && data.map_bp !== null) {
+ machVitals.map_bp = (data.map_bp > 0) ? Math.round(data.map_bp) : null;
}
// Temperature 1 (T1)
- if (data.temperature !== undefined) {
- machVitals.temperature = (data.temperature != null && data.temperature > 0) ? Number(data.temperature) : null;
+ if (data.temperature !== undefined && data.temperature !== null) {
+ machVitals.temperature = (data.temperature > 0) ? Number(data.temperature) : null;
}
// Temperature 2 (T2)
- if (data.temperature2 !== undefined) {
- machVitals.temperature2 = (data.temperature2 != null && data.temperature2 > 0) ? Number(data.temperature2) : null;
+ if (data.temperature2 !== undefined && data.temperature2 !== null) {
+ machVitals.temperature2 = (data.temperature2 > 0) ? Number(data.temperature2) : null;
}
// Temperature Difference (TD)
@@ -3111,16 +3244,25 @@
machVitals.temperature_diff = Number(data.temperature_diff);
} else if (machVitals.temperature != null && machVitals.temperature2 != null) {
machVitals.temperature_diff = Math.round(Math.abs(machVitals.temperature - machVitals.temperature2) * 10) / 10;
- } else {
- machVitals.temperature_diff = null;
}
- if (data.patient_name && data.patient_name !== 'Unknown') machVitals.patient_name = data.patient_name;
- if (data.name && data.name !== 'Unknown') machVitals.patient_name = data.name;
- if (data.mrn && data.mrn !== 'UNKNOWN') machVitals.patient_id = data.mrn;
- if (data.patient_id && data.patient_id !== 'UNKNOWN') machVitals.patient_id = data.patient_id;
+ if (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') machVitals.patient_name = data.patient_name;
+ if (data.name && data.name !== 'Unknown' && data.name !== 'UNKNOWN') machVitals.patient_name = data.name;
+ if (data.mrn && data.mrn !== 'UNKNOWN' && data.mrn !== 'UNASSIGNED') machVitals.patient_id = data.mrn;
+ if (data.patient_id && data.patient_id !== 'UNKNOWN' && data.patient_id !== 'UNASSIGNED') machVitals.patient_id = data.patient_id;
if (data.bed_id) machVitals.bed_id = data.bed_id;
if (data.device_id) machVitals.device_id = data.device_id;
+ if (data.patient_type) machVitals.patient_type = data.patient_type;
+
+ // Ensure top machine pills reflect current patient directly from machine
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+ const elCat = document.getElementById('machCategoryPill');
+ if (elCat && machVitals.patient_type) elCat.textContent = machVitals.patient_type.toUpperCase();
updateMachineDisplay();
}
@@ -3638,6 +3780,22 @@
function selectDevice(devId) {
activeDeviceId = devId;
setViewMode('single');
+ if (devId && connectedDevices[devId]) {
+ const dev = connectedDevices[devId];
+ machVitals.patient_name = (dev.patient_name && dev.patient_name !== 'Unknown' && dev.patient_name !== 'UNKNOWN') ? dev.patient_name : (dev.name && dev.name !== 'Unknown' && dev.name !== 'UNKNOWN' ? dev.name : null);
+ machVitals.patient_id = (dev.patient_id && dev.patient_id !== 'UNKNOWN' && dev.patient_id !== 'UNASSIGNED') ? dev.patient_id : ((dev.mrn && dev.mrn !== 'UNKNOWN' && dev.mrn !== 'UNASSIGNED') ? dev.mrn : null);
+ machVitals.bed_id = dev.bed_id || dev.bedLabel || null;
+ machVitals.device_id = devId;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+
+ applyVitalsToMachine(dev);
+ }
}
function updateDeviceTabs() {
@@ -3865,6 +4023,22 @@
bed_id: data.bed_id,
bedLabel: data.bed_id || connectedDevices[devId]?.bedLabel
};
+ if (activeDeviceId === devId || !activeDeviceId) {
+ machVitals.patient_name = (data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN') ? data.patient_name : (data.name && data.name !== 'Unknown' && data.name !== 'UNKNOWN' ? data.name : null);
+ machVitals.patient_id = (data.mrn && data.mrn !== 'UNKNOWN' && data.mrn !== 'UNASSIGNED') ? data.mrn : ((data.patient_id && data.patient_id !== 'UNKNOWN' && data.patient_id !== 'UNASSIGNED') ? data.patient_id : null);
+ machVitals.bed_id = data.bed_id || null;
+ machVitals.device_id = devId;
+ if (data.patient_type) machVitals.patient_type = data.patient_type;
+
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = machVitals.patient_name ? machVitals.patient_name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = machVitals.patient_id ? `MRN: ${machVitals.patient_id}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = machVitals.bed_id ? `BED ${machVitals.bed_id}` : 'BED --';
+ const elCat = document.getElementById('machCategoryPill');
+ if (elCat && machVitals.patient_type) elCat.textContent = machVitals.patient_type.toUpperCase();
+ }
updateDeviceTabs();
if (hasTargetFilter) {
if (matchesTargetPatient(connectedDevices[devId], devId)) {
@@ -3929,16 +4103,13 @@
};
// Real-time sync with Contec Machine Display (Strictly live telemetry, zero fake values)
- if (devId) {
- const isMatch = !hasTargetFilter || matchesTargetPatient(connectedDevices[devId], devId) ||
- (devId.includes('192.168.100.106') && (targetMrn === '09091102' || (targetName && targetName.toLowerCase().includes('shahil'))));
- if (isMatch) {
- applyVitalsToMachine(data);
- }
- }
-
const isTargetMatch = !hasTargetFilter || matchesTargetPatient(connectedDevices[devId], devId);
+ if (devId && isTargetMatch) {
+ if (!activeDeviceId) activeDeviceId = devId;
+ applyVitalsToMachine(connectedDevices[devId]);
+ }
+
if (isTargetMatch) {
if (!activeDeviceId || hasTargetFilter) {
activeDeviceId = devId;
@@ -3949,21 +4120,23 @@
if (isTargetMatch && hasTargetFilter) {
// Target patient matched — force single view and render immediately
- // (don't wait for fetchInitialDevicesAndPatients async race)
if (viewMode !== 'single') setViewMode('single');
renderSingleDevice(connectedDevices[devId]);
+ } else if (hasTargetFilter && !isTargetMatch) {
+ // Incoming data is for ANOTHER patient/device - do NOT show it on target patient view!
+ if (viewMode === 'single') {
+ renderSingleDevice(null);
+ }
} else if (viewMode === 'grid') {
renderMultiBedGrid();
} else if (viewMode === 'single') {
- if (isTargetMatch && activeDeviceId === devId) {
+ if (activeDeviceId === devId) {
renderSingleDevice(connectedDevices[devId]);
- } else if (hasTargetFilter && !activeDeviceId) {
- renderSingleDevice(null);
}
}
}
- function renderSingleDevice(data) {
+ function renderSingleDevice(dev) {
const waitingState = document.getElementById('singleTargetWaitingState');
const patientBanner = document.getElementById('singlePatientBanner');
const vitalsGrid = document.getElementById('singleVitalsGrid');
@@ -3972,67 +4145,52 @@
// Always ensure the ICU Live Monitor is visible in single view
if (icuMon) icuMon.style.display = 'flex';
- if (!data && hasTargetFilter) {
- data = {
- patient_name: targetName || machVitals.patient_name || 'Shahilfaah',
- name: targetName || machVitals.patient_name || 'Shahilfaah',
- patient_id: targetMrn || machVitals.patient_id || '09091102',
- mrn: targetMrn || machVitals.patient_id || '09091102',
- device_id: machVitals.device_id || 'CMS8500_192.168.100.106',
- bed_id: machVitals.bed_id || 'Bed 01',
- bedLabel: machVitals.bed_id || 'Bed 01',
- heart_rate: null,
- spo2: null,
- systolic_bp: null,
- diastolic_bp: null,
- map_bp: null,
- respiratory_rate: null,
- temperature: null
- };
- }
-
- if (hasTargetFilter && !isMinimal) {
- const isMatch = data && matchesTargetPatient(data, data.device_id);
- if (!isMatch) {
- if (waitingState) {
- waitingState.style.display = 'flex';
- const badge = document.getElementById('targetWaitingMrnBadge');
- if (badge) badge.innerHTML = getTargetPatientBadge();
+ let data = dev;
+ if (hasTargetFilter) {
+ if (data && !matchesTargetPatient(data, data.device_id)) {
+ data = null;
+ }
+ if (!data) {
+ const matchDev = Object.values(connectedDevices).find(d => matchesTargetPatient(d, d.device_id));
+ if (matchDev) {
+ data = matchDev;
}
- } else {
- if (waitingState) waitingState.style.display = 'none';
}
- } else {
- if (waitingState) waitingState.style.display = 'none';
}
+ if (hasTargetFilter && !data) {
+ // Target patient has NO connected machine or matching readings
+ clearMachineVitals();
+
+ // Clear standard vital cards
+ updateVitalCard('hr', null, 'heart_rate');
+ updateVitalCard('spo2', null, 'spo2');
+ updateVitalCard('bp', null, 'systolic_bp');
+ updateVitalCard('rr', null, 'respiratory_rate');
+ updateVitalCard('temp', null, 'temperature');
+ updateVitalCard('map', null, 'map_bp');
+
+ if (!isMinimal && waitingState) {
+ waitingState.style.display = 'flex';
+ const badge = document.getElementById('targetWaitingMrnBadge');
+ if (badge) badge.innerHTML = getTargetPatientBadge();
+ }
+ if (!isMinimal) {
+ if (patientBanner) patientBanner.style.display = 'none';
+ if (vitalsGrid) vitalsGrid.style.display = 'none';
+ }
+ return;
+ }
+
+ if (waitingState) waitingState.style.display = 'none';
if (!isMinimal) {
if (patientBanner) patientBanner.style.display = 'flex';
if (vitalsGrid) vitalsGrid.style.display = 'grid';
}
if (!data) return;
- // Sync with ICU Live Monitor
- if (data) {
- if (data.heart_rate != null) icuVitals.heart_rate = data.heart_rate;
- if (data.spo2 != null) icuVitals.spo2 = data.spo2;
- if (data.systolic_bp != null) icuVitals.systolic_bp = data.systolic_bp;
- if (data.diastolic_bp != null) icuVitals.diastolic_bp = data.diastolic_bp;
- if (data.map_bp != null) icuVitals.map_bp = data.map_bp;
- if (data.respiratory_rate != null) icuVitals.respiratory_rate = data.respiratory_rate;
- if (data.temperature != null) icuVitals.temperature = data.temperature;
- if (data.patient_name && data.patient_name !== 'Unknown') icuVitals.patient_name = data.patient_name;
- if (data.mrn || data.patient_id) icuVitals.patient_id = data.mrn || data.patient_id;
- if (data.device_id) icuVitals.device_id = data.device_id;
- if (data.bed_id || data.bedLabel) icuVitals.bed_id = data.bed_id || data.bedLabel;
-
- document.getElementById('icuPatientNameText').textContent = icuVitals.patient_name;
- document.getElementById('icuAvatar').textContent = icuVitals.patient_name.charAt(0).toUpperCase();
- document.getElementById('icuMrnTag').textContent = `MRN: ${icuVitals.patient_id}`;
- document.getElementById('icuBedTag').textContent = `BED: ${icuVitals.bed_id}`;
- document.getElementById('icuDevTag').textContent = `DEV: ${icuVitals.device_id}`;
- updateIcuMonitorReadouts();
- }
+ // Sync with Contec Machine Screen
+ applyVitalsToMachine(data);
// 1. Update Patient Banner
const hasAssigned = data.patient_name && data.patient_name !== 'Unknown' && data.patient_name !== 'UNKNOWN' && data.patient_name !== 'Unassigned Patient';
@@ -4182,6 +4340,17 @@
};
closeAssignPatientModal();
+ if (activeDeviceId === device_id || !activeDeviceId) {
+ machVitals.patient_name = name;
+ machVitals.patient_id = mrn;
+ machVitals.bed_id = bed_id;
+ const elName = document.getElementById('machPatientName');
+ if (elName) elName.textContent = name ? name.toUpperCase() : '--';
+ const elMrn = document.getElementById('machMrnPill');
+ if (elMrn) elMrn.textContent = mrn ? `MRN: ${mrn}` : 'MRN: --';
+ const elBed = document.getElementById('machBedPill');
+ if (elBed) elBed.textContent = bed_id ? `BED ${bed_id}` : 'BED --';
+ }
updateDeviceTabs();
if (viewMode === 'grid') renderMultiBedGrid();
else if (viewMode === 'single' && activeDeviceId === device_id) {
@@ -4343,12 +4512,18 @@
const updateEl = document.getElementById(`update-${cardKey}`);
const cardEl = document.getElementById(`card-${cardKey}`);
+ if (!valueEl) return;
+
if (value === null || value === undefined) {
valueEl.innerHTML = '---';
valueEl.className = 'vital-value no-data';
- statusEl.textContent = 'No Data';
- statusEl.className = 'status-badge offline';
- cardEl.classList.remove('has-value');
+ if (statusEl) {
+ statusEl.textContent = 'No Data';
+ statusEl.className = 'status-badge offline';
+ }
+ if (cardEl) {
+ cardEl.classList.remove('has-value');
+ }
return;
}