reorganize application code into fiveparaminte-main directory structure
This commit is contained in:
@@ -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
|
||||
|
||||
+22
-31
@@ -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:
|
||||
|
||||
+100
-29
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2192,10 +2192,10 @@
|
||||
<div class="machine-top-bar">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span class="machine-brand">🏥 CONTEC CMS7000PLUS</span>
|
||||
<span class="machine-pill" id="machBedPill">BED 01</span>
|
||||
<span class="machine-pill">ADULT</span>
|
||||
<span style="font-weight: 800; color: #ffffff;" id="machPatientName">SHAHIL</span>
|
||||
<span class="machine-pill" id="machMrnPill">MRN: 09091102</span>
|
||||
<span class="machine-pill" id="machBedPill">BED --</span>
|
||||
<span class="machine-pill" id="machCategoryPill">ADULT</span>
|
||||
<span style="font-weight: 800; color: #ffffff;" id="machPatientName">--</span>
|
||||
<span class="machine-pill" id="machMrnPill">MRN: --</span>
|
||||
<span style="color: #64748b; font-size: 11px;">PACE: NO</span>
|
||||
<span class="machine-alarm-normal">● TELEMETRY NORMAL</span>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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"
|
||||
|
||||
+68
-15
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+304
-129
@@ -2192,10 +2192,10 @@
|
||||
<div class="machine-top-bar">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span class="machine-brand">🏥 CONTEC CMS7000PLUS</span>
|
||||
<span class="machine-pill" id="machBedPill">BED 01</span>
|
||||
<span class="machine-pill">ADULT</span>
|
||||
<span style="font-weight: 800; color: #ffffff;" id="machPatientName">SHAHIL</span>
|
||||
<span class="machine-pill" id="machMrnPill">MRN: 09091102</span>
|
||||
<span class="machine-pill" id="machBedPill">BED --</span>
|
||||
<span class="machine-pill" id="machCategoryPill">ADULT</span>
|
||||
<span style="font-weight: 800; color: #ffffff;" id="machPatientName">--</span>
|
||||
<span class="machine-pill" id="machMrnPill">MRN: --</span>
|
||||
<span style="color: #64748b; font-size: 11px;">PACE: NO</span>
|
||||
<span class="machine-alarm-normal">● TELEMETRY NORMAL</span>
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user