From 4a48063931aaf53d3e576e9b82200eb4a15b6729 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Thu, 7 May 2026 05:23:37 +0700 Subject: [PATCH] Fix BD ASTM frame reassembly and result parsing --- .DS_Store | Bin 10244 -> 10244 bytes htdocs/app/Services/AstmMessageService.php | 69 ++++++-- listener/app.py | 193 ++++++++++----------- 3 files changed, 155 insertions(+), 107 deletions(-) diff --git a/.DS_Store b/.DS_Store index 7403d6a4bfaba90e0bb74ae106b200ddcfc8a8d7..1662f77ae1759b6b58f3415dadb9cb644ba9ed07 100644 GIT binary patch delta 133 zcmZn(XbIS$Di9lVi2(>$81xv@88R74a`RnWl5+BsfMOg^C3je^yK>wSRXzo;d_jg` vaB_Zb0mxA9W1E`5~hjL^j`$(%}UFzG5ck delta 133 zcmZn(XbIS$DiG`Rpp1cmfrUYjA)O(Up(Hoo#U&{xKM5$tq5kC9<0qGnJEF>`;FT}P xFbq!4&n*DzVc=fBxkreassembleAstmFrames($raw)); $lines = preg_split("/\r\n|\n|\r/", $rawClean); $parsed = [ @@ -771,14 +803,18 @@ class AstmMessageService } elseif (str_starts_with($line, 'R|')) { $f = explode('|', $line); $test = explode('^', $f[2] ?? ''); + $bdResult = $this->normalizeBDResult($f[3] ?? null) ?? $bdResult; + $resultDateTime = $this->astmToDateTime($f[12] ?? null) + ?? $this->astmToDateTime($f[11] ?? null) + ?? $resultDateTime; // Instrument detail di akhir baris preg_match('/(MGIT960|BACTECFX)[^|]*/', $line, $inst); $parsed['result'] = [ 'organism' => $test[2] ?? null, - 'test_status' => explode('^', $f[3] ?? '')[0], - 'result_status_datetime' => $this->astmToDateTime($f[13] ?? $resultDateTime), + 'test_status' => $bdResult ?? explode('^', $f[3] ?? '')[0], + 'result_status_datetime' => $resultDateTime, ]; $parsed['instrument'] = [ @@ -834,6 +870,19 @@ class AstmMessageService ] ); } + if ($accnumber != '' && $bdResult){ + KomponenJawaban::updateOrCreate( + [ + 'accnumber' => $accnumber, + 'komponen' => 'bd_result', + 'isidata' => $bdResult, + ], + [ + 'template' => 'Kultur', + 'created_by' => 'BD' + ] + ); + } //Log::info("Data BD ASTM Berhasil di Parse dan di simpan ", $resultSample->toArray()); diff --git a/listener/app.py b/listener/app.py index 95f80990..93da78ab 100644 --- a/listener/app.py +++ b/listener/app.py @@ -568,7 +568,7 @@ def detect_genexpert_message_framing(message_bytes): def frame_genexpert_response(hl7_message, framing): message = str(hl7_message or "") if framing == "astm": - frame_body = f"{message}\x03" + frame_body = f"1{message}\x03" chk = calculate_astm_checksum(frame_body) return f"\x02{frame_body}{chk}\r\n".encode("latin-1") if framing == "mllp": @@ -2128,9 +2128,61 @@ 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 process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing): + 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 + + if "ORU^" in clean_hl7: + 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) + 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) + 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}") buffer = b"" + pending_astm_hl7 = None + pending_astm_framing = None conn.settimeout(60) client_ip = addr[0] with connection_lock: @@ -2152,6 +2204,8 @@ def handle_genexpert_client(conn, addr): log_genexpert_handshake(addr[0], "ETX-RX", detail=f"bytes={len(data)}") if b"\x04" in data: log_genexpert_handshake(addr[0], "EOT-RX", detail=f"bytes={len(data)}") + if b"\x06" in data: + log_genexpert_handshake(addr[0], "ACK-RX", detail=f"bytes={len(data)}") if b"\x15" in data: log_genexpert_handshake(addr[0], "NAK-RX", detail=f"bytes={len(data)}") @@ -2168,6 +2222,9 @@ def handle_genexpert_client(conn, addr): 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'') + if b'\x06' in buffer: + log_genexpert_handshake(addr[0], "ACK-BUFFER-CLEAR", detail=f"buffer_len={len(buffer)}") + buffer = buffer.replace(b'\x06', b'') # --- 2. CEK APAKAH PESAN SUDAH LENGKAP? --- # Kita cari tanda akhir pesan umum: @@ -2197,6 +2254,11 @@ def handle_genexpert_client(conn, addr): if end_marker_pos == 0 and buffer[:1] == b'\x04': log_genexpert_handshake(addr[0], "EOT-CLEAR", detail="standalone-eot") buffer = buffer[1:].lstrip(b'\r').lstrip(b'\n') + if pending_astm_hl7: + log_genexpert_handshake(addr[0], "ASTM-MSG-PROCESS", detail=f"framing={pending_astm_framing}") + process_genexpert_hl7_message(conn, addr[0], pending_astm_hl7, pending_astm_framing or "astm") + pending_astm_hl7 = None + pending_astm_framing = None continue # Ambil pesan dari awal sampai marker @@ -2232,74 +2294,12 @@ def handle_genexpert_client(conn, addr): if "MSH|" in temp_str: msh_index = temp_str.find("MSH|") clean_hl7 = temp_str[msh_index:] - log_genexpert_hl7("IN", addr[0], 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 - # ========================================== - # SKENARIO 1: LEGACY QRY TIDAK DIDUKUNG UNTUK GENEXPERT - # ========================================== - if "QRY^" in clean_hl7 or "QRY|" in clean_hl7: - print( - f"[GENEXPERT] Legacy QRY diterima dari {addr[0]} tetapi diabaikan. " - "Host hanya mendukung QBP^Z01/QBP^Z03 untuk order query." - ) + 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 - - # ========================================== - # SKENARIO 2: ALAT KIRIM HASIL (RESULT / ORU) - # ========================================== - elif "ORU^" in clean_hl7: - print(f"[RESULT] Menerima Hasil Lab.") - - # 1. Parse dan Simpan Hasil (Panggil fungsi parser Anda) - parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{addr[0]}") - - # 2. Kirim ACK (Terima Kasih) - ack_msg = create_genexpert_ack_r01_response(clean_hl7) - send_genexpert_response(conn, addr[0], ack_msg, response_framing, label="oru-ack") - print(f"[ACK SENT] Untuk hasil ID {incoming_control_id}") - continue - - # ========================================== - # SKENARIO 3: Handle Pesan Apa Adanya - # ========================================== - elif "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, addr[0], clean_hl7, msg_id, response_framing=response_framing) - continue - - elif "QCN^J01" in clean_hl7: - clear_genexpert_inflight_for_ip(addr[0], reason="query-confirmation") - ack_msg = create_genexpert_ack_j01_response(clean_hl7) - send_genexpert_response(conn, addr[0], ack_msg, response_framing, label="qcn-ack") - 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]}...") - - # Panggil Parser - parse_hl7_result(conn, msg_id, clean_hl7, device_name=f"GeneXpert-{addr[0]}, ") - - # --- KIRIM ACK --- - try: - lines = clean_hl7.split('\r') - msh_fields = lines[0].split('|') - 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" - - # Kirim ACK format MLLP - full_ack = f"\x0b{ack_msg}\x1c\r" - log_genexpert_hl7("OUT", addr[0], 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}") + process_genexpert_hl7_message(conn, addr[0], clean_hl7, response_framing) else: # Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang) pass @@ -3057,7 +3057,7 @@ def manage_bd_port(config): print(f"[{port_name}] Membuka port untuk alat {alat_name}...") # Buffer untuk menampung pecahan data - rx_buffer = b"" + rx_buffer = "" try: with serial.Serial( @@ -3078,44 +3078,43 @@ def manage_bd_port(config): data_chunk = ser.read(ser.in_waiting or 1024) if data_chunk: - # 1. Handle Handshake Awal (ENQ) - if b'\x05' in data_chunk: - print(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.") - ser.write(b'\x06') # ACK - rx_buffer = "" # Reset buffer untuk data baru - continue - - # 2. Handle Akhir Transmisi (EOT) - if b'\x04' in data_chunk: - print(f"[{port_name}] Terima EOT (Selesai). Memproses data...") - # Proses semua data yang terkumpul di buffer - parse_and_save_bd_result(rx_buffer, alat_name) - rx_buffer = "" # Kosongkan buffer setelah save - continue + try: + # 1. Handle Handshake Awal (ENQ) + if b'\x05' in data_chunk: + print(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.") + ser.write(b'\x06') # ACK + rx_buffer = "" # Reset buffer untuk data baru + data_chunk = data_chunk.replace(b'\x05', b'') - # 3. Handle Data Frame Normal (STX ... ETX) - # ASTM butuh kita balas ACK setiap kali dikirim Frame - # Frame biasanya diawali STX (\x02) - if b'\x02' in data_chunk: - # Simpan ke buffer (decode dulu ke string) - try: - decoded_str = data_chunk.decode('utf-8', errors='ignore') - rx_buffer += decoded_str - - # WAJIB: Kirim ACK agar alat lanjut kirim baris berikutnya + # 2. Simpan semua byte data yang tersisa. + # Chunk lanjutan frame bisa datang tanpa STX, jadi tidak boleh dibuang. + chunk_without_eot = data_chunk.replace(b'\x04', b'') + if chunk_without_eot: + rx_buffer += chunk_without_eot.decode('latin-1', errors='ignore') + + # 3. Handle Data Frame Normal (STX ... ETX/ETB) + # ASTM butuh ACK setiap frame baru agar alat lanjut kirim frame berikutnya. + if b'\x02' in data_chunk: ser.write(b'\x06') # logging.debug(f"[{port_name}] Frame diterima, ACK dikirim.") print(f"[{port_name}] Frame diterima, ACK dikirim.") - except Exception as e: - logging.error(f"Error decode frame: {e}") - print(f"Error decode frame: {e}") + + # 4. Handle Akhir Transmisi (EOT) + if b'\x04' in data_chunk: + print(f"[{port_name}] Terima EOT (Selesai). Memproses data...") + parse_and_save_bd_result(rx_buffer, alat_name) + rx_buffer = "" # Kosongkan buffer setelah save + + except Exception as e: + logging.error(f"Error decode frame: {e}") + print(f"Error decode frame: {e}") continue # Loop lagi untuk ambil sisa data except Exception as e: logging.error(f"[{port_name}] Error Reading: {e}") print(f"[{port_name}] Error Reading: {e}") - rx_buffer = b"" # Reset jika error parah + rx_buffer = "" # Reset jika error parah # ==========================================