reorganize application code into fiveparaminte-main directory structure
This commit is contained in:
+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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user