From e1a35984726565707f9c13636ba8da34158d96b0 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Fri, 10 Apr 2026 19:45:42 +0700 Subject: [PATCH] Enable GenExpert auto polling and flag eligible TCM orders --- .../Http/Controllers/FrontpageController.php | 52 ++-- listener/app.py | 225 +++++++++++++++++- 2 files changed, 250 insertions(+), 27 deletions(-) diff --git a/htdocs/app/Http/Controllers/FrontpageController.php b/htdocs/app/Http/Controllers/FrontpageController.php index d1d4c218..33f749c0 100644 --- a/htdocs/app/Http/Controllers/FrontpageController.php +++ b/htdocs/app/Http/Controllers/FrontpageController.php @@ -1255,7 +1255,13 @@ class FrontpageController extends Controller 'nmpendaftar' => $clientIp, 'orderid' => $notransaksi, ]); - $pesan = $nofoto; + $pesan = $nofoto; + $daftaridtcm = [79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; + if (in_array($poli_id, $daftaridtcm)){ + $genexpert = true; + } else { + $genexpert = false; + } Paslab::updateOrCreate( [ 'rnoreg' => $nofoto, @@ -1278,9 +1284,9 @@ class FrontpageController extends Controller 'flg_vitek2' => false, 'flg_bd1' => false, 'flg_bd2' => false, - 'flg_gxp1' => false, - 'flg_gxp2' => false, - 'flg_gxp3' => false, + 'flg_gxp1' => $genexpert, + 'flg_gxp2' => $genexpert, + 'flg_gxp3' => $genexpert, 'flg_vitek3' => false, ] ); @@ -1478,6 +1484,13 @@ class FrontpageController extends Controller 'orderid' => $notransaksi, ]); $pesan = $nofoto; + $daftaridtcm = [79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; + if (in_array($poli_id, $daftaridtcm)){ + $genexpert = true; + } else { + $genexpert = false; + } + Paslab::updateOrCreate( [ 'rnoreg' => $nofoto, @@ -1500,9 +1513,9 @@ class FrontpageController extends Controller 'flg_vitek2' => false, 'flg_bd1' => false, 'flg_bd2' => false, - 'flg_gxp1' => false, - 'flg_gxp2' => false, - 'flg_gxp3' => false, + 'flg_gxp1' => $genexpert, + 'flg_gxp2' => $genexpert, + 'flg_gxp3' => $genexpert, 'flg_vitek3' => false, ] ); @@ -1729,6 +1742,13 @@ class FrontpageController extends Controller 'pendaftar' => Session('previlage'), 'nmpendaftar' => Session('nama'), ]); + $daftaridtcm = [79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; + if (in_array($poli_id, $daftaridtcm)){ + $genexpert = true; + } else { + $genexpert = false; + } + Paslab::updateOrCreate( [ 'rnoreg' => $nofoto, @@ -1751,9 +1771,9 @@ class FrontpageController extends Controller 'flg_vitek2' => false, 'flg_bd1' => false, 'flg_bd2' => false, - 'flg_gxp1' => false, - 'flg_gxp2' => false, - 'flg_gxp3' => false, + 'flg_gxp1' => $genexpert, + 'flg_gxp2' => $genexpert, + 'flg_gxp3' => $genexpert, 'flg_vitek3' => false, ] ); @@ -1876,9 +1896,9 @@ class FrontpageController extends Controller 'flg_vitek2' => false, 'flg_bd1' => false, 'flg_bd2' => false, - 'flg_gxp1' => false, - 'flg_gxp2' => false, - 'flg_gxp3' => false, + 'flg_gxp1' => $genexpert, + 'flg_gxp2' => $genexpert, + 'flg_gxp3' => $genexpert, 'flg_vitek3' => false, ] ); @@ -1945,9 +1965,9 @@ class FrontpageController extends Controller 'flg_vitek2' => false, 'flg_bd1' => false, 'flg_bd2' => false, - 'flg_gxp1' => false, - 'flg_gxp2' => false, - 'flg_gxp3' => false, + 'flg_gxp1' => $genexpert, + 'flg_gxp2' => $genexpert, + 'flg_gxp3' => $genexpert, 'flg_vitek3' => false, ] ); diff --git a/listener/app.py b/listener/app.py index 02092249..8ad2b164 100644 --- a/listener/app.py +++ b/listener/app.py @@ -39,10 +39,15 @@ active_genexpert_connections = {} connection_lock = threading.Lock() pending_result_queries = {} pending_query_lock = threading.Lock() +scheduled_result_queries = {} +scheduled_result_query_lock = threading.Lock() # Network Configuration TCP_LISTENER_PORT = 6001 # PC GeneXpert set ke mode Client, konek ke IP:PORT ini SERVER_HOST = '0.0.0.0' # Listen di semua interface HTTP_API_PORT = 6002 # Endpoint trigger dari Laravel -> Python +GENEXPERT_RESULT_QUERY_INITIAL_DELAY_SECONDS = 60 +GENEXPERT_RESULT_QUERY_INTERVAL_SECONDS = 120 +GENEXPERT_RESULT_QUERY_MAX_DURATION_SECONDS = 21600 # Mapping Flag ke IP Address GeneXpert # Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode) TARGET_MAPPING = { @@ -220,6 +225,7 @@ def get_pending_orders(ip_addr): def send_all_orders(conn, ip_addr, hl7_msg, msg_id): orders = get_pending_orders(ip_addr) session = SessionLocal() + scheduled_orders = [] try: if not orders: print(f"[GENEXPERT] Tidak ada order pending untuk {ip_addr}") @@ -242,8 +248,19 @@ def send_all_orders(conn, ip_addr, hl7_msg, msg_id): flag = get_flag_by_device(ip_addr) setattr(order, flag, True) session.add(order) + scheduled_orders.append({ + "accnumber": str(order.rnoreg or "").strip(), + "register_no": str(order.rnoreg or "").strip(), + "target_ip": ip_addr, + }) session.commit() + for scheduled_order in scheduled_orders: + schedule_result_query_for_order( + accnumber=scheduled_order["accnumber"], + register_no=scheduled_order["register_no"], + target_ip=scheduled_order["target_ip"], + ) finally: session.close() @@ -257,6 +274,16 @@ def extract_msg_control_id(hl7_message): except: return None +def extract_message_type(hl7_message): + try: + segments = hl7_message.split('\r') + msh = segments[0].split('|') + if len(msh) > 8: + return msh[8].strip() + return "" + except: + return "" + def build_genexpert_result_query(accnumber, msg_control_id): ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S') # Query hasil berbasis accession number di QRD-8. @@ -270,6 +297,164 @@ def get_active_genexpert_ips(): with connection_lock: return list(active_genexpert_connections.keys()) +def stop_scheduled_result_query(accnumber, reason="completed"): + accnumber = str(accnumber or "").strip() + if not accnumber: + return False + + with scheduled_result_query_lock: + state = scheduled_result_queries.get(accnumber) + if not state: + return False + state["status"] = reason + stop_event = state.get("stop_event") + if stop_event: + stop_event.set() + scheduled_result_queries.pop(accnumber, None) + + logging.info(f"[GENEXPERT-SCHEDULER] Stop accnumber={accnumber}, reason={reason}") + print(f"[GENEXPERT-SCHEDULER] Stop accnumber={accnumber}, reason={reason}") + return True + +def scheduled_result_query_worker(accnumber): + while True: + with scheduled_result_query_lock: + state = scheduled_result_queries.get(accnumber) + if not state: + return + stop_event = state["stop_event"] + target_ip = state.get("target_ip") + register_no = state.get("register_no") or accnumber + attempt = int(state.get("attempt", 0)) + created_at = state.get("created_at") or datetime.datetime.now() + max_duration_seconds = max( + int(state.get("max_duration_seconds", GENEXPERT_RESULT_QUERY_MAX_DURATION_SECONDS)), + 1 + ) + age_seconds = max(int((datetime.datetime.now() - created_at).total_seconds()), 0) + if age_seconds >= max_duration_seconds: + state["status"] = "expired" + state["expired_at"] = datetime.datetime.now() + state["age_seconds"] = age_seconds + stop_event.set() + scheduled_result_queries.pop(accnumber, None) + logging.info( + f"[GENEXPERT-SCHEDULER] Expired accnumber={accnumber}, " + f"age={age_seconds}s, max_duration={max_duration_seconds}s" + ) + print( + f"[GENEXPERT-SCHEDULER] Expired accnumber={accnumber}, " + f"age={age_seconds}s, max_duration={max_duration_seconds}s" + ) + return + next_delay = max( + int( + state.get( + "initial_delay_seconds" if attempt == 0 else "interval_seconds", + GENEXPERT_RESULT_QUERY_INITIAL_DELAY_SECONDS if attempt == 0 else GENEXPERT_RESULT_QUERY_INTERVAL_SECONDS + ) + ), + 1 + ) + + if stop_event.wait(next_delay): + return + + with scheduled_result_query_lock: + state = scheduled_result_queries.get(accnumber) + if not state: + return + state["attempt"] = int(state.get("attempt", 0)) + 1 + state["last_requested_at"] = datetime.datetime.now() + attempt_no = state["attempt"] + + logging.info( + f"[GENEXPERT-SCHEDULER] Trigger query hasil accnumber={accnumber}, " + f"register_no={register_no}, target_ip={target_ip}, attempt={attempt_no}" + ) + print( + f"[GENEXPERT-SCHEDULER] Trigger query hasil accnumber={accnumber}, " + f"register_no={register_no}, target_ip={target_ip}, attempt={attempt_no}" + ) + + result = trigger_result_query_to_genexpert( + accnumber=accnumber, + register_no=register_no, + target_ip=target_ip, + wait_seconds=0, + ) + + with scheduled_result_query_lock: + state = scheduled_result_queries.get(accnumber) + if not state: + return + state["last_result"] = result + +def schedule_result_query_for_order( + accnumber, + register_no, + target_ip=None, + initial_delay_seconds=GENEXPERT_RESULT_QUERY_INITIAL_DELAY_SECONDS, + interval_seconds=GENEXPERT_RESULT_QUERY_INTERVAL_SECONDS, + max_duration_seconds=GENEXPERT_RESULT_QUERY_MAX_DURATION_SECONDS, +): + accnumber = str(accnumber or "").strip() + register_no = str(register_no or accnumber).strip() + target_ip = str(target_ip or "").strip() or None + + if not accnumber: + logging.warning("[GENEXPERT-SCHEDULER] Jadwal query hasil dilewati karena accnumber kosong.") + print("[GENEXPERT-SCHEDULER] Jadwal query hasil dilewati karena accnumber kosong.") + return False + + with scheduled_result_query_lock: + existing = scheduled_result_queries.get(accnumber) + if existing: + existing["register_no"] = register_no + existing["target_ip"] = target_ip + existing["initial_delay_seconds"] = initial_delay_seconds + existing["interval_seconds"] = interval_seconds + existing["max_duration_seconds"] = max_duration_seconds + existing["status"] = "active" + logging.info(f"[GENEXPERT-SCHEDULER] Jadwal sudah aktif untuk accnumber={accnumber}") + print(f"[GENEXPERT-SCHEDULER] Jadwal sudah aktif untuk accnumber={accnumber}") + return True + + stop_event = threading.Event() + worker = threading.Thread( + target=scheduled_result_query_worker, + args=(accnumber,), + name=f"GeneXpertResultQuery-{accnumber}", + daemon=True, + ) + scheduled_result_queries[accnumber] = { + "register_no": register_no, + "target_ip": target_ip, + "initial_delay_seconds": initial_delay_seconds, + "interval_seconds": interval_seconds, + "max_duration_seconds": max_duration_seconds, + "attempt": 0, + "status": "active", + "created_at": datetime.datetime.now(), + "stop_event": stop_event, + "thread_name": worker.name, + } + + worker.start() + logging.info( + f"[GENEXPERT-SCHEDULER] Jadwal dibuat accnumber={accnumber}, " + f"register_no={register_no}, target_ip={target_ip}, " + f"initial_delay={initial_delay_seconds}s, interval={interval_seconds}s, " + f"max_duration={max_duration_seconds}s" + ) + print( + f"[GENEXPERT-SCHEDULER] Jadwal dibuat accnumber={accnumber}, " + f"register_no={register_no}, target_ip={target_ip}, " + f"initial_delay={initial_delay_seconds}s, interval={interval_seconds}s, " + f"max_duration={max_duration_seconds}s" + ) + return True + def trigger_result_query_to_genexpert(accnumber, register_no, target_ip=None, wait_seconds=20): active_ips = get_active_genexpert_ips() if not active_ips: @@ -395,6 +580,7 @@ def parse_hl7_result(conn, msg_id, hl7_message, device_name="GeneXpert"): patient_name = "" # rnmpas result_date = None # tgl_data results_list = [] # untuk organisme + message_type = "" # Waktu default jika tidak ada di pesan result_date = datetime.datetime.now() @@ -408,6 +594,8 @@ def parse_hl7_result(conn, msg_id, hl7_message, device_name="GeneXpert"): # --- MSH (Header) --- if seg_type == 'MSH': + if len(fields) > 8 and fields[8]: + message_type = fields[8].strip() # Ambil tanggal pesan (Field 7) Format: YYYYMMDDHHMMSS if len(fields) > 6 and fields[6]: try: @@ -456,19 +644,23 @@ def parse_hl7_result(conn, msg_id, hl7_message, device_name="GeneXpert"): # 4. Validasi Data Penting if not sample_id: - source_ip = "" - try: - source_ip = str(conn.getpeername()[0]).strip() - except Exception: - source_ip = str(device_name).replace("GeneXpert-", "").replace(",", " ").strip() + if str(message_type).startswith("ORU^"): + source_ip = "" + try: + source_ip = str(conn.getpeername()[0]).strip() + except Exception: + source_ip = str(device_name).replace("GeneXpert-", "").replace(",", " ").strip() - if source_ip: - logging.info(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.") - print(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.") - send_all_orders(conn, source_ip, clean_hl7, msg_id) + if source_ip: + logging.info(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.") + print(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.") + send_all_orders(conn, source_ip, clean_hl7, msg_id) + else: + logging.warning("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.") + print("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.") else: - logging.warning("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.") - print("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.") + logging.info(f"[HL7 Parser] Pesan {message_type or 'UNKNOWN'} tanpa sample_id diabaikan.") + print(f"[HL7 Parser] Pesan {message_type or 'UNKNOWN'} tanpa sample_id diabaikan.") return mapped_no_id = sample_id @@ -506,6 +698,7 @@ def parse_hl7_result(conn, msg_id, hl7_message, device_name="GeneXpert"): session.commit() logging.info(f"[DB] Berhasil simpan ke LisPhoenix: {sample_id}") print(f"[DB] Berhasil simpan ke LisPhoenix: {sample_id}") + stop_scheduled_result_query(sample_id, reason="result-received") if pending_event: pending_event.set() @@ -1577,6 +1770,11 @@ def handle_genexpert_client(conn, addr): session.commit() logging.info(f"[UPDATE] {target_flag_col} diset TRUE untuk {search_sample_id}") print(f"[UPDATE] {target_flag_col} diset TRUE untuk {search_sample_id}") + schedule_result_query_for_order( + accnumber=str(order.rnoreg or "").strip(), + register_no=str(order.rnoreg or "").strip(), + target_ip=client_ip, + ) else: logging.warning(f"[NOT FOUND/ALREADY SENT] Tidak ada order baru untuk {search_sample_id} di kolom {target_flag_col}") @@ -1621,6 +1819,11 @@ def handle_genexpert_client(conn, addr): send_all_orders(conn, addr[0], clean_hl7, msg_id) continue + elif "QCN^J01" in clean_hl7: + logging.info(f"[GENEXPERT] Menerima konfirmasi query dari {addr[0]}.") + print(f"[GENEXPERT] Menerima konfirmasi query dari {addr[0]}.") + continue + # Logging sample data (50 karakter) print(f"[GenExpert_TCP] Pesan Lengkap Diterima: {clean_hl7[:50]}...")