update
This commit is contained in:
+121
-384
@@ -35,59 +35,6 @@ logging.basicConfig(level=logging.INFO, handlers=[log_handler])
|
||||
THREAD_LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thread_logs")
|
||||
thread_log_lock = threading.Lock()
|
||||
|
||||
def _sanitize_thread_log_name(thread_name):
|
||||
safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(thread_name or "main").strip())
|
||||
return safe_name or "main"
|
||||
|
||||
def _write_thread_log(message):
|
||||
try:
|
||||
os.makedirs(THREAD_LOG_DIR, exist_ok=True)
|
||||
thread_name = threading.current_thread().name
|
||||
safe_thread_name = _sanitize_thread_log_name(thread_name)
|
||||
log_path = os.path.join(THREAD_LOG_DIR, f"{safe_thread_name}.log")
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with thread_log_lock:
|
||||
with open(log_path, "a", encoding="utf-8") as fh:
|
||||
fh.write(f"{timestamp} | {message}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def print(*args, **kwargs):
|
||||
sep = kwargs.get("sep", " ")
|
||||
end = kwargs.get("end", "\n")
|
||||
message = sep.join(str(arg) for arg in args)
|
||||
_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
|
||||
# ==========================================
|
||||
@@ -132,17 +79,17 @@ GENEXPERT_HOST_APPLICATION_BY_IP = {
|
||||
# Kanan: 'Host Test Code' dari Dokumen Word Anda
|
||||
GENEXPERT_TEST_MAPPING = {
|
||||
# Mapping untuk IP 10.10.120.75 (Multi-Assay)
|
||||
"HIV": "HIV-1_VL", # Xpert HIV-1 Viral Load XC Version 3
|
||||
"HIV": "HIV1-VL", # Xpert HIV-1 Viral Load XC Version 3
|
||||
"TCM TB": "MTBRIF", # Xpert MTBRIF Assay G4 Version 6
|
||||
"TCM TB ULTRA": "MTBRIF_ULTRA2", # Xpert MTBRIF Ultra Version 4
|
||||
"TCM TB ULTRA": "MTBRIF", # Xpert MTBRIF Ultra Version 4
|
||||
"TCM TB XDR": "MTB-XDR", # Xpert MTB-XDR Version 1
|
||||
"HCV VL": "HCV", # Xpert HCV Viral Load Version 1
|
||||
"COVID-19": "COV-2 2", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.1 TCM COVID-19": "COV-2 2", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.2 PCR COVID-19": "COV-2 2", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.1 TCM COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.2 PCR COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"E.2.5 HCV TCM": "HCV",
|
||||
"18.1.1 TCM HCV": "HCV",
|
||||
"18.1.2 TCM HIV VIRAL LOAD": "HIV-1_VL",
|
||||
"18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL",
|
||||
"18.1.4 TCM HPV": "HCV",
|
||||
"7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
|
||||
"5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
|
||||
@@ -180,7 +127,7 @@ GENEXPERT_TEST_MAPPING = {
|
||||
|
||||
}
|
||||
GENEXPERT_IP_CAPABILITIES = {
|
||||
"10.10.120.75": ["MTBRIF", "MTBRIF_ULTRA2", "MTB-XDR", "HIV-1_VL", "COV-2 2"],
|
||||
"10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR", "HCV VL", "SARSCOV2FLURSV"],
|
||||
"10.10.120.13": ["HCV", "HBV"],
|
||||
"10.10.120.73": ["MTBRIF"]
|
||||
}
|
||||
@@ -308,6 +255,59 @@ Base.metadata.create_all(bind=engine)
|
||||
# ==========================================
|
||||
# 3. HL7 HELPER FUNCTIONS
|
||||
# ==========================================
|
||||
def _sanitize_thread_log_name(thread_name):
|
||||
safe_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", str(thread_name or "main").strip())
|
||||
return safe_name or "main"
|
||||
|
||||
def _write_thread_log(message):
|
||||
try:
|
||||
os.makedirs(THREAD_LOG_DIR, exist_ok=True)
|
||||
thread_name = threading.current_thread().name
|
||||
safe_thread_name = _sanitize_thread_log_name(thread_name)
|
||||
log_path = os.path.join(THREAD_LOG_DIR, f"{safe_thread_name}.log")
|
||||
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with thread_log_lock:
|
||||
with open(log_path, "a", encoding="utf-8") as fh:
|
||||
fh.write(f"{timestamp} | {message}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def print(*args, **kwargs):
|
||||
sep = kwargs.get("sep", " ")
|
||||
end = kwargs.get("end", "\n")
|
||||
message = sep.join(str(arg) for arg in args)
|
||||
_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 "")
|
||||
|
||||
def get_flag_by_device(ip_addr):
|
||||
for flag, ip in TARGET_MAPPING.items():
|
||||
if ip == ip_addr:
|
||||
@@ -546,173 +546,76 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"):
|
||||
session.close()
|
||||
|
||||
def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing):
|
||||
# ==========================================================
|
||||
# 1. BLOK PENANGANAN ASTM (Karena tidak diawali "MSH|")
|
||||
# ==========================================================
|
||||
if not str(clean_hl7 or "").startswith("MSH|"):
|
||||
records = parse_astm_records(clean_hl7)
|
||||
record_types = [rec.split("|", 1)[0] for rec in records if rec]
|
||||
print(f"[GENEXPERT-ASTM] ip={ip_addr}, record_types={record_types}")
|
||||
records = parse_astm_records(clean_hl7)
|
||||
record_types = [rec.split("|", 1)[0] for rec in records if rec]
|
||||
print(f"[GENEXPERT-ASTM] ip={ip_addr}, record_types={record_types}")
|
||||
|
||||
# A. Cek Jika Alat Meminta Order (Query)
|
||||
if any(rec.startswith("Q|") for rec in records):
|
||||
print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)")
|
||||
send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm")
|
||||
return
|
||||
# A. Cek Jika Alat Meminta Order (Query)
|
||||
if any(rec.startswith("Q|") for rec in records):
|
||||
print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)")
|
||||
send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm")
|
||||
return
|
||||
|
||||
# B. Cek Jika Alat Mengirim Hasil Lab (Result)
|
||||
if any(rec.startswith("R|") for rec in records):
|
||||
print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.")
|
||||
# Memanggil fungsi parser Anda untuk menyimpan hasil ke DB
|
||||
parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}")
|
||||
return
|
||||
# B. Cek Jika Alat Mengirim Hasil Lab (Result)
|
||||
if any(rec.startswith("R|") for rec in records):
|
||||
print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.")
|
||||
# Memanggil fungsi parser Anda untuk menyimpan hasil ke DB
|
||||
parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}")
|
||||
return
|
||||
|
||||
# C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment)
|
||||
if any(rec.startswith("C|") for rec in records):
|
||||
# 1. Ekstrak teks komentar untuk ditampilkan di log
|
||||
comments = [rec for rec in records if rec.startswith("C|")]
|
||||
for c in comments:
|
||||
parts = c.split('|')
|
||||
comment_text = parts[3] if len(parts) > 3 else c
|
||||
print(f"[GENEXPERT-ASTM-INFO] Komentar dari Alat: {comment_text}")
|
||||
# C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment)
|
||||
if any(rec.startswith("C|") for rec in records):
|
||||
# 1. Ekstrak teks komentar untuk ditampilkan di log
|
||||
comments = [rec for rec in records if rec.startswith("C|")]
|
||||
for c in comments:
|
||||
parts = c.split('|')
|
||||
comment_text = parts[3] if len(parts) > 3 else c
|
||||
print(f"[GENEXPERT-ASTM-INFO] Komentar dari Alat: {comment_text}")
|
||||
|
||||
# 2. Ekstrak NoReg (Nomor Order) dari record 'O'
|
||||
rnoreg = None
|
||||
for rec in records:
|
||||
if rec.startswith("O|"):
|
||||
o_parts = rec.split('|')
|
||||
if len(o_parts) > 2:
|
||||
rnoreg = o_parts[2].strip()
|
||||
break
|
||||
|
||||
# 3. Update Database PaslabOrder
|
||||
if rnoreg:
|
||||
# Cari kolom flag yang cocok dengan IP yang sedang terkoneksi
|
||||
target_flag_col = None
|
||||
for flag_col, mapped_ip in TARGET_MAPPING.items():
|
||||
if mapped_ip == ip_addr:
|
||||
target_flag_col = flag_col
|
||||
break
|
||||
|
||||
# 2. Ekstrak NoReg (Nomor Order) dari record 'O'
|
||||
rnoreg = None
|
||||
for rec in records:
|
||||
if rec.startswith("O|"):
|
||||
o_parts = rec.split('|')
|
||||
if len(o_parts) > 2:
|
||||
rnoreg = o_parts[2].strip()
|
||||
break
|
||||
|
||||
# 3. Update Database PaslabOrder
|
||||
if rnoreg:
|
||||
# Cari kolom flag yang cocok dengan IP yang sedang terkoneksi
|
||||
target_flag_col = None
|
||||
for flag_col, mapped_ip in TARGET_MAPPING.items():
|
||||
if mapped_ip == ip_addr:
|
||||
target_flag_col = flag_col
|
||||
break
|
||||
|
||||
if target_flag_col:
|
||||
try:
|
||||
# Buka sesi database dan update
|
||||
with SessionLocal() as session:
|
||||
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg).first()
|
||||
if order:
|
||||
# Set flag mesin tersebut menjadi True agar tidak dikirim ulang
|
||||
setattr(order, target_flag_col, True)
|
||||
session.commit()
|
||||
print(f"[GENEXPERT-DB] Order {rnoreg} ditolak alat. Flag {target_flag_col} di-set True (Selesai).")
|
||||
else:
|
||||
print(f"[GENEXPERT-DB] Order {rnoreg} tidak ditemukan di database saat memproses penolakan.")
|
||||
except Exception as e:
|
||||
print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg}: {e}")
|
||||
if target_flag_col:
|
||||
try:
|
||||
# Buka sesi database dan update
|
||||
with SessionLocal() as session:
|
||||
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg).first()
|
||||
if order:
|
||||
# Set flag mesin tersebut menjadi True agar tidak dikirim ulang
|
||||
setattr(order, target_flag_col, True)
|
||||
session.commit()
|
||||
print(f"[GENEXPERT-DB] Order {rnoreg} ditolak alat. Flag {target_flag_col} di-set True (Selesai).")
|
||||
else:
|
||||
print(f"[GENEXPERT-DB] Order {rnoreg} tidak ditemukan di database saat memproses penolakan.")
|
||||
except Exception as e:
|
||||
print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg}: {e}")
|
||||
|
||||
print(f"[GENEXPERT-ASTM] Transaksi penolakan order selesai diproses.")
|
||||
return
|
||||
|
||||
# D. Jika hanya berisi H dan L tanpa ada transaksi berarti (Status Echo)
|
||||
if set(record_types).issubset({'H', 'L'}):
|
||||
print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.")
|
||||
return
|
||||
|
||||
print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}")
|
||||
print(f"[GENEXPERT-ASTM] Transaksi penolakan order selesai diproses.")
|
||||
return
|
||||
|
||||
# ==========================================================
|
||||
# 2. BLOK PENANGANAN HL7 (Fallback / Cadangan)
|
||||
# ==========================================================
|
||||
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."
|
||||
)
|
||||
# D. Jika hanya berisi H dan L tanpa ada transaksi berarti (Status Echo)
|
||||
if set(record_types).issubset({'H', 'L'}):
|
||||
print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.")
|
||||
return
|
||||
|
||||
if "ORU^" in clean_hl7:
|
||||
# --- [PATCH] CEK APAKAH INI PESAN ERROR / PENOLAKAN ---
|
||||
if "|Error^" in clean_hl7 or "|X\r" in clean_hl7 or "\rTE|" in clean_hl7:
|
||||
print(f"[GENEXPERT-ERROR] Mesin {ip_addr} menolak tes (Test Unknown/Disabled).")
|
||||
|
||||
# Coba ekstrak NoReg dari segmen SPM agar kita bisa mengunci ordernya (set Flag = True)
|
||||
rnoreg_error = None
|
||||
for line in clean_hl7.split('\r'):
|
||||
if line.startswith('SPM|'):
|
||||
parts = line.split('|')
|
||||
if len(parts) > 2:
|
||||
rnoreg_error = parts[2].replace('^', '').strip()
|
||||
break
|
||||
|
||||
if rnoreg_error:
|
||||
# Cari kolom flag yang cocok dengan IP yang sedang terkoneksi
|
||||
target_flag_col = None
|
||||
for flag_col, mapped_ip in TARGET_MAPPING.items():
|
||||
if mapped_ip == ip_addr:
|
||||
target_flag_col = flag_col
|
||||
break
|
||||
|
||||
if target_flag_col:
|
||||
try:
|
||||
# Buka sesi database dan update
|
||||
with SessionLocal() as session:
|
||||
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg_error).first()
|
||||
if order:
|
||||
# Set flag mesin tersebut menjadi True agar tidak dikirim ulang
|
||||
setattr(order, target_flag_col, True)
|
||||
session.commit()
|
||||
print(f"[GENEXPERT-DB] Order {rnoreg_error} ditolak alat. Flag {target_flag_col} di-set True (Selesai).")
|
||||
else:
|
||||
print(f"[GENEXPERT-DB] Order {rnoreg_error} tidak ditemukan di database saat memproses penolakan.")
|
||||
except Exception as e:
|
||||
print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg_error}: {e}")
|
||||
|
||||
# Kirim ACK agar alat berhenti mengirim error
|
||||
ack_msg = create_genexpert_ack_r01_response(clean_hl7, ip_addr=ip_addr)
|
||||
send_genexpert_response(conn, ip_addr, ack_msg, response_framing, label="oru-ack")
|
||||
return # BERHENTI DI SINI. Jangan lanjut ke parse_hl7_result!
|
||||
# -----------------------------------------------------
|
||||
print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}")
|
||||
return
|
||||
|
||||
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, ip_addr=ip_addr)
|
||||
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, ip_addr=ip_addr)
|
||||
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|^~\\&|MyLIS|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}")
|
||||
@@ -1486,9 +1389,8 @@ def debug_genexpert_order_message(hl7_message, ip_addr=None):
|
||||
|
||||
def build_genexpert_result_query(accnumber, msg_control_id):
|
||||
ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
# Query hasil berbasis accession number di QRD-8.
|
||||
query_msg = (
|
||||
f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{ts}||QRY^Q02|{msg_control_id}|P|2.5\r"
|
||||
f"MSH|^~\\&|MyLIS|Mikrobiology|GeneXpert|Cepheid|{ts}||QRY^Q02|{msg_control_id}|P|2.5\r"
|
||||
f"QRD|{ts}|R|I|{msg_control_id}|||1^RD|{accnumber}|OTH|||T\r"
|
||||
)
|
||||
return query_msg
|
||||
@@ -1506,9 +1408,6 @@ def clear_genexpert_inflight_for_ip(ip_addr, reason="cleared"):
|
||||
state = genexpert_query_inflight_by_ip.pop(ip_addr, None)
|
||||
|
||||
if state:
|
||||
logging.info(
|
||||
f"[GENEXPERT-QUERY] Clear inflight ip={ip_addr}, reason={reason}, accnumber={state.get('accnumber')}"
|
||||
)
|
||||
print(
|
||||
f"[GENEXPERT-QUERY] Clear inflight ip={ip_addr}, reason={reason}, accnumber={state.get('accnumber')}"
|
||||
)
|
||||
@@ -1544,168 +1443,6 @@ def api_query_genexpert_result():
|
||||
"message": "Mode GeneXpert pasif aktif. Host hanya menjawab request dari alat.",
|
||||
}), 409
|
||||
|
||||
def parse_hl7_result(conn, msg_id, hl7_message, device_name="GeneXpert"):
|
||||
session = SessionLocal()
|
||||
clean_hl7 = hl7_message
|
||||
try:
|
||||
# 1. Bersihkan dan Split Pesan
|
||||
# HL7 dipisahkan oleh \r (Carriage Return)
|
||||
if "MSH|" not in hl7_message:
|
||||
logging.warning("[HL7] Data tidak mengandung segmen MSH valid.")
|
||||
print("[HL7] Data tidak mengandung segmen MSH valid.")
|
||||
return
|
||||
start_idx = hl7_message.find("MSH|")
|
||||
hl7_message = hl7_message[start_idx:]
|
||||
|
||||
segments = hl7_message.strip().split('\r')
|
||||
|
||||
# Variabel penampung
|
||||
sample_id = "" # no_id
|
||||
patient_id = "" # seq_no
|
||||
patient_name = "" # rnmpas
|
||||
result_date = None # tgl_data
|
||||
results_list = [] # untuk organisme
|
||||
message_type = ""
|
||||
|
||||
# Waktu default jika tidak ada di pesan
|
||||
result_date = datetime.datetime.now()
|
||||
|
||||
# 2. Loop setiap segmen
|
||||
for segment in segments:
|
||||
fields = segment.split('|')
|
||||
if not fields: continue
|
||||
|
||||
seg_type = fields[0]
|
||||
|
||||
# --- MSH (Header) ---
|
||||
if seg_type == 'MSH':
|
||||
if len(fields) > 8 and fields[8]:
|
||||
message_type = fields[8].strip()
|
||||
# Ambil tanggal pesan (Field 7) Format: YYYYMMDDHHMMSS
|
||||
if len(fields) > 6 and fields[6]:
|
||||
try:
|
||||
raw_date = fields[6][:14] # Ambil 14 digit pertama
|
||||
result_date = datetime.datetime.strptime(raw_date, "%Y%m%d%H%M%S")
|
||||
except ValueError:
|
||||
pass # Gunakan default datetime.now() jika format salah
|
||||
|
||||
# --- PID (Patient ID) ---
|
||||
elif seg_type == 'PID':
|
||||
# PID|3 = Patient ID (seq_no)
|
||||
if len(fields) > 3:
|
||||
patient_id = fields[3].replace('^', '')
|
||||
|
||||
# PID|5 = Patient Name (rnmpas)
|
||||
if len(fields) > 5:
|
||||
# Ganti caret ^ dengan spasi (Family^Name -> Family Name)
|
||||
patient_name = fields[5].replace('^', ' ').strip()
|
||||
|
||||
# --- OBR (Observation Request - Info Sample) ---
|
||||
elif seg_type == 'OBR':
|
||||
# OBR|2 atau OBR|3 biasanya berisi Sample ID / Accession No
|
||||
# Kita coba ambil field 3 (Filler Order Number) dulu, kalau kosong field 2
|
||||
if len(fields) > 3 and fields[3]:
|
||||
sample_id = fields[3].replace('^', '')
|
||||
elif len(fields) > 2 and fields[2]:
|
||||
sample_id = fields[2].replace('^', '')
|
||||
|
||||
# --- OBX (Observation Result - Hasil Tes) ---
|
||||
elif seg_type == 'OBX':
|
||||
# OBX|3 = Test Name/Code (Misal: MTB, RIF)
|
||||
# OBX|5 = Result Value (Misal: DETECTED, NOT DETECTED)
|
||||
if len(fields) > 5:
|
||||
test_name = fields[3].split('^')[1] if '^' in fields[3] else fields[3]
|
||||
test_val = fields[5].replace('^', ' ')
|
||||
|
||||
# Gabungkan nama tes dan hasil
|
||||
# Contoh: "MTB: DETECTED"
|
||||
results_list.append(f"{test_name}: {test_val}")
|
||||
|
||||
# 3. Gabungkan semua hasil OBX menjadi satu string
|
||||
if not results_list:
|
||||
final_result = "No Result Found"
|
||||
else:
|
||||
final_result = "; ".join(results_list)
|
||||
|
||||
# 4. Validasi Data Penting
|
||||
if not sample_id:
|
||||
if str(message_type).startswith("ORU^"):
|
||||
source_ip = ""
|
||||
try:
|
||||
source_ip = str(conn.getpeername()[0]).strip()
|
||||
except Exception:
|
||||
source_ip = str(device_name).replace("GeneXpert-", "").replace(",", " ").strip()
|
||||
|
||||
if source_ip:
|
||||
logging.info(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.")
|
||||
print(f"[HL7 Parser] ORU tanpa sample_id dari {source_ip}. Anggap sebagai request order.")
|
||||
send_all_orders(conn, source_ip, clean_hl7, msg_id)
|
||||
else:
|
||||
logging.warning("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.")
|
||||
print("[HL7 Parser] ORU tanpa sample_id diterima, tetapi IP sumber tidak dapat diidentifikasi.")
|
||||
else:
|
||||
logging.info(f"[HL7 Parser] Pesan {message_type or 'UNKNOWN'} tanpa sample_id diabaikan.")
|
||||
print(f"[HL7 Parser] Pesan {message_type or 'UNKNOWN'} tanpa sample_id diabaikan.")
|
||||
return
|
||||
|
||||
mapped_no_id = sample_id
|
||||
mapped_seq_no = patient_id
|
||||
mapped_alat = device_name
|
||||
pending_event = None
|
||||
|
||||
with pending_query_lock:
|
||||
pending = pending_result_queries.get(sample_id)
|
||||
if pending:
|
||||
source_ip = str(device_name).replace("GeneXpert-", "").strip()
|
||||
mapped_no_id = pending.get("register_no") or sample_id
|
||||
mapped_seq_no = sample_id
|
||||
mapped_alat = f"GeneXpert-{source_ip}" if source_ip else device_name
|
||||
pending["status"] = "found"
|
||||
pending["source_ip"] = source_ip
|
||||
pending["response_at"] = datetime.datetime.now()
|
||||
pending_event = pending.get("event")
|
||||
clear_genexpert_inflight_for_ip(source_ip, reason="result-received")
|
||||
|
||||
source_ip = ""
|
||||
try:
|
||||
source_ip = str(conn.getpeername()[0]).strip()
|
||||
except Exception:
|
||||
source_ip = str(device_name).replace("GeneXpert-", "").replace(",", " ").strip()
|
||||
|
||||
if source_ip:
|
||||
flag_name = get_flag_by_device(source_ip)
|
||||
if flag_name:
|
||||
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == sample_id).first()
|
||||
if order:
|
||||
setattr(order, flag_name, True)
|
||||
print(f"[GENEXPERT] Hasil diterima, set {flag_name}=TRUE untuk {sample_id}")
|
||||
|
||||
print(f"[HL7 Parser] Menyimpan hasil untuk Sample: {sample_id}")
|
||||
|
||||
# 5. Simpan ke Database
|
||||
new_data = LisPhoenix(
|
||||
no_id=mapped_no_id,
|
||||
seq_no=mapped_seq_no,
|
||||
rnmpas=patient_name,
|
||||
tgl_data=result_date,
|
||||
rawdt=hl7_message,
|
||||
organisme=final_result,
|
||||
alat=mapped_alat
|
||||
)
|
||||
|
||||
session.add(new_data)
|
||||
session.commit()
|
||||
print(f"[DB] Berhasil simpan ke LisPhoenix: {sample_id}")
|
||||
stop_scheduled_result_query(sample_id, reason="result-received")
|
||||
if pending_event:
|
||||
pending_event.set()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[HL7 Parser] Error menyimpan data: {e}")
|
||||
print(f"[HL7 Parser] Error menyimpan data: {e}")
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
|
||||
"""
|
||||
|
||||
+15
-21
@@ -22,7 +22,7 @@ from sqlalchemy.orm import declarative_base, sessionmaker # type: ignore
|
||||
# Logging Setup
|
||||
# Konfigurasi logging per hari
|
||||
log_handler = TimedRotatingFileHandler(
|
||||
filename="app.log",
|
||||
filename="geneexpert.log",
|
||||
when="midnight",
|
||||
interval=1,
|
||||
backupCount=7,
|
||||
@@ -116,9 +116,9 @@ GENEXPERT_RESPONSE_MODE_BY_IP = {
|
||||
}
|
||||
GENEXPERT_HOST_APPLICATION_DEFAULT = "DE002"
|
||||
GENEXPERT_HOST_APPLICATION_BY_IP = {
|
||||
"10.10.120.75": "GE01",
|
||||
"10.10.120.13": "DE002",
|
||||
"10.10.120.73": "GE01",
|
||||
"10.10.120.75": "GE01", # GenExpert Kecil
|
||||
"10.10.120.13": "DE002", # GenExpert Tempat Lama
|
||||
"10.10.120.73": "GE01", # GenExpert Besa
|
||||
}
|
||||
# Mapping Flag ke IP Address GeneXpert
|
||||
# Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode)
|
||||
@@ -135,17 +135,17 @@ TARGET_MAPPING = {
|
||||
# Kanan: 'Host Test Code' dari Dokumen Word Anda
|
||||
GENEXPERT_TEST_MAPPING = {
|
||||
# Mapping untuk IP 10.10.120.75 (Multi-Assay)
|
||||
"HIV": "HIV-1_VL", # Xpert HIV-1 Viral Load XC Version 3
|
||||
"HIV": "HIV1-VL", # Xpert HIV-1 Viral Load XC Version 3
|
||||
"TCM TB": "MTBRIF", # Xpert MTBRIF Assay G4 Version 6
|
||||
"TCM TB ULTRA": "MTBRIF_ULTRA2", # Xpert MTBRIF Ultra Version 4
|
||||
"TCM TB ULTRA": "MTBRIF", # Xpert MTBRIF Ultra Version 4
|
||||
"TCM TB XDR": "MTB-XDR", # Xpert MTB-XDR Version 1
|
||||
"HCV VL": "HCV", # Xpert HCV Viral Load Version 1
|
||||
"COVID-19": "COV-2 2", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.1 TCM COVID-19": "COV-2 2", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.2 PCR COVID-19": "COV-2 2", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.1 TCM COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"17.3.2 PCR COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2
|
||||
"E.2.5 HCV TCM": "HCV",
|
||||
"18.1.1 TCM HCV": "HCV",
|
||||
"18.1.2 TCM HIV VIRAL LOAD": "HIV-1_VL",
|
||||
"18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL",
|
||||
"18.1.4 TCM HPV": "HCV",
|
||||
"7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
|
||||
"5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
|
||||
@@ -183,7 +183,7 @@ GENEXPERT_TEST_MAPPING = {
|
||||
|
||||
}
|
||||
GENEXPERT_IP_CAPABILITIES = {
|
||||
"10.10.120.75": ["MTBRIF", "MTBRIF_ULTRA2", "MTB-XDR", "HIV-1_VL", "COV-2 2"],
|
||||
"10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR", "HCV VL", "SARSCOV2FLURSV"],
|
||||
"10.10.120.13": ["HCV", "HBV"],
|
||||
"10.10.120.73": ["MTBRIF"]
|
||||
}
|
||||
@@ -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.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all()
|
||||
).order_by(PaslabOrder.urut.asc()).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":
|
||||
@@ -611,7 +611,7 @@ def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""):
|
||||
f"assay_code={assay_code}, assay_source={assay_source}, capability_match={capability_match}"
|
||||
)
|
||||
|
||||
records.append(f"P|{index}|{patient_id}||{patient_id}|{patient_name}|||{sex}")
|
||||
records.append(f"P|{index}|{patient_name}||{patient_id}|{patient_name}|||{sex}")
|
||||
records.append(f"O|1|{sample_id}||^^^{assay_code}|R|{order_ts}|||||||||ORH||||||||||A")
|
||||
|
||||
records.append("L|1|N")
|
||||
@@ -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.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all()
|
||||
).order_by(PaslabOrder.urut.asc()).all()
|
||||
|
||||
selected_orders = []
|
||||
if requested_sample_id and requested_sample_id.upper() != "ALL":
|
||||
@@ -2460,18 +2460,12 @@ def parse_genexpert_astm_records(astm_string, device_name):
|
||||
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!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[GENEXPERT-PARSER-ERROR] Gagal memparsing/menyimpan hasil: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def manage_tcp_server():
|
||||
|
||||
Reference in New Issue
Block a user