From c78d15e67ae3184028b21a4465cef4c49e774fe5 Mon Sep 17 00:00:00 2001 From: Dwi Swandhana Date: Mon, 18 May 2026 12:24:18 +0700 Subject: [PATCH] update --- .../app/Http/Controllers/ListController.php | 2 +- htdocs/app/Services/AstmMessageService.php | 123 +++++---- .../views/dokter/pemeriksaan.blade.php | 18 ++ htdocs/resources/views/dokter/ppds.blade.php | 18 ++ .../views/dokter/ppdsdeveloper.blade.php | 18 ++ listener/app.py | 236 +++++++++++++++--- listener/geneexpert.py | 12 +- 7 files changed, 343 insertions(+), 84 deletions(-) diff --git a/htdocs/app/Http/Controllers/ListController.php b/htdocs/app/Http/Controllers/ListController.php index 706db2eb..68b9a38e 100644 --- a/htdocs/app/Http/Controllers/ListController.php +++ b/htdocs/app/Http/Controllers/ListController.php @@ -255,7 +255,7 @@ class ListController extends Controller 'kd_spesimen', 'nm_spesimen', 'dlp', 'daftar', 'foto', 'tgldraft', 'nmdrafter', 'baca', 'nmpembaca', 'verifikasi', 'export', 'tgladendum', 'nmadendum', 'filefoto', 'pendaftar', 'status', 'created_at', 'updated_at', 'bhp_media', 'bhp_potsputum', 'bhp_poturine', 'bhp_oshe', 'bhp_obyekglass', 'bhp_botolbd', 'bhp_parafilm', - 'bhp_tips', 'bhp_cottonswab', 'bhp_antibiotiktambahan', 'id_esbl', 'id_mrsa', 'rating', 'nmpendaftar', + 'bhp_tips', 'bhp_cottonswab', 'bhp_antibiotiktambahan', 'id_esbl', 'id_mrsa', 'rating', 'nmpendaftar', 'tanggalregis', 'tanggalsampel', 'pengambilan', DB::raw("DATE(daftar) as daftartgl"), DB::raw("TO_CHAR(daftar, 'HH24:MI:SS') as daftarjam"), diff --git a/htdocs/app/Services/AstmMessageService.php b/htdocs/app/Services/AstmMessageService.php index f3267975..1dd37f65 100644 --- a/htdocs/app/Services/AstmMessageService.php +++ b/htdocs/app/Services/AstmMessageService.php @@ -605,13 +605,14 @@ class AstmMessageService { $normalizedData = str_replace(["\r\n", "\r"], "\n", $response); $segments = explode("\n", $normalizedData); + $patient_id = ''; $patient_name = ''; $accession_number = ''; + $waktu_periksa = date('d-m-Y H:i:s'); // Default jika tidak ada di ASTM - // Array untuk menyimpan data terstruktur $parsedResults = []; - $hasil_list = []; // Tetap dipertahankan untuk kebutuhan legacy/summary + $hasil_list = []; foreach ($segments as $rsegmen) { $cekdata = explode('|', $rsegmen); @@ -619,32 +620,37 @@ class AstmMessageService $recordType = preg_replace("/[^a-zA-Z]/", "", $cekdata[0]); + // 1. Parsing Pasien if ($recordType === 'P') { - $patient_id = $cekdata[3] ?? ($cekdata[4] ?? ''); - if (isset($cekdata[5])) { - $patient_name = trim(str_replace('^', ' ', $cekdata[5])); + $patient_id = !empty($cekdata[3]) ? $cekdata[3] : (!empty($cekdata[4]) ? $cekdata[4] : ''); + + $raw_name = isset($cekdata[5]) ? trim(str_replace('^', ' ', $cekdata[5])) : ''; + $patient_name = !empty(trim($raw_name)) ? $raw_name : $patient_id; + } + + // 2. Parsing Accession Number + if ($recordType === 'O') { + $accession_number = !empty($cekdata[2]) ? $cekdata[2] : $accession_number; + // Jika ada waktu di record O (index 6), kita bisa format (opsional) + if (!empty($cekdata[6])) { + $rawTime = preg_replace('/[^0-9]/', '', $cekdata[6]); + if (strlen($rawTime) >= 14) { + $waktu_periksa = date('d-m-Y H:i:s', strtotime(substr($rawTime, 0, 14))); + } } } - if ($recordType === 'O') { - $accession_number = $cekdata[2] ?? ''; - } - + // 3. Parsing Data Result, Ct, dan EndPt if ($recordType === 'R') { if (isset($cekdata[2]) && isset($cekdata[3])) { $test_info_raw = $cekdata[2]; $result_val = trim(str_replace('^', '', $cekdata[3])); if ($result_val !== '') { - // Pecah test info berdasarkan '^' $test_info = explode('^', $test_info_raw); - - // Pada format GeneXpert: index ke-6 biasanya nama target (FII, FII 20210G, dll) - // index ke-7 biasanya penanda tipe data (Ct, EndPt) - $target_name = $test_info[6] ?? 'Unknown Target'; - $metric_type = $test_info[7] ?? 'Result'; // Jika tidak ada, berarti ini Result (POS/NEG) + $target_name = !empty($test_info[6]) ? $test_info[6] : 'Unknown Target'; + $metric_type = (!empty($test_info[7])) ? $test_info[7] : 'Result'; - // Inisialisasi array untuk target ini jika belum ada if (!isset($parsedResults[$target_name])) { $parsedResults[$target_name] = [ 'Result' => '-', @@ -653,12 +659,11 @@ class AstmMessageService ]; } - // Masukkan nilai ke dalam struktur data $parsedResults[$target_name][$metric_type] = $result_val; - // Untuk summary legacy (kolom organism/additional_result) - if ($metric_type === 'Result') { - $hasil_list[] = $target_name . ": " . $result_val; + if ($metric_type === 'Result' && $result_val !== '') { + // Simpan semua result ke dalam list untuk kesimpulan + $hasil_list[$target_name] = "{$target_name}: {$result_val}"; } } } @@ -670,19 +675,50 @@ class AstmMessageService return false; } + // Buat string kesimpulan DULUAN sebelum membuat HTML + $kesimpulan_html = implode(" | ", array_values($hasil_list)); + $kesimpulan_db = strip_tags($kesimpulan_html); // Versi tanpa tag HTML untuk kolom DB + $kesimpulan_safe = substr($kesimpulan_db, 0, 100); + // ==================================================================== // MEMBUAT HTML REPORT & GRAFIK (CHART.JS) // ==================================================================== - // 1. Buat Tabel HTML - $reporthtml = '
'; - $reporthtml .= '

GeneXpert Assay Result

'; - $reporthtml .= ''; - $reporthtml .= ' + // Wrapper Utama + $reporthtml = '
'; + $reporthtml .= '

Hasil Pemeriksaan GeneXpert

'; + $reporthtml .= '
'; + + // Bagian 1: Identitas Pasien (Grid Table) + $reporthtml .= '
'; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= ''; + $reporthtml .= '
No. Reg LIS: ' . htmlspecialchars($accession_number) . 'ID Pasien: ' . htmlspecialchars($patient_id) . '
Nama Pasien: ' . htmlspecialchars($patient_name) . 'Waktu Tes: ' . htmlspecialchars($waktu_periksa) . '
'; + + // Bagian 2: Kesimpulan / Hasil Utama + $reporthtml .= '
'; + $reporthtml .= '

Kesimpulan Hasil:

'; + $reporthtml .= '

' . $kesimpulan_html . '

'; + $reporthtml .= '
'; + + // Bagian 3: Tabel Detail Analit + $reporthtml .= '

Detail Target / Analit

'; + $reporthtml .= ''; + $reporthtml .= ' - + '; $chartLabels = []; @@ -697,7 +733,6 @@ class AstmMessageService "; - // Siapkan data untuk grafik (hanya ambil target yang memiliki nilai numerik Ct/EndPt) if (is_numeric($data['Ct']) || is_numeric($data['EndPt'])) { $chartLabels[] = $target; $chartCtData[] = is_numeric($data['Ct']) ? (float)$data['Ct'] : 0; @@ -706,27 +741,25 @@ class AstmMessageService } $reporthtml .= '
Target/Analyte Result Ct ValueEndPtEnd Point
{$data['EndPt']}
'; - // 2. Buat Grafik menggunakan Chart.js (Inject Script) + // Bagian 4: Grafik Chart.js if (!empty($chartLabels)) { $chartId = 'gxChart_' . preg_replace('/[^a-zA-Z0-9]/', '', $accession_number); $labelsJson = json_encode($chartLabels); $ctJson = json_encode($chartCtData); $endPtJson = json_encode($chartEndPtData); - // Tambahkan Canvas untuk grafik + $reporthtml .= '

Grafik Amplifikasi

'; $reporthtml .= '
'; $reporthtml .= ''; $reporthtml .= '
'; - // Tambahkan script Chart.js (asumsi CDN) dan inisialisasi $reporthtml .= " "; } - $reporthtml .= '
'; + $reporthtml .= ''; // End of Wrapper Utama // ==================================================================== // SIMPAN KE DATABASE // ==================================================================== - $kesimpulan = implode(" | ", $hasil_list); - $kesimpulan_safe = substr($kesimpulan, 0, 100); - try { Periksa::where('nofoto', $accession_number) ->whereNotIn('status', ['Selesai', 'Arsip', 'Dibatalkan (Arsip)', 'Batal']) @@ -788,7 +819,6 @@ class AstmMessageService 'status' => 'Data GeneXpert di Terima' ]); - Riwayat::create([ 'nofoto' => $accession_number, 'jawaban' => $reporthtml, @@ -797,15 +827,14 @@ class AstmMessageService 'verifikasi' => '', ]); - $resultSample = ResultSample::firstOrNew(['accession_number' => $accession_number]); - $resultSample->sender_name = $alat; - $resultSample->patient_id = substr($patient_id, 0, 50); - $resultSample->patient_name_first = substr($patient_name, 0, 100); - $resultSample->organism = $kesimpulan_safe; // Tetap menyimpan text summary - $resultSample->additional_result = json_encode($parsedResults); // Menyimpan JSON utuh agar lebih informatif - $resultSample->message_datetime = date('Y-m-d H:i:s'); + $resultSample = ResultSample::firstOrNew(['accession_number' => $accession_number]); + $resultSample->sender_name = $alat; + $resultSample->patient_id = substr($patient_id, 0, 50); + $resultSample->patient_name_first = substr($patient_name, 0, 100); + $resultSample->organism = $kesimpulan_safe; // Disimpan versi plain text + $resultSample->additional_result = json_encode($parsedResults); + $resultSample->message_datetime = date('Y-m-d H:i:s'); $resultSample->save(); - Log::info("GeneXpert Result Disimpan", ["NoReg" => $accession_number, "Hasil" => $kesimpulan_safe]); return true; } catch (\Exception $e) { Log::error("GeneXpert DB Save Error: " . $e->getMessage()); diff --git a/htdocs/resources/views/dokter/pemeriksaan.blade.php b/htdocs/resources/views/dokter/pemeriksaan.blade.php index 1b307576..53fd34fa 100644 --- a/htdocs/resources/views/dokter/pemeriksaan.blade.php +++ b/htdocs/resources/views/dokter/pemeriksaan.blade.php @@ -119,6 +119,18 @@ Address + + Tanggal Registrasi + + + + Tanggal Pengambilan + + + + Cara Pengambilan + + Status @@ -5002,6 +5014,9 @@ { name: 'otherppds', type: 'text'}, { name: 'nmpendaftar', type: 'text'}, { name: 'alamatpasien', type: 'text'}, + { name: 'tanggalregis', type: 'text'}, + { name: 'tanggalsampel', type: 'text'}, + { name: 'pengambilan', type: 'text'}, ], type : 'GET', data : {jenis:jenis, mulai:mulai, akhir:akhir, valcari:valcari, poli:poli, master:master, _token: '{{csrf_token()}}'}, @@ -5180,6 +5195,9 @@ $("#tlppasien").html(dataRecord.tlppasien); $("#periksa_id").val(dataRecord.id); $("#ruangan").val(dataRecord.idruangan); + $("#tanggalregis").val(dataRecord.tanggalregis); + $("#tanggalsampel").val(dataRecord.tanggalsampel); + $("#pengambilan").val(dataRecord.pengambilan); if (dataRecord.dokter_id == '0' || dataRecord.dokter_id == null || dataRecord.dokter_id == ''){ $("#dokter").val(masteru).select2().trigger('change'); } else { diff --git a/htdocs/resources/views/dokter/ppds.blade.php b/htdocs/resources/views/dokter/ppds.blade.php index 1455d986..e3fe4bb3 100644 --- a/htdocs/resources/views/dokter/ppds.blade.php +++ b/htdocs/resources/views/dokter/ppds.blade.php @@ -175,6 +175,18 @@ Address + + Tanggal Registrasi + + + + Tanggal Pengambilan + + + + Cara Pengambilan + + Status @@ -5053,6 +5065,9 @@ { name: 'otherppds', type: 'text'}, { name: 'nmpendaftar', type: 'text'}, { name: 'alamatpasien', type: 'text'}, + { name: 'tanggalregis', type: 'text'}, + { name: 'tanggalsampel', type: 'text'}, + { name: 'pengambilan', type: 'text'}, ], type : 'GET', data : {jenis:jenis, mulai:mulai, akhir:akhir, valcari:valcari, poli:poli, master:master, _token: '{{csrf_token()}}'}, @@ -5229,6 +5244,9 @@ $("#tlppasien").html(dataRecord.tlppasien); $("#periksa_id").val(dataRecord.id); $("#ruangan").val(dataRecord.idruangan); + $("#tanggalregis").val(dataRecord.tanggalregis); + $("#tanggalsampel").val(dataRecord.tanggalsampel); + $("#pengambilan").val(dataRecord.pengambilan); if (dataRecord.dokter_id == '0' || dataRecord.dokter_id == null || dataRecord.dokter_id == ''){ $("#dokter").val(masteru).select2().trigger('change'); } else { diff --git a/htdocs/resources/views/dokter/ppdsdeveloper.blade.php b/htdocs/resources/views/dokter/ppdsdeveloper.blade.php index c1ea556b..3bfd25f0 100644 --- a/htdocs/resources/views/dokter/ppdsdeveloper.blade.php +++ b/htdocs/resources/views/dokter/ppdsdeveloper.blade.php @@ -175,6 +175,18 @@ Address + + Tanggal Registrasi + + + + Tanggal Pengambilan + + + + Cara Pengambilan + + Status @@ -5053,6 +5065,9 @@ { name: 'otherppds', type: 'text'}, { name: 'nmpendaftar', type: 'text'}, { name: 'alamatpasien', type: 'text'}, + { name: 'tanggalregis', type: 'text'}, + { name: 'tanggalsampel', type: 'text'}, + { name: 'pengambilan', type: 'text'}, ], type : 'GET', data : {jenis:jenis, mulai:mulai, akhir:akhir, valcari:valcari, poli:poli, master:master, _token: '{{csrf_token()}}'}, @@ -5229,6 +5244,9 @@ $("#tlppasien").html(dataRecord.tlppasien); $("#periksa_id").val(dataRecord.id); $("#ruangan").val(dataRecord.idruangan); + $("#tanggalregis").val(dataRecord.tanggalregis); + $("#tanggalsampel").val(dataRecord.tanggalsampel); + $("#pengambilan").val(dataRecord.pengambilan); if (dataRecord.dokter_id == '0' || dataRecord.dokter_id == null || dataRecord.dokter_id == ''){ $("#dokter").val(masteru).select2().trigger('change'); } else { diff --git a/listener/app.py b/listener/app.py index bae3b840..f6a6c439 100644 --- a/listener/app.py +++ b/listener/app.py @@ -59,6 +59,35 @@ def print(*args, **kwargs): _write_thread_log(message) return builtins.print(*args, **kwargs) +def _visible_bytes(data: bytes) -> str: + mapping = { + 0x02: "", + 0x03: "", + 0x04: "", + 0x05: "", + 0x06: "", + 0x0D: "", + 0x0A: "", + 0x17: "", + 0x15: "", + 0x1C: "", + 0x0B: "", + } + parts = [] + for b in data: + if b in mapping: + parts.append(mapping[b]) + elif 32 <= b <= 126: + parts.append(chr(b)) + else: + parts.append(f"<0x{b:02X}>") + return "".join(parts) + +def _hex_bytes(data: bytes, limit: int = 160) -> str: + clipped = data[:limit] + text = clipped.hex().upper() + return text + ("..." if len(data) > limit else "") + # ========================================== # 1. KONFIGURASI SISTEM # ========================================== @@ -481,7 +510,7 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"): base_orders = session.query(PaslabOrder).filter( (flag_attr == False) | (flag_attr == None) - ).order_by(PaslabOrder.urut.asc()).all() + ).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all() selected_orders = [] if requested_sample_id and requested_sample_id.upper() != "ALL": @@ -802,20 +831,21 @@ def handle_genexpert_client(conn, addr): # Decode ke string temp_str = full_message_bytes.decode('latin-1', errors='ignore') - # --- 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) - else: - # Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang) - pass else: if buffer: head_hex = buffer[:12].hex() @@ -957,10 +987,16 @@ def parse_genexpert_astm_records(astm_string, device_name): tgl_data=datetime.datetime.now().date(), rawdt=astm_string, # Simpan data mentahnya (utuh) ke TEXT untuk jaga-jaga/bisa dibaca ulang organisme=kesimpulan_safe, # Hasil yang sudah bersih dan muat - alat=device_name, - processed='N' # Atau menyesuaikan default sistem Anda + alat=device_name ) session.add(new_result) + source_ip = str(device_name or "").replace("GeneXpert-", "").strip() + flag_name = get_flag_by_device(source_ip) + if flag_name: + order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == seq_no_safe).first() + if order: + setattr(order, flag_name, True) + print(f"[GENEXPERT] Hasil ASTM diterima, set {flag_name}=TRUE untuk {seq_no_safe}") session.commit() print(f"[GENEXPERT-DB-SUCCESS] Hasil lab untuk Order {seq_no_safe} berhasil disimpan ke LisPhoenix!") @@ -1026,7 +1062,7 @@ def get_genexpert_query_orders(ip_addr, hl7_msg): base_orders = session.query(PaslabOrder).filter( (flag_attr == False) | (flag_attr == None) - ).order_by(PaslabOrder.urut.asc()).all() + ).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all() requested_sample_id = (param_2 or param_1).strip() if (param_2 or param_1) else "" if requested_sample_id and requested_sample_id.upper() != "ALL": @@ -1049,24 +1085,25 @@ def get_genexpert_query_orders(ip_addr, hl7_msg): finally: session.close() -def build_genexpert_response_msh(message_code, incoming_hl7, resp_control_id): +def build_genexpert_response_msh(message_code, incoming_hl7, resp_control_id, ip_addr=None): timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') msh_fields = extract_segment(incoming_hl7, "MSH").split('|') sender_app = msh_fields[2] if len(msh_fields) > 2 else "GeneXpert" sender_fac = msh_fields[3] if len(msh_fields) > 3 else "" - return f"MSH|^~\\&|LIS||{sender_app}|{sender_fac}|{timestamp}||{message_code}|{resp_control_id}|P|2.5|||NE|NE" + host_app = get_genexpert_host_application(ip_addr) + return f"MSH|^~\\&|{host_app}||{sender_app}|{sender_fac}|{timestamp}||{message_code}|{resp_control_id}|P|2.5|||NE|NE" -def create_genexpert_ack_j01_response(incoming_hl7): +def create_genexpert_ack_j01_response(incoming_hl7, ip_addr=None): incoming_control_id = extract_msg_control_id(incoming_hl7) or "UNKNOWN" resp_control_id = f"ACK{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" - msh = build_genexpert_response_msh("ACK^J01", incoming_hl7, resp_control_id) + msh = build_genexpert_response_msh("ACK^J01", incoming_hl7, resp_control_id, ip_addr=ip_addr) msa = f"MSA|CA|{incoming_control_id}" return f"{msh}\r{msa}\r" -def create_genexpert_ack_r01_response(incoming_hl7): +def create_genexpert_ack_r01_response(incoming_hl7, ip_addr=None): incoming_control_id = extract_msg_control_id(incoming_hl7) or "UNKNOWN" resp_control_id = f"ACK{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" - msh = build_genexpert_response_msh("ACK^R01", incoming_hl7, resp_control_id) + msh = build_genexpert_response_msh("ACK^R01", incoming_hl7, resp_control_id, ip_addr=ip_addr) msa = f"MSA|CA|{incoming_control_id}" return f"{msh}\r{msa}\r" @@ -1081,7 +1118,7 @@ def create_genexpert_rsp_z02_response(orders, incoming_hl7, ip_addr=None): if str(incoming_message_type or "").startswith("QBP^Z03"): query_name = "Z03^HOST QUERY" - msh = build_genexpert_response_msh("RSP^Z02", incoming_hl7, resp_control_id) + msh = build_genexpert_response_msh("RSP^Z02", incoming_hl7, resp_control_id, ip_addr=ip_addr) msa = f"MSA|AA|{query_tag}" qak = f"QAK|{query_tag}|OK|{query_name}" segments = [msh, msa, qak] @@ -1186,10 +1223,27 @@ def detect_genexpert_message_framing(message_bytes): raw = message_bytes or b"" if b"\x1c" in raw or raw.startswith(b"\x0b"): return "mllp" - if b"\x03" in raw: + if b"\x03" in raw or b"\x17" in raw: return "astm" return "plain" +def extract_astm_frame_text(frame_bytes): + raw = frame_bytes or b"" + if not raw.startswith(b"\x02"): + return "" + + etx_pos = raw.find(b"\x03") + etb_pos = raw.find(b"\x17") + if etx_pos != -1 and etb_pos != -1: + end_pos = min(etx_pos, etb_pos) + else: + end_pos = max(etx_pos, etb_pos) + + if end_pos == -1 or end_pos < 2: + return "" + + return raw[2:end_pos].decode("latin-1", errors="ignore") + def frame_genexpert_response(hl7_message, framing): message = str(hl7_message or "") if framing == "astm": @@ -1200,6 +1254,30 @@ def frame_genexpert_response(hl7_message, framing): return f"\x0b{message}\x1c\r".encode("utf-8") return message.encode("utf-8") +def build_genexpert_astm_frames(hl7_message, max_text_bytes=240): + message = str(hl7_message or "") + text_bytes = message.encode("latin-1") + chunks = [text_bytes[i:i + max_text_bytes] for i in range(0, len(text_bytes), max_text_bytes)] or [b""] + frames = [] + frame_number = 1 + + for index, chunk in enumerate(chunks): + is_last = index == len(chunks) - 1 + terminator = b"\x03" if is_last else b"\x17" + frame_no_byte = str(frame_number).encode("ascii") + frame_core_bytes = frame_no_byte + chunk + terminator + checksum = calculate_astm_checksum(frame_core_bytes.decode("latin-1", errors="ignore")).encode("ascii") + full_frame = b"\x02" + frame_core_bytes + checksum + b"\x0D\x0A" + frames.append({ + "frame_number": frame_number, + "is_last": is_last, + "chunk_len": len(chunk), + "payload": full_frame, + "checksum": checksum.decode("ascii", errors="ignore"), + }) + frame_number = (frame_number + 1) % 8 + return frames + def recv_genexpert_control_char(conn, timeout_seconds=5): previous_timeout = conn.gettimeout() try: @@ -1208,8 +1286,10 @@ def recv_genexpert_control_char(conn, timeout_seconds=5): finally: conn.settimeout(previous_timeout) -def send_genexpert_astm_frame(conn, ip_addr, payload, label=""): +def send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=""): try: + frames = build_genexpert_astm_frames(hl7_message) + print(f"[GENEXPERT-ASTM-TX] ip={ip_addr}, label={label}, total_frames={len(frames)}, mode=astm_active") conn.sendall(b"\x05") log_genexpert_handshake(ip_addr, "ENQ-TX", detail=f"label={label}") @@ -1223,25 +1303,46 @@ def send_genexpert_astm_frame(conn, ip_addr, payload, label=""): log_genexpert_handshake(ip_addr, "CTRL-RX", detail=f"phase=pre-frame,label={label},hex={ctrl.hex() if ctrl else 'timeout'}") return False - conn.sendall(payload) - log_genexpert_handshake(ip_addr, "FRAME-TX", detail=f"label={label},bytes={len(payload)}") + for frame in frames: + payload = frame["payload"] + debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=f"{label}:frame{frame['frame_number']}") + conn.sendall(payload) + log_genexpert_handshake( + ip_addr, + "FRAME-TX", + detail=( + f"label={label},frame_no={frame['frame_number']},bytes={len(payload)}," + f"chunk_len={frame['chunk_len']},last={frame['is_last']}" + ), + ) - ctrl = recv_genexpert_control_char(conn, timeout_seconds=5) - if ctrl == b"\x06": - log_genexpert_handshake(ip_addr, "ACK-RX", detail=f"phase=post-frame,label={label}") + ctrl = recv_genexpert_control_char(conn, timeout_seconds=15) + if ctrl == b"\x06": + log_genexpert_handshake(ip_addr, "ACK-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}") + continue + if ctrl == b"\x15": + log_genexpert_handshake(ip_addr, "NAK-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}") + conn.sendall(b"\x04") + log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=nak,frame_no={frame['frame_number']}") + return False + if ctrl == b"\x04": + log_genexpert_handshake(ip_addr, "EOT-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}") + conn.sendall(b"\x04") + log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after-peer-eot,frame_no={frame['frame_number']}") + return False + + log_genexpert_handshake( + ip_addr, + "CTRL-RX", + detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']},hex={ctrl.hex() if ctrl else 'timeout'}", + ) conn.sendall(b"\x04") - log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label}") - return True - if ctrl == b"\x15": - log_genexpert_handshake(ip_addr, "NAK-RX", detail=f"phase=post-frame,label={label}") - conn.sendall(b"\x04") - log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=nak") + log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=unexpected,frame_no={frame['frame_number']}") return False - log_genexpert_handshake(ip_addr, "CTRL-RX", detail=f"phase=post-frame,label={label},hex={ctrl.hex() if ctrl else 'timeout'}") conn.sendall(b"\x04") - log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=unexpected") - return False + log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label}") + return True except Exception as exc: log_genexpert_handshake(ip_addr, "ASTM-SEND-ERROR", detail=f"label={label},error={exc}") return False @@ -1263,8 +1364,15 @@ def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""): f"response_mode={response_mode}, label={label}, bytes={len(payload)}" ) if framing == "astm" and response_mode == "astm_active": - send_genexpert_astm_frame(conn, ip_addr, payload, label=label) + send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=label) return + if framing == "astm": + debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=label) + else: + print( + f"[GENEXPERT-FRAME-TX] ip={ip_addr}, label={label}, framing={framing}, " + f"hex={_hex_bytes(payload)}, visible={_visible_bytes(payload)}" + ) conn.sendall(payload) def send_genexpert_transport_ack(conn, ip_addr, framing, reason="frame-received"): @@ -1280,6 +1388,46 @@ def log_genexpert_handshake(ip_addr, event, detail=""): suffix = f", detail={detail}" if detail else "" print(f"[GENEXPERT-HANDSHAKE] ip={ip_addr}, event={event}{suffix}") +def debug_genexpert_astm_frame(ip_addr, frame_bytes, direction="RX", label=""): + raw = frame_bytes or b"" + suffix = f", label={label}" if label else "" + if not raw: + print(f"[GENEXPERT-ASTM-{direction}] ip={ip_addr}{suffix}, empty-frame") + return + + detail_parts = [ + f"len={len(raw)}", + f"hex={_hex_bytes(raw)}", + f"visible={_visible_bytes(raw)}", + ] + + if raw.startswith(b"\x02") and len(raw) >= 6: + frame_no = raw[1:2] + etx_pos = raw.find(b"\x03") + etb_pos = raw.find(b"\x17") + end_pos = etx_pos if etx_pos != -1 else etb_pos + end_name = "ETX" if etx_pos != -1 else ("ETB" if etb_pos != -1 else "NONE") + if end_pos != -1 and len(raw) >= end_pos + 5: + checksum_rx = raw[end_pos + 1:end_pos + 3] + trailer = raw[end_pos + 3:end_pos + 5] + checksum_basis = raw[1:end_pos + 1].decode("latin-1", errors="ignore") + checksum_calc = calculate_astm_checksum(checksum_basis).encode("ascii") + checksum_ok = checksum_rx.upper() == checksum_calc.upper() + trailer_ok = trailer == b"\r\n" + detail_parts.extend([ + f"frame_no={frame_no.decode('ascii', errors='ignore')}", + f"terminator=<{end_name}>", + f"checksum_rx={checksum_rx.decode('ascii', errors='ignore')}", + f"checksum_calc={checksum_calc.decode('ascii', errors='ignore')}", + f"checksum_ok={checksum_ok}", + f"trailer={_visible_bytes(trailer)}", + f"trailer_ok={trailer_ok}", + ]) + else: + detail_parts.append(f"terminator=<{end_name}>") + + print(f"[GENEXPERT-ASTM-{direction}] ip={ip_addr}{suffix}, " + ", ".join(detail_parts)) + def debug_genexpert_order_message(hl7_message, ip_addr=None): segments = parse_hl7_segments(hl7_message) current_pid = "" @@ -1367,6 +1515,28 @@ def clear_genexpert_inflight_for_ip(ip_addr, reason="cleared"): return True return False +def stop_all_scheduled_result_queries(reason="no-active-genexpert"): + with scheduled_result_query_lock: + scheduled_result_queries.clear() + with genexpert_query_inflight_lock: + genexpert_query_inflight_by_ip.clear() + print(f"[GENEXPERT-SCHEDULER] Nonaktif. Clear jadwal, reason={reason}") + return 0 + +def stop_scheduled_result_query(accnumber, reason="completed"): + accnumber = str(accnumber or "").strip() + with scheduled_result_query_lock: + scheduled_result_queries.pop(accnumber, None) + print(f"[GENEXPERT-SCHEDULER] Nonaktif. Stop accnumber={accnumber}, reason={reason}") + return False + +def schedule_result_query_for_order(accnumber, register_no, target_ip=None, **kwargs): + print( + f"[GENEXPERT-SCHEDULER] Nonaktif. Jadwal query hasil dilewati " + f"untuk accnumber={accnumber}, target_ip={target_ip}" + ) + return False + @app.route("/api/genexpert/query-result", methods=["POST"]) def api_query_genexpert_result(): return jsonify({ diff --git a/listener/geneexpert.py b/listener/geneexpert.py index ad29bb5d..07eeed2e 100644 --- a/listener/geneexpert.py +++ b/listener/geneexpert.py @@ -455,7 +455,7 @@ def get_genexpert_query_orders(ip_addr, hl7_msg): base_orders = session.query(PaslabOrder).filter( (flag_attr == False) | (flag_attr == None) - ).order_by(PaslabOrder.urut.asc()).all() + ).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all() requested_sample_id = (param_2 or param_1).strip() if (param_2 or param_1) else "" if requested_sample_id and requested_sample_id.upper() != "ALL": @@ -672,7 +672,7 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"): base_orders = session.query(PaslabOrder).filter( (flag_attr == False) | (flag_attr == None) - ).order_by(PaslabOrder.urut.asc()).all() + ).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all() selected_orders = [] if requested_sample_id and requested_sample_id.upper() != "ALL": @@ -2458,9 +2458,15 @@ def parse_genexpert_astm_records(astm_string, device_name): rawdt=astm_string, # Simpan data mentahnya (utuh) ke TEXT untuk jaga-jaga/bisa dibaca ulang organisme=kesimpulan_safe, # Hasil yang sudah bersih dan muat alat=device_name, - processed='N' # Atau menyesuaikan default sistem Anda ) session.add(new_result) + source_ip = str(device_name or "").replace("GeneXpert-", "").strip() + flag_name = get_flag_by_device(source_ip) + if flag_name: + order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == seq_no_safe).first() + if order: + setattr(order, flag_name, True) + print(f"[GENEXPERT] Hasil ASTM diterima, set {flag_name}=TRUE untuk {seq_no_safe}") session.commit() print(f"[GENEXPERT-DB-SUCCESS] Hasil lab untuk Order {seq_no_safe} berhasil disimpan ke LisPhoenix!")