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 .= '| No. Reg LIS | ';
+ $reporthtml .= ': ' . htmlspecialchars($accession_number) . ' | ';
+ $reporthtml .= 'ID Pasien | ';
+ $reporthtml .= ': ' . htmlspecialchars($patient_id) . ' | ';
+ $reporthtml .= '
';
+ $reporthtml .= '';
+ $reporthtml .= '| Nama Pasien | ';
+ $reporthtml .= ': ' . htmlspecialchars($patient_name) . ' | ';
+ $reporthtml .= 'Waktu Tes | ';
+ $reporthtml .= ': ' . htmlspecialchars($waktu_periksa) . ' | ';
+ $reporthtml .= '
';
+ $reporthtml .= '
';
+
+ // Bagian 2: Kesimpulan / Hasil Utama
+ $reporthtml .= '
';
+ $reporthtml .= '
Kesimpulan Hasil:
';
+ $reporthtml .= '
' . $kesimpulan_html . '
';
+ $reporthtml .= '
';
+
+ // Bagian 3: Tabel Detail Analit
+ $reporthtml .= '
Detail Target / Analit
';
+ $reporthtml .= '
';
+ $reporthtml .= '
| Target/Analyte |
Result |
Ct Value |
- EndPt |
+ End Point |
';
$chartLabels = [];
@@ -697,7 +733,6 @@ class AstmMessageService
{$data['EndPt']} |
";
- // 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 .= '
';
- // 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!")