From 539d0a8b50cab46d443e7e786ad15a87611ba087 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Thu, 5 Feb 2026 05:59:37 +0700 Subject: [PATCH] update --- .../Controllers/JsonTransferController.php | 2 - listener/app.py | 207 ++++++++++++++++-- 2 files changed, 184 insertions(+), 25 deletions(-) diff --git a/htdocs/app/Http/Controllers/JsonTransferController.php b/htdocs/app/Http/Controllers/JsonTransferController.php index c19c0a19..9b9f599f 100644 --- a/htdocs/app/Http/Controllers/JsonTransferController.php +++ b/htdocs/app/Http/Controllers/JsonTransferController.php @@ -126,8 +126,6 @@ class JsonTransferController extends Controller return back()->with('success', 'Import berhasil!'); } - - public function pull(Request $request){ if (session('previlage') !== 'developer') { abort(403, 'Akses ditolak. Anda tidak memiliki izin untuk menjalankan git pull.'); diff --git a/listener/app.py b/listener/app.py index e2ab4fb6..8286affb 100644 --- a/listener/app.py +++ b/listener/app.py @@ -52,7 +52,11 @@ GENEXPERT_TEST_MAPPING = { # Pastikan ini benar, atau sesuaikan jika itu typo di dokumen. "HBV VL": "HCV", } - +GENEXPERT_IP_CAPABILITIES = { + "10.10.120.75": ["MTB-RIF", "MTB-RIF_ULTRA2", "MTB-XDR", "HIV-1_VL", "COV-2 2"], + "10.10.120.74": ["HCV", "HBV"], + "10.10.120.73": [] # Belum ada yang aktif +} # Default code jika nama tes di database tidak dikenali DEFAULT_GXP_CODE = "MTB-RIF" @@ -458,7 +462,65 @@ def send_order_response(conn, hl7_msg, msg_id=None): conn.sendall(mllp.encode('utf-8')) print(f"[GENEXPERT] >> RSP^Z03 terkirim untuk ORDER {order.urut}") +def create_hl7_dsr_response(order, msg_control_id, qrd_segment): + """ + Membuat pesan balasan DSR^Q03 (Data Response) untuk GeneXpert. + """ + timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + + # --- 1. HEADER (MSH) --- + # Field 9 (Message Type) adalah DSR^Q03 + # Field 10 (Control ID) kita generate baru + # Field 6 (Receiving Fac) harusnya GeneXpert + resp_control_id = f"RESP{timestamp}" + msh = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{timestamp}||DSR^Q03|{resp_control_id}|P|2.5" + + # --- 2. ACKNOWLEDGEMENT (MSA) --- + # AA = Application Accept (Kita mengerti pertanyaannya) + # msg_control_id = ID dari pesan QRY yang dikirim GeneXpert (Supaya dia tahu ini jawaban untuk pertanyaan yg mana) + msa = f"MSA|AA|{msg_control_id}" + + # --- 3. QUERY DEFINITION (QRD) --- + # Kita kembalikan segmen QRD yang dikirim alat (Echo back) + # qrd_segment harus string raw dari pesan masuk + qrd = qrd_segment + # --- 4. QUERY RESPONSE STATUS (QRF) --- + # Opsional, tapi baik untuk konfirmasi + qrf = f"QRF|LIS|{timestamp}||||" + + # --- 5. DATA PASIEN & ORDER (Jika Order Ditemukan) --- + if order: + # Mapping Data + sample_id = str(order.rnoreg) + pid_norm = order.norm if order.norm else "" + pid_nama = order.nama if order.nama else "No Name" + + # Gender + raw_gender = str(order.rjenis).upper() + pid_gender = 'M' if 'LAKI' in raw_gender or raw_gender == 'L' else 'F' + + # Test Code (Pakai Mapping yang tadi kita buat) + nama_tes_db = str(order.tes).strip() if order.tes else "" + test_code = GENEXPERT_TEST_MAPPING.get(nama_tes_db, DEFAULT_GXP_CODE) # Default: MTB-RIF + + # Segmen Data + # DSP/PID/ORC/OBR tergantung setting alat. + # GeneXpert standar biasanya terima format ORM di dalam DSR atau sequence PID-ORC-OBR + + pid = f"PID|1||{pid_norm}||{pid_nama}|||{pid_gender}" + orc = f"ORC|NW|{sample_id}" + obr = f"OBR|1|{sample_id}||{test_code}^{nama_tes_db}^L|||{timestamp}" + + # Gabungkan + return f"{msh}\r{msa}\r{qrd}\r{qrf}\r{pid}\r{orc}\r{obr}\r" + + else: + # Jika TIDAK ADA ORDER (Not Found) + # Kita kirim QAK (Query Acknowledge) dengan status NF (Not Found) di QRF/QAK + # Atau cukup kirim MSA|AA tapi tanpa segmen Order + return f"{msh}\r{msa}\r{qrd}\r{qrf}\r" + # ========================================== # 4. NETWORK & COMMUNICATION LOGIC # ========================================== @@ -528,11 +590,6 @@ def manage_tcp_server(): logging.critical(f"[TCP-SERVER] Gagal Start: {e}") print(f"[TCP-SERVER] Gagal Start: {e}") -def run_flask(): - """Wrapper untuk menjalankan Flask di Thread""" - # use_reloader=False WAJIB agar tidak membuat duplikat proses - app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False) - def send_mllp_message(sock, hl7_msg): """Membungkus pesan HL7 dengan MLLP (Minimal Lower Layer Protocol)""" # Start Block: 0x0B () @@ -606,7 +663,127 @@ def handle_genexpert_client(conn, addr): if "MSH|" in temp_str: msh_index = temp_str.find("MSH|") clean_hl7 = temp_str[msh_index:] - if "QBP^Z03" in 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" + # ========================================== + # SKENARIO 1: ALAT BERTANYA (QUERY / QRY) + # ========================================== + if "QRY^" in clean_hl7 or "QRY|" in clean_hl7: + client_ip = addr[0] + logging.info(f"[QUERY] Request dari IP: {client_ip} (Control ID: {incoming_control_id})") + print(f"[QUERY] Request dari IP: {client_ip} (Control ID: {incoming_control_id})") + + # 1. Tentukan Kolom Flag mana yang harus dicek berdasarkan IP + # Kita balik mappingnya: IP -> Nama Kolom + target_flag_col = None + for col_name, ip_addr in TARGET_MAPPING.items(): + if ip_addr == client_ip: + target_flag_col = col_name + break + + if not target_flag_col: + logging.warning(f"[DENIED] IP {client_ip} tidak terdaftar di TARGET_MAPPING. Abaikan.") + print(f"[DENIED] IP {client_ip} tidak terdaftar di TARGET_MAPPING. Abaikan.") + # Kirim jawaban kosong agar alat tidak hang + reply_msg = create_hl7_dsr_response(None, incoming_control_id, "") + conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8')) + continue # Skip proses selanjutnya + + logging.info(f"[TARGET] IP {client_ip} akan mengecek kolom: {target_flag_col}") + print(f"[TARGET] IP {client_ip} akan mengecek kolom: {target_flag_col}") + + # 2. Cari Sample ID di pesan QRY + search_sample_id = None + qrd_line = "" + for line in lines: + if line.startswith("QRD|"): + qrd_line = line + fields = line.split('|') + if len(fields) > 8: + search_sample_id = fields[8] + break + + if search_sample_id: + session = SessionLocal() + try: + # 3. Query Database Dinamis (Pakai getattr) + # Kita cari RN yang cocok DAN (Flag Kolom Tersebut False ATAU Null) + + # Ambil atribut kolom Paslab berdasarkan nama string (misal: Paslab.flg_gxp3) + flag_attr = getattr(PaslabOrder, target_flag_col, None) + + if flag_attr is None: + logging.error(f"Kolom '{target_flag_col}' tidak ditemukan di Model Paslab!") + print(f"Kolom '{target_flag_col}' tidak ditemukan di Model Paslab!") + raise Exception("Invalid Column Name") + + logging.info(f"[DB LOOKUP] Mencari {search_sample_id} dimana {target_flag_col} == False") + print(f"[DB LOOKUP] Mencari {search_sample_id} dimana {target_flag_col} == False") + + order = session.query(PaslabOrder).filter( + PaslabOrder.rnoreg == search_sample_id, + (flag_attr == False) | (flag_attr == None) + ).first() + + # Variabel kontrol kirim + send_order = False + + if order: + # (Opsional) Validasi Kapabilitas Mesin di sini jika perlu + # ... + send_order = True + + # 4. Kirim Respon + if send_order and order: + logging.info(f"[FOUND] Order ditemukan: {order.nama}. Mengirim ke {client_ip}...") + print(f"[FOUND] Order ditemukan: {order.nama}. Mengirim ke {client_ip}...") + + reply_msg = create_hl7_dsr_response(order, incoming_control_id, qrd_line) + conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8')) + + # 5. UPDATE FLAG DINAMIS (PENTING) + # Set kolom yang sesuai (misal flg_gxp3) menjadi True + setattr(order, target_flag_col, True) + 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}") + + else: + logging.warning(f"[NOT FOUND/ALREADY SENT] Tidak ada order baru untuk {search_sample_id} di kolom {target_flag_col}") + print(f"[NOT FOUND/ALREADY SENT] Tidak ada order baru untuk {search_sample_id} di kolom {target_flag_col}") + # Kirim DSR Kosong (NF) + reply_msg = create_hl7_dsr_response(None, incoming_control_id, qrd_line) + conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8')) + + except Exception as db_err: + logging.error(f"Database Error: {db_err}") + print(f"Database Error: {db_err}") + session.rollback() + finally: + session.close() + + # ========================================== + # SKENARIO 2: ALAT KIRIM HASIL (RESULT / ORU) + # ========================================== + elif "ORU^" in clean_hl7: + logging.info(f"[RESULT] Menerima Hasil Lab.") + + # 1. Parse dan Simpan Hasil (Panggil fungsi parser Anda) + parse_hl7_result(clean_hl7, msg_id, clean_hl7, device_name=f"GeneXpert-{addr[0]}") + + # 2. Kirim ACK (Terima Kasih) + ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S') + ack_msg = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{ack_time}||ACK|{incoming_control_id}|P|2.5\rMSA|AA|{incoming_control_id}\r" + full_ack = f"\x0b{ack_msg}\x1c\r" + conn.sendall(full_ack.encode('utf-8')) + logging.info(f"[ACK SENT] Untuk hasil ID {incoming_control_id}") + print(f"[ACK SENT] Untuk hasil ID {incoming_control_id}") + + # ========================================== + # SKENARIO 3: Handle Pesan Apa Adanya + # ========================================== + elif "QBP^Z03" in clean_hl7: print("[GENEXPERT] Alat meminta ORDER") msg_id = extract_msg_control_id(clean_hl7) send_all_orders(conn, addr[0], clean_hl7, msg_id) @@ -1599,16 +1776,6 @@ def manage_serial_port(config): else: logging.error(f"Tipe alat tidak diketahui: '{device_type}' untuk port {config.get('port')}. Thread dihentikan.") print(f"Tipe alat tidak diketahui: '{device_type}' untuk port {config.get('port')}. Thread dihentikan.") -# ========================================== -# 6. FLASK API (Opsional) -# ========================================== -@app.route('/status', methods=['GET']) -def status(): - with connection_lock: - return jsonify({ - "active_connections": list(active_genexpert_connections.keys()), - "total_connected": len(active_genexpert_connections) - }) # ========================================== # 7. MAIN EXECUTION @@ -1641,12 +1808,6 @@ if __name__ == "__main__": t_tcp.start() all_threads.append(t_tcp) - # 4. Start Thread Flask Web Server (API) - # PENTING: Flask juga harus di thread agar tidak memblokir monitoring - t_flask = threading.Thread(target=run_flask, name="WebServer-Flask", daemon=True) - t_flask.start() - all_threads.append(t_flask) - # 5. LOOP UTAMA (Keep-Alive & Monitoring) # Ini sekarang bisa berjalan karena Flask sudah dipindah ke thread try: