Fix BD ASTM frame reassembly and result parsing
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
# ==========================================
|
||||
|
||||
Reference in New Issue
Block a user