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; }