This commit is contained in:
Dwi Swandhana
2026-05-18 12:24:18 +07:00
parent c70d7ebb93
commit c78d15e67a
7 changed files with 343 additions and 84 deletions
@@ -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"),
+76 -47
View File
@@ -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] = "<strong>{$target_name}:</strong> {$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 = '<div class="genexpert-report" style="font-family: Arial, sans-serif;">';
$reporthtml .= '<h4 style="margin-bottom:10px;">GeneXpert Assay Result</h4>';
$reporthtml .= '<table border="1" cellpadding="5" style="width: 100%; border-collapse: collapse; text-align: left; margin-bottom: 20px;">';
$reporthtml .= '<tr style="background-color: #f4f4f4;">
// Wrapper Utama
$reporthtml = '<div class="genexpert-report" style="font-family: Arial, sans-serif; max-width: 900px; margin: auto; border: 1px solid #ddd; padding: 20px; border-radius: 8px;">';
$reporthtml .= '<h3 style="text-align: center; margin-top: 0; color: #2c3e50;">Hasil Pemeriksaan GeneXpert</h3>';
$reporthtml .= '<hr style="border: 0; border-top: 2px solid #3498db; margin-bottom: 20px;">';
// Bagian 1: Identitas Pasien (Grid Table)
$reporthtml .= '<table style="width: 100%; margin-bottom: 20px; font-size: 14px; border: none;">';
$reporthtml .= '<tr>';
$reporthtml .= '<td style="width: 15%; padding: 4px 0;"><strong>No. Reg LIS</strong></td>';
$reporthtml .= '<td style="width: 35%; padding: 4px 0;">: ' . htmlspecialchars($accession_number) . '</td>';
$reporthtml .= '<td style="width: 15%; padding: 4px 0;"><strong>ID Pasien</strong></td>';
$reporthtml .= '<td style="width: 35%; padding: 4px 0;">: ' . htmlspecialchars($patient_id) . '</td>';
$reporthtml .= '</tr>';
$reporthtml .= '<tr>';
$reporthtml .= '<td style="padding: 4px 0;"><strong>Nama Pasien</strong></td>';
$reporthtml .= '<td style="padding: 4px 0;">: ' . htmlspecialchars($patient_name) . '</td>';
$reporthtml .= '<td style="padding: 4px 0;"><strong>Waktu Tes</strong></td>';
$reporthtml .= '<td style="padding: 4px 0;">: ' . htmlspecialchars($waktu_periksa) . '</td>';
$reporthtml .= '</tr>';
$reporthtml .= '</table>';
// Bagian 2: Kesimpulan / Hasil Utama
$reporthtml .= '<div style="background-color: #f8f9fa; padding: 15px; border-left: 5px solid #2ecc71; margin-bottom: 25px; border-radius: 4px;">';
$reporthtml .= '<h4 style="margin: 0 0 10px 0; color: #2c3e50;">Kesimpulan Hasil:</h4>';
$reporthtml .= '<p style="margin: 0; font-size: 14px; line-height: 1.6;">' . $kesimpulan_html . '</p>';
$reporthtml .= '</div>';
// Bagian 3: Tabel Detail Analit
$reporthtml .= '<h4 style="margin-bottom:10px; color: #2c3e50;">Detail Target / Analit</h4>';
$reporthtml .= '<table border="1" cellpadding="8" style="width: 100%; border-collapse: collapse; text-align: left; margin-bottom: 25px; font-size: 13px;">';
$reporthtml .= '<tr style="background-color: #ecf0f1; color: #2c3e50;">
<th>Target/Analyte</th>
<th>Result</th>
<th>Ct Value</th>
<th>EndPt</th>
<th>End Point</th>
</tr>';
$chartLabels = [];
@@ -697,7 +733,6 @@ class AstmMessageService
<td>{$data['EndPt']}</td>
</tr>";
// 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 .= '</table>';
// 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 .= '<h4 style="margin-bottom:10px; color: #2c3e50; text-align: center;">Grafik Amplifikasi</h4>';
$reporthtml .= '<div style="width: 100%; max-width: 800px; margin: auto;">';
$reporthtml .= '<canvas id="'.$chartId.'"></canvas>';
$reporthtml .= '</div>';
// Tambahkan script Chart.js (asumsi CDN) dan inisialisasi
$reporthtml .= "
<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>
<script>
// Gunakan DOMContentLoaded agar script jalan setelah HTML di-render di frontend
document.addEventListener('DOMContentLoaded', function() {
var ctx = document.getElementById('{$chartId}').getContext('2d');
new Chart(ctx, {
type: 'bar', // Menggunakan kombinasi Bar & Line chart
type: 'bar',
data: {
labels: {$labelsJson},
datasets: [
@@ -745,6 +778,7 @@ class AstmMessageService
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
borderWidth: 2,
tension: 0.1,
yAxisID: 'y1'
}
]
@@ -762,8 +796,8 @@ class AstmMessageService
type: 'linear',
display: true,
position: 'right',
title: { display: true, text: 'EndPt' },
grid: { drawOnChartArea: false } // Agar garis grid tidak bertumpuk
title: { display: true, text: 'End Point' },
grid: { drawOnChartArea: false }
}
}
}
@@ -772,15 +806,12 @@ class AstmMessageService
</script>
";
}
$reporthtml .= '</div>';
$reporthtml .= '</div>'; // 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());
@@ -119,6 +119,18 @@
<td>Address</td>
<td id="alamat"></td>
</tr>
<tr>
<td>Tanggal Registrasi</td>
<td id="tanggalregis"></td>
</tr>
<tr>
<td>Tanggal Pengambilan</td>
<td id="tanggalsampel"></td>
</tr>
<tr>
<td>Cara Pengambilan</td>
<td id="pengambilan"></td>
</tr>
<tr>
<td>Status</td>
<td id="nostatus"></td>
@@ -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 {
@@ -175,6 +175,18 @@
<td>Address</td>
<td id="alamat"></td>
</tr>
<tr>
<td>Tanggal Registrasi</td>
<td id="tanggalregis"></td>
</tr>
<tr>
<td>Tanggal Pengambilan</td>
<td id="tanggalsampel"></td>
</tr>
<tr>
<td>Cara Pengambilan</td>
<td id="pengambilan"></td>
</tr>
<tr>
<td>Status</td>
<td id="nostatus"></td>
@@ -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 {
@@ -175,6 +175,18 @@
<td>Address</td>
<td id="alamat"></td>
</tr>
<tr>
<td>Tanggal Registrasi</td>
<td id="tanggalregis"></td>
</tr>
<tr>
<td>Tanggal Pengambilan</td>
<td id="tanggalsampel"></td>
</tr>
<tr>
<td>Cara Pengambilan</td>
<td id="pengambilan"></td>
</tr>
<tr>
<td>Status</td>
<td id="nostatus"></td>
@@ -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 {
+203 -33
View File
@@ -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: "<STX>",
0x03: "<ETX>",
0x04: "<EOT>",
0x05: "<ENQ>",
0x06: "<ACK>",
0x0D: "<CR>",
0x0A: "<LF>",
0x17: "<ETB>",
0x15: "<NAK>",
0x1C: "<FS>",
0x0B: "<VT>",
}
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({
+9 -3
View File
@@ -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!")