reorganize application code into fiveparaminte-main directory structure

This commit is contained in:
2026-09-17 14:30:02 +05:30
parent 1b1b382f3e
commit 68bda2519f
16 changed files with 1080 additions and 379 deletions
+100 -29
View File
@@ -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