Fix BD ASTM frame reassembly and result parsing

This commit is contained in:
Dwi Swandhana
2026-05-07 05:23:37 +07:00
parent 04ad16f72a
commit 4a48063931
3 changed files with 155 additions and 107 deletions
Vendored
BIN
View File
Binary file not shown.
+59 -10
View File
@@ -664,7 +664,7 @@ class AstmMessageService
protected function splitBDAstmMessages(string $raw): array
{
// bersihkan karakter kontrol
$clean = preg_replace('/[\x02\x03\x04]/', '', $raw);
$clean = preg_replace('/[\x02\x03\x04\x17]/', '', $raw);
$lines = preg_split("/\r\n|\n|\r/", $clean);
@@ -698,17 +698,48 @@ class AstmMessageService
protected function reassembleAstmFrames(string $raw): string
{
$buffer = '';
$frames = [];
$frames = explode("\x02", $raw);
// ambil semua frame
preg_match_all('/\x02(\d)(.*?)\x03(..)/s', $raw, $frames);
foreach ($frames as $frame) {
if ($frame === '') {
continue;
}
$content = $frame;
$etxPos = strpos($content, "\x03");
$etbPos = strpos($content, "\x17");
if ($etxPos !== false && ($etbPos === false || $etxPos < $etbPos)) {
$content = substr($content, 0, $etxPos);
} elseif ($etbPos !== false) {
$content = substr($content, 0, $etbPos);
}
if ($content !== '' && ctype_digit($content[0])) {
$content = substr($content, 1);
}
foreach ($frames[2] as $content) {
// gabungkan isi frame (tanpa ETX & checksum)
$buffer .= $content;
}
return trim($buffer);
return trim($buffer) !== '' ? trim($buffer) : trim($raw);
}
protected function normalizeBDResult(?string $value): ?string
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
if (stripos($value, 'NEGATIVE') !== false) {
return 'NEGATIVE';
}
if (stripos($value, 'POSITIVE') !== false) {
return 'POSITIVE';
}
return explode('^', $value)[0] ?: null;
}
protected function processBDAstmResponse(string $raw, $data): bool
{
@@ -718,7 +749,8 @@ class AstmMessageService
// =========================
$accnumber= '';
$resultDateTime = null;
$rawClean = preg_replace('/[\x02\x03\x04]/', '', $raw);
$bdResult = null;
$rawClean = preg_replace('/[\x02\x03\x04\x17]/', '', $this->reassembleAstmFrames($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());
+96 -97
View File
@@ -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
# ==========================================