From 73a9ac103e4aa7f517096a656c9c09e488cef8d9 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Thu, 6 Aug 2026 17:57:59 +0700 Subject: [PATCH] update --- .../Http/Controllers/FrontpageController.php | 4 +- listener/app.py | 413 +++++++++++------- listener/geneexpert.py | 39 +- 3 files changed, 298 insertions(+), 158 deletions(-) diff --git a/htdocs/app/Http/Controllers/FrontpageController.php b/htdocs/app/Http/Controllers/FrontpageController.php index 6d34710b..c8e54a4b 100644 --- a/htdocs/app/Http/Controllers/FrontpageController.php +++ b/htdocs/app/Http/Controllers/FrontpageController.php @@ -1274,7 +1274,7 @@ 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]; + $daftaridtcm = [77,78,79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; if (in_array($poli_id, $daftaridtcm)){ $genexpert = false; } else { @@ -1504,7 +1504,7 @@ 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]; + $daftaridtcm = [77,78,79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; if (in_array($poli_id, $daftaridtcm)){ $genexpert = false; } else { diff --git a/listener/app.py b/listener/app.py index 4ea456b7..863b5402 100644 --- a/listener/app.py +++ b/listener/app.py @@ -14,7 +14,6 @@ import datetime import traceback import serial # type: ignore -from flask import Flask, jsonify, request # type: ignore from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text # type: ignore from sqlalchemy import DateTime as SqDateTime # type: ignore from sqlalchemy import Date as SqDate # type: ignore @@ -54,11 +53,8 @@ ERROR_LOG_KEYWORDS = ( # 1. KONFIGURASI SISTEM # ========================================== # Global Variables -app = Flask(__name__) active_genexpert_connections = {} connection_lock = threading.Lock() -pending_result_queries = {} -pending_query_lock = threading.Lock() scheduled_result_queries = {} scheduled_result_query_lock = threading.Lock() genexpert_query_inflight_by_ip = {} @@ -66,26 +62,23 @@ genexpert_query_inflight_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_RESPONSE_MODE_DEFAULT = "astm_active" GENEXPERT_RESPONSE_MODE_BY_IP = { - # "10.10.120.73": "astm_active", # "10.10.120.75": "hl7_passive", } +GENEXPERT_HOST_APPLICATION_DEFAULT = "DE002" +GENEXPERT_HOST_APPLICATION_BY_IP = { + "10.10.120.75": "GE01", # GenExpert Kecil + "10.10.120.108": "DE002", # GenExpert Tempat Lama + "10.10.120.73": "GE01", # GenExpert Besa +} # Mapping Flag ke IP Address GeneXpert # Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode) TARGET_MAPPING = { 'flg_gxp1': '10.10.120.73', - 'flg_gxp2': '10.10.120.13', + 'flg_gxp2': '10.10.120.108', 'flg_gxp3': '10.10.120.75' } -GENEXPERT_HOST_APPLICATION_DEFAULT = "GE01" -GENEXPERT_HOST_APPLICATION_BY_IP = { - "10.10.120.75": "GE01", - "10.10.120.13": "DE002", - "10.10.120.73": "GE01", -} # GeneXpert Configuration # ========================================== # KONFIGURASI MAPPING TES (DATABASE -> GENEXPERT) @@ -94,43 +87,45 @@ GENEXPERT_HOST_APPLICATION_BY_IP = { # Kanan: 'Host Test Code' dari Dokumen Word Anda GENEXPERT_TEST_MAPPING = { # Mapping untuk IP 10.10.120.75 (Multi-Assay) - "HIV": "HIV1-VL", # Xpert HIV-1 Viral Load XC Version 3 - "TCM TB": "MTBRIF", # Xpert MTBRIF Assay G4 Version 6 - "TCM TB ULTRA": "MTBRIF", # Xpert MTBRIF Ultra Version 4 - "TCM TB XDR": "MTB-XDR", # Xpert MTB-XDR Version 1 - "HCV VL": "HCV", # Xpert HCV Viral Load Version 1 - "COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2 - "17.3.1 TCM COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2 - "17.3.2 PCR COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2 + "HIV": "HIV1-VL", + "HBV": "HBVVL", + "TCM TB": "MTBRIF", + "TCM TB ULTRA": "MTBRIF", + "TCM TB XDR": "MTB-XDR 2", + "HCV VL": "HCV", + "COVID-19": "SARSCOV2FLURSV", + "17.3.1 TCM COVID-19": "SARSCOV2FLURSV", + "17.3.2 PCR COVID-19": "SARSCOV2FLURSV", "E.2.5 HCV TCM": "HCV", "18.1.1 TCM HCV": "HCV", "18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL", "18.1.4 TCM HPV": "HCV", "7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", - "5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "3.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL) ": "MTB-XDR", - "2.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "1.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "1.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "15.2.1 KULTUR TB MEDIA LJ": "MTB-XDR", - "8.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "9.3.5 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "9.3.6 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "12.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "H.2.5 PEMERIKSAAN KULTUR MYCROBACTERIUM TBC": "MTB-XDR", - "12.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "11.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "10.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "10.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "3.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "15.2.2 KULTUR TB MEDIA MGIT (AUTOMATIC)": "MTB-XDR", - "11.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", - "8.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", - "7.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", + "5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "3.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL) ": "MTB-XDR 2", + "2.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "1.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "1.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "15.2.1 KULTUR TB MEDIA LJ": "MTB-XDR 2", + "8.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "9.3.5 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "9.3.6 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "12.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "H.2.5 PEMERIKSAAN KULTUR MYCROBACTERIUM TBC": "MTB-XDR 2", + "12.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "11.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "10.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "10.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "3.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "15.2.2 KULTUR TB MEDIA MGIT (AUTOMATIC)": "MTB-XDR 2", + "11.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2", + "8.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", + "7.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", "2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF", "12.3.8 TCM TB (GENE EXPERT)": "MTBRIF", "15.2.3 TCM TB (GENE EXPERT)": "MTBRIF", + "15.2.3 TCM TB (GENE EXPERT) GENE EXPERT": "MTBRIF", "2.3.9 TCM GENE EXPERT": "MTBRIF", "3.3.8 TCM GENE EXPERT": "MTBRIF", "3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF", @@ -142,11 +137,10 @@ GENEXPERT_TEST_MAPPING = { } GENEXPERT_IP_CAPABILITIES = { - "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR", "HCV VL", "SARSCOV2FLURSV"], - "10.10.120.13": ["HCV", "HBV"], - "10.10.120.73": ["MTBRIF"] + "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"], + "10.10.120.73": ["MTBRIF", "HBVVL", "HIV1-VL", "HCV", "SARSCOV2FLURSV"], + "10.10.120.108": ["MTBRIF", "HCV", "HIV1-VL", "HBVVL", "SARSCOV2FLURSV"], } -# Default code jika nama tes di database tidak dikenali DEFAULT_GXP_CODE = "MTBRIF" DEVICE_CONFIGS = [ @@ -452,10 +446,6 @@ def manage_tcp_server(): logging.critical(f"[TCP-SERVER] Gagal Start: {e}") print(f"[TCP-SERVER] Gagal Start: {e}") -def run_http_api_server(): - print(f"[HTTP-API] Listening di port {HTTP_API_PORT}...") - app.run(host=SERVER_HOST, port=HTTP_API_PORT, debug=False, use_reloader=False, threaded=True) - def get_genexpert_host_application(ip_addr): ip_addr = str(ip_addr or "").strip() host_app = GENEXPERT_HOST_APPLICATION_BY_IP.get(ip_addr, GENEXPERT_HOST_APPLICATION_DEFAULT) @@ -490,6 +480,28 @@ def parse_genexpert_astm_query(message_text): query["query_sample_id"] = fields[2] if len(fields) > 2 else "" return query +def summarize_genexpert_astm_orders(records): + summaries = [] + for rec in records: + if not rec.startswith("O|"): + continue + + fields = rec.split("|") + sample_id = fields[2].strip() if len(fields) > 2 else "" + container_id = fields[3].strip() if len(fields) > 3 else "" + assay_code = "" + if len(fields) > 4: + assay_parts = [part.strip() for part in fields[4].split("^") if part.strip()] + assay_code = assay_parts[-1] if assay_parts else fields[4].strip() + priority = fields[5].strip() if len(fields) > 5 else "" + action_code = fields[11].strip() if len(fields) > 11 else "" + + summaries.append( + f"sample_id={sample_id or '-'}, container_id={container_id or '-'}, " + f"assay={assay_code or '-'}, priority={priority or '-'}, action={action_code or '-'}" + ) + return summaries + def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""): timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") records = [ @@ -521,7 +533,7 @@ def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""): f"assay_code={assay_code}, assay_source={assay_source}, capability_match={capability_match}" ) - records.append(f"P|{index}|{patient_id}||{patient_id}|{patient_name}|||{sex}") + records.append(f"P|{index}|{patient_name}||{patient_id}|{patient_name}|||{sex}") records.append(f"O|1|{sample_id}||^^^{assay_code}|R|{order_ts}|||||||||ORH||||||||||A") records.append("L|1|N") @@ -583,76 +595,166 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"): session.close() def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing): - records = parse_astm_records(clean_hl7) - record_types = [rec.split("|", 1)[0] for rec in records if rec] - print(f"[GENEXPERT-ASTM] ip={ip_addr}, record_types={record_types}") + # ========================================================== + # 1. BLOK PENANGANAN ASTM (Karena tidak diawali "MSH|") + # ========================================================== + if not str(clean_hl7 or "").startswith("MSH|"): + records = parse_astm_records(clean_hl7) + record_types = [rec.split("|", 1)[0] for rec in records if rec] + print(f"[GENEXPERT-ASTM] ip={ip_addr}, record_types={record_types}") - # A. Cek Jika Alat Meminta Order (Query) - if any(rec.startswith("Q|") for rec in records): - print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)") - send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm") - return + # A. Cek Jika Alat Meminta Order (Query) + if any(rec.startswith("Q|") for rec in records): + print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)") + send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm") + return - # B. Cek Jika Alat Mengirim Hasil Lab (Result) - if any(rec.startswith("R|") for rec in records): - print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.") - # Memanggil fungsi parser Anda untuk menyimpan hasil ke DB - parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}") - return + # B. Cek Jika Alat Mengirim Hasil Lab (Result) + if any(rec.startswith("R|") for rec in records): + print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.") + # Memanggil fungsi parser Anda untuk menyimpan hasil ke DB + parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}") + return - # C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment) - if any(rec.startswith("C|") for rec in records): - # 1. Ekstrak teks komentar untuk ditampilkan di log - comments = [rec for rec in records if rec.startswith("C|")] - for c in comments: - parts = c.split('|') - comment_text = parts[3] if len(parts) > 3 else c - print(f"[GENEXPERT-ASTM-INFO] Komentar dari Alat: {comment_text}") - - # 2. Ekstrak NoReg (Nomor Order) dari record 'O' - rnoreg = None - for rec in records: - if rec.startswith("O|"): - o_parts = rec.split('|') - if len(o_parts) > 2: - rnoreg = o_parts[2].strip() - break - - # 3. Update Database PaslabOrder - if rnoreg: - # Cari kolom flag yang cocok dengan IP yang sedang terkoneksi - target_flag_col = None - for flag_col, mapped_ip in TARGET_MAPPING.items(): - if mapped_ip == ip_addr: - target_flag_col = flag_col - break + # C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment) + if any(rec.startswith("C|") for rec in records): + # 1. Ekstrak teks komentar untuk ditampilkan di log + comments = [rec for rec in records if rec.startswith("C|")] + for c in comments: + parts = c.split('|') + comment_text = parts[3] if len(parts) > 3 else c + print(f"[GENEXPERT-ASTM-INFO] Komentar dari Alat: {comment_text}") - if target_flag_col: - try: - # Buka sesi database dan update - with SessionLocal() as session: - order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg).first() - if order: - # Set flag mesin tersebut menjadi True agar tidak dikirim ulang - setattr(order, target_flag_col, True) - session.commit() - print(f"[GENEXPERT-DB] Order {rnoreg} ditolak alat. Flag {target_flag_col} di-set True (Selesai).") - else: - print(f"[GENEXPERT-DB] Order {rnoreg} tidak ditemukan di database saat memproses penolakan.") - except Exception as e: - print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg}: {e}") + # 2. Ekstrak NoReg (Nomor Order) dari record 'O' + rnoreg = None + for rec in records: + if rec.startswith("O|"): + o_parts = rec.split('|') + if len(o_parts) > 2: + rnoreg = o_parts[2].strip() + break + + # 3. Update Database PaslabOrder + if rnoreg: + # Cari kolom flag yang cocok dengan IP yang sedang terkoneksi + target_flag_col = None + for flag_col, mapped_ip in TARGET_MAPPING.items(): + if mapped_ip == ip_addr: + target_flag_col = flag_col + break + + if target_flag_col: + try: + # Buka sesi database dan update + with SessionLocal() as session: + order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg).first() + if order: + # Set flag mesin tersebut menjadi True agar tidak dikirim ulang + setattr(order, target_flag_col, True) + session.commit() + print(f"[GENEXPERT-DB] Order {rnoreg} ditolak alat. Flag {target_flag_col} di-set True (Selesai).") + else: + print(f"[GENEXPERT-DB] Order {rnoreg} tidak ditemukan di database saat memproses penolakan.") + except Exception as e: + print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg}: {e}") - print(f"[GENEXPERT-ASTM] Transaksi penolakan order selesai diproses.") + print(f"[GENEXPERT-ASTM] Transaksi penolakan order selesai diproses.") + return + + # D. Jika hanya berisi H dan L tanpa ada transaksi berarti (Status Echo) + if set(record_types).issubset({'H', 'L'}): + print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.") + return + + # E. GeneXpert dapat mengirim status/echo order ASTM berisi H/P/O/L. + # Ini bukan hasil lab karena tidak ada R record, dan bukan query karena tidak ada Q record. + if "O" in record_types and set(record_types).issubset({'H', 'P', 'O', 'L'}): + order_summaries = summarize_genexpert_astm_orders(records) + print( + f"[GENEXPERT-ASTM] Menerima status/echo order tanpa hasil dari {ip_addr}. " + f"orders={order_summaries}" + ) + return + + log_genexpert_hl7("GENEXPERT-ASTM", ip_addr, clean_hl7, label="pesantidakdikenali") + print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}") return - # D. Jika hanya berisi H dan L tanpa ada transaksi berarti (Status Echo) - if set(record_types).issubset({'H', 'L'}): - print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.") + # ========================================================== + # 2. BLOK PENANGANAN HL7 (Fallback / Cadangan) + # ========================================================== + log_genexpert_hl7("IN", ip_addr, clean_hl7) + lines = clean_hl7.split('\r') + msh_fields = lines[0].split('|') + incoming_control_id = msh_fields[9] if len(msh_fields) > 9 else "UNKNOWN" + msg_id = incoming_control_id + + if "QRY^" in clean_hl7 or "QRY|" in clean_hl7: + print( + f"[GENEXPERT] Legacy QRY diterima dari {ip_addr} tetapi diabaikan. " + "Host hanya mendukung QBP^Z01/QBP^Z03 untuk order query." + ) return - print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}") - return + if "ORU^" in clean_hl7: + # --- [PATCH] CEK APAKAH INI PESAN ERROR / PENOLAKAN --- + if "|Error^" in clean_hl7 or "|X\r" in clean_hl7 or "\rTE|" in clean_hl7: + print(f"[GENEXPERT-ERROR] Mesin {ip_addr} menolak tes (Test Unknown/Disabled).") + + # Coba ekstrak NoReg dari segmen SPM agar kita bisa mengunci ordernya (set Flag = True) + rnoreg_error = None + for line in clean_hl7.split('\r'): + if line.startswith('SPM|'): + parts = line.split('|') + if len(parts) > 2: + rnoreg_error = parts[2].replace('^', '').strip() + break + + if rnoreg_error: + print(f"[GENEXPERT-ERROR] Mengunci/Membatalkan Order {rnoreg_error} agar tidak terjadi Infinite Loop.") + # (OPSIONAL: Jalankan fungsi update ke DB untuk mengubah flag PaslabOrder menjadi True di sini) + # ... + + # Kirim ACK agar alat berhenti mengirim error + ack_msg = create_genexpert_ack_r01_response(clean_hl7, ip_addr=ip_addr) + send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="oru-ack") + return # BERHENTI DI SINI. Jangan lanjut ke parse_hl7_result! + # ----------------------------------------------------- + print(f"[RESULT] Menerima Hasil Lab.") + parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{ip_addr}") + ack_msg = create_genexpert_ack_r01_response(clean_hl7, ip_addr=ip_addr) + send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="oru-ack") + print(f"[ACK SENT] Untuk hasil ID {incoming_control_id}") + return + + if "QBP^Z01" in clean_hl7 or "QBP^Z03" in clean_hl7: + print("[GENEXPERT] Alat meminta ORDER") + msg_id = extract_msg_control_id(clean_hl7) + send_all_orders(conn, ip_addr, clean_hl7, msg_id, response_framing=response_framing) + return + + if "QCN^J01" in clean_hl7: + clear_genexpert_inflight_for_ip(ip_addr, reason="query-confirmation") + ack_msg = create_genexpert_ack_j01_response(clean_hl7, ip_addr=ip_addr) + send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="qcn-ack") + print(f"[GENEXPERT] Menerima konfirmasi query dari {ip_addr}.") + return + + print(f"[GenExpert_TCP] Pesan Lengkap Diterima: {clean_hl7[:50]}...") + parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{ip_addr}, ") + + try: + if len(msh_fields) > 9: + msg_control_id = msh_fields[9] + ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + ack_msg = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{ack_time}||ACK|{msg_control_id}|P|2.5\rMSA|AA|{msg_control_id}\r" + full_ack = f"\x0b{ack_msg}\x1c\r" + log_genexpert_hl7("OUT", ip_addr, ack_msg, label="generic-ack") + conn.sendall(full_ack.encode('utf-8')) + print(f"[ACK] Terkirim untuk ID {msg_control_id}") + except Exception as e: + print(f"Gagal kirim ACK: {e}") def handle_genexpert_client(conn, addr): print(f"[GenExpert_TCP] Koneksi baru dari {addr}") @@ -693,13 +795,15 @@ def handle_genexpert_client(conn, addr): # --- 1. HANDLE HANDSHAKE (ENQ) --- # Jika alat kirim ENQ (\x05/♣), langsung balas ACK (\x06) if b'\x05' in buffer: - # logging.info(f"[TCP] Terima ENQ dari {addr}, kirim ACK.") log_genexpert_handshake(addr[0], "ENQ-RX", detail=f"buffer_len={len(buffer)}") conn.sendall(b'\x06') log_genexpert_handshake(addr[0], "ACK-TX", detail="reason=enq") - # Hapus ENQ dari buffer agar tidak mengganggu - buffer = buffer.replace(b'\x05', b'') - + + # [PERBAIKAN KURSIS 2]: KOSONGKAN TOTAL BUFFER SAAT ENQ! + # Alat meminta sesi baru, pastikan tidak ada sisa pesan lama yang nyangkut + buffer = b"" + pending_astm_hl7 = "" + continue # Langsung lanjut ke recv() berikutnya if b'\x15' in buffer: log_genexpert_handshake(addr[0], "NAK-BUFFER-CLEAR", detail=f"buffer_len={len(buffer)}") buffer = buffer.replace(b'\x15', b'') @@ -719,13 +823,23 @@ def handle_genexpert_client(conn, addr): if b'\x1c' in buffer: # Pola MLLP Standard end_marker_pos = buffer.find(b'\x1c') msg_complete = True - elif b'\x03' in buffer: # Pola ASTM (Ada Checksum setelahnya) - # ASTM: ...CS - # Kita cari \x03, lalu ambil ETX + 2 checksum + CRLF secara penuh. - pos = buffer.find(b'\x03') - if len(buffer) >= pos + 5: + elif b'\x03' in buffer or b'\x17' in buffer: + # Cari di index mana letak ETX atau ETB + pos_etx = buffer.find(b'\x03') + pos_etb = buffer.find(b'\x17') + + # Tentukan mana yang muncul lebih dulu di buffer + pos = -1 + if pos_etx != -1 and pos_etb != -1: + pos = min(pos_etx, pos_etb) + else: + pos = max(pos_etx, pos_etb) + + # Pastikan kita menerima 5 bytes penuh (ETB/ETX + C1 + C2 + CR + LF) + if pos != -1 and len(buffer) >= pos + 5: end_marker_pos = pos + 5 msg_complete = True + elif b'\x04' in buffer: # Pola EOT (Putus Koneksi/Selesai) end_marker_pos = buffer.find(b'\x04') msg_complete = True @@ -748,6 +862,8 @@ def handle_genexpert_client(conn, addr): full_message_bytes = buffer[:end_marker_pos] response_framing = detect_genexpert_message_framing(full_message_bytes) + if response_framing == "astm": + debug_genexpert_astm_frame(addr[0], full_message_bytes, direction="RX") log_genexpert_handshake( addr[0], "FRAME-COMPLETE", @@ -770,22 +886,27 @@ def handle_genexpert_client(conn, addr): # Decode ke string temp_str = full_message_bytes.decode('latin-1', errors='ignore') + astm_text = extract_astm_frame_text(full_message_bytes) if response_framing == "astm" else "" + # --- SANITIZING (PEMBERSIHAN) --- + # Cari MSH pertama if "MSH|" in temp_str: msh_index = temp_str.find("MSH|") clean_hl7 = temp_str[msh_index:] - elif response_framing == "astm": - clean_hl7 = extract_astm_frame_text(full_message_bytes) - else: - clean_hl7 = temp_str.strip("\x0b\x1c\r\n") - - if clean_hl7: if response_framing == "astm": pending_astm_hl7 = clean_hl7 pending_astm_framing = response_framing log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(clean_hl7)}") continue process_genexpert_hl7_message(conn, addr[0], clean_hl7, response_framing) + elif response_framing == "astm" and astm_text: + pending_astm_hl7 = (pending_astm_hl7 or "") + astm_text + pending_astm_framing = response_framing + log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(pending_astm_hl7)}, mode=records") + continue + else: + # Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang) + pass else: if buffer: head_hex = buffer[:12].hex() @@ -1297,15 +1418,16 @@ def get_genexpert_response_mode(ip_addr): def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""): log_genexpert_hl7("OUT", ip_addr, hl7_message, label=label) log_genexpert_hl7_full("OUT", ip_addr, hl7_message, label=label) - payload = frame_genexpert_response(hl7_message, framing) response_mode = get_genexpert_response_mode(ip_addr) - print( - f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, " - f"response_mode={response_mode}, label={label}, bytes={len(payload)}" - ) if framing == "astm" and response_mode == "astm_active": + print( + f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, " + f"response_mode={response_mode}, label={label}, bytes=multi-frame" + ) send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=label) return + + payload = frame_genexpert_response(hl7_message, framing) if framing == "astm": debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=label) else: @@ -1313,6 +1435,10 @@ def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""): f"[GENEXPERT-FRAME-TX] ip={ip_addr}, label={label}, framing={framing}, " f"hex={_hex_bytes(payload)}, visible={_visible_bytes(payload)}" ) + print( + f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, " + f"response_mode={response_mode}, label={label}, bytes={len(payload)}" + ) conn.sendall(payload) def send_genexpert_transport_ack(conn, ip_addr, framing, reason="frame-received"): @@ -1423,15 +1549,7 @@ def debug_genexpert_order_message(hl7_message, ip_addr=None): f"specimen_id='{specimen_id}', specimen_type='{specimen_type}', patient_id='{current_pid}', " f"patient_name='{current_patient_name}', raw='{segment}'" ) - -def build_genexpert_result_query(accnumber, msg_control_id): - ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - query_msg = ( - f"MSH|^~\\&|MyLIS|Mikrobiology|GeneXpert|Cepheid|{ts}||QRY^Q02|{msg_control_id}|P|2.5\r" - f"QRD|{ts}|R|I|{msg_control_id}|||1^RD|{accnumber}|OTH|||T\r" - ) - return query_msg - + def get_active_genexpert_ips(): with connection_lock: return list(active_genexpert_connections.keys()) @@ -1473,14 +1591,6 @@ def schedule_result_query_for_order(accnumber, register_no, target_ip=None, **kw ) return False -@app.route("/api/genexpert/query-result", methods=["POST"]) -def api_query_genexpert_result(): - return jsonify({ - "ok": False, - "message": "Mode GeneXpert pasif aktif. Host hanya menjawab request dari alat.", - }), 409 - - def create_hl7_dsr_response(order, msg_control_id, qrd_segment): """ Membuat pesan balasan DSR^Q03 (Data Response) untuk GeneXpert. @@ -3240,12 +3350,7 @@ if __name__ == "__main__": myla_inbound_thread.start() all_threads.append(myla_inbound_thread) - # 4. Start Thread HTTP API (Trigger dari Laravel) - t_http = threading.Thread(target=run_http_api_server, name="Manager-HTTP-API", daemon=True) - t_http.start() - all_threads.append(t_http) - - # 5. LOOP UTAMA (Keep-Alive & Monitoring) + # 4. LOOP UTAMA (Keep-Alive & Monitoring) try: while True: print(f"--- Monitoring {len(all_threads)} Threads ---") diff --git a/listener/geneexpert.py b/listener/geneexpert.py index 5e8a24e3..a7861796 100644 --- a/listener/geneexpert.py +++ b/listener/geneexpert.py @@ -136,6 +136,7 @@ TARGET_MAPPING = { GENEXPERT_TEST_MAPPING = { # Mapping untuk IP 10.10.120.75 (Multi-Assay) "HIV": "HIV1-VL", + "HBV": "HBVVL", "TCM TB": "MTBRIF", "TCM TB ULTRA": "MTBRIF", "TCM TB XDR": "MTB-XDR 2", @@ -146,7 +147,7 @@ GENEXPERT_TEST_MAPPING = { "E.2.5 HCV TCM": "HCV", "18.1.1 TCM HCV": "HCV", "18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL", - "18.1.4 TCM HPV": "HCV", + "18.1.4 TCM HPV": "HPV", "7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", @@ -172,6 +173,7 @@ GENEXPERT_TEST_MAPPING = { "2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF", "12.3.8 TCM TB (GENE EXPERT)": "MTBRIF", "15.2.3 TCM TB (GENE EXPERT)": "MTBRIF", + "15.2.3 TCM TB (GENE EXPERT) GENE EXPERT": "MTBRIF", "2.3.9 TCM GENE EXPERT": "MTBRIF", "3.3.8 TCM GENE EXPERT": "MTBRIF", "3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF", @@ -185,7 +187,7 @@ GENEXPERT_TEST_MAPPING = { GENEXPERT_IP_CAPABILITIES = { "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"], "10.10.120.73": ["MTBRIF", "HBVVL", "HIV1-VL", "HCV", "SARSCOV2FLURSV"], - "10.10.120.108": ["HCV", "HIV1-VL", "HBVVL"], + "10.10.120.108": ["MTBRIF", "HCV", "HIV1-VL", "HBVVL", "SARSCOV2FLURSV"], } DEFAULT_GXP_CODE = "MTBRIF" @@ -379,6 +381,28 @@ def parse_genexpert_astm_query(message_text): query["query_sample_id"] = fields[2] if len(fields) > 2 else "" return query +def summarize_genexpert_astm_orders(records): + summaries = [] + for rec in records: + if not rec.startswith("O|"): + continue + + fields = rec.split("|") + sample_id = fields[2].strip() if len(fields) > 2 else "" + container_id = fields[3].strip() if len(fields) > 3 else "" + assay_code = "" + if len(fields) > 4: + assay_parts = [part.strip() for part in fields[4].split("^") if part.strip()] + assay_code = assay_parts[-1] if assay_parts else fields[4].strip() + priority = fields[5].strip() if len(fields) > 5 else "" + action_code = fields[11].strip() if len(fields) > 11 else "" + + summaries.append( + f"sample_id={sample_id or '-'}, container_id={container_id or '-'}, " + f"assay={assay_code or '-'}, priority={priority or '-'}, action={action_code or '-'}" + ) + return summaries + def detect_genexpert_message_framing(message_bytes): raw = message_bytes or b"" if b"\x1c" in raw or raw.startswith(b"\x0b"): @@ -2569,6 +2593,17 @@ def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing): print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.") return + # E. GeneXpert dapat mengirim status/echo order ASTM berisi H/P/O/L. + # Ini bukan hasil lab karena tidak ada R record, dan bukan query karena tidak ada Q record. + if "O" in record_types and set(record_types).issubset({'H', 'P', 'O', 'L'}): + order_summaries = summarize_genexpert_astm_orders(records) + print( + f"[GENEXPERT-ASTM] Menerima status/echo order tanpa hasil dari {ip_addr}. " + f"orders={order_summaries}" + ) + return + + log_genexpert_hl7("GENEXPERT-ASTM", ip_addr, clean_hl7, label="pesantidakdikenali") print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}") return