This commit is contained in:
Dwi Swandhana
2026-08-06 17:57:59 +07:00
parent 3e6b70d7bd
commit 73a9ac103e
3 changed files with 298 additions and 158 deletions

No files matched your search

@@ -1274,7 +1274,7 @@ class FrontpageController extends Controller
'orderid' => $notransaksi, 'orderid' => $notransaksi,
]); ]);
$pesan = $nofoto; $pesan = $nofoto;
$daftaridtcm = [79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; $daftaridtcm = [77,78,79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146];
if (in_array($poli_id, $daftaridtcm)){ if (in_array($poli_id, $daftaridtcm)){
$genexpert = false; $genexpert = false;
} else { } else {
@@ -1504,7 +1504,7 @@ class FrontpageController extends Controller
'orderid' => $notransaksi, 'orderid' => $notransaksi,
]); ]);
$pesan = $nofoto; $pesan = $nofoto;
$daftaridtcm = [79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146]; $daftaridtcm = [77,78,79,80,81,82,83,84,203, 14,28,55,62,94,104,105,124,138,146];
if (in_array($poli_id, $daftaridtcm)){ if (in_array($poli_id, $daftaridtcm)){
$genexpert = false; $genexpert = false;
} else { } else {
+259 -154
View File
@@ -14,7 +14,6 @@ import datetime
import traceback import traceback
import serial # type: ignore import serial # type: ignore
from flask import Flask, jsonify, request # type: ignore
from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text # type: ignore from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text # type: ignore
from sqlalchemy import DateTime as SqDateTime # type: ignore from sqlalchemy import DateTime as SqDateTime # type: ignore
from sqlalchemy import Date as SqDate # type: ignore from sqlalchemy import Date as SqDate # type: ignore
@@ -54,11 +53,8 @@ ERROR_LOG_KEYWORDS = (
# 1. KONFIGURASI SISTEM # 1. KONFIGURASI SISTEM
# ========================================== # ==========================================
# Global Variables # Global Variables
app = Flask(__name__)
active_genexpert_connections = {} active_genexpert_connections = {}
connection_lock = threading.Lock() connection_lock = threading.Lock()
pending_result_queries = {}
pending_query_lock = threading.Lock()
scheduled_result_queries = {} scheduled_result_queries = {}
scheduled_result_query_lock = threading.Lock() scheduled_result_query_lock = threading.Lock()
genexpert_query_inflight_by_ip = {} genexpert_query_inflight_by_ip = {}
@@ -66,26 +62,23 @@ genexpert_query_inflight_lock = threading.Lock()
# Network Configuration # Network Configuration
TCP_LISTENER_PORT = 6001 # PC GeneXpert set ke mode Client, konek ke IP:PORT ini TCP_LISTENER_PORT = 6001 # PC GeneXpert set ke mode Client, konek ke IP:PORT ini
SERVER_HOST = '0.0.0.0' # Listen di semua interface SERVER_HOST = '0.0.0.0' # Listen di semua interface
HTTP_API_PORT = 6002 # Endpoint trigger dari Laravel -> Python
GENEXPERT_RESPONSE_MODE_DEFAULT = "astm_active" GENEXPERT_RESPONSE_MODE_DEFAULT = "astm_active"
GENEXPERT_RESPONSE_MODE_BY_IP = { GENEXPERT_RESPONSE_MODE_BY_IP = {
# "10.10.120.73": "astm_active",
# "10.10.120.75": "hl7_passive", # "10.10.120.75": "hl7_passive",
} }
GENEXPERT_HOST_APPLICATION_DEFAULT = "DE002"
GENEXPERT_HOST_APPLICATION_BY_IP = {
"10.10.120.75": "GE01", # GenExpert Kecil
"10.10.120.108": "DE002", # GenExpert Tempat Lama
"10.10.120.73": "GE01", # GenExpert Besa
}
# Mapping Flag ke IP Address GeneXpert # Mapping Flag ke IP Address GeneXpert
# Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode) # Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode)
TARGET_MAPPING = { TARGET_MAPPING = {
'flg_gxp1': '10.10.120.73', 'flg_gxp1': '10.10.120.73',
'flg_gxp2': '10.10.120.13', 'flg_gxp2': '10.10.120.108',
'flg_gxp3': '10.10.120.75' 'flg_gxp3': '10.10.120.75'
} }
GENEXPERT_HOST_APPLICATION_DEFAULT = "GE01"
GENEXPERT_HOST_APPLICATION_BY_IP = {
"10.10.120.75": "GE01",
"10.10.120.13": "DE002",
"10.10.120.73": "GE01",
}
# GeneXpert Configuration # GeneXpert Configuration
# ========================================== # ==========================================
# KONFIGURASI MAPPING TES (DATABASE -> GENEXPERT) # KONFIGURASI MAPPING TES (DATABASE -> GENEXPERT)
@@ -94,43 +87,45 @@ GENEXPERT_HOST_APPLICATION_BY_IP = {
# Kanan: 'Host Test Code' dari Dokumen Word Anda # Kanan: 'Host Test Code' dari Dokumen Word Anda
GENEXPERT_TEST_MAPPING = { GENEXPERT_TEST_MAPPING = {
# Mapping untuk IP 10.10.120.75 (Multi-Assay) # Mapping untuk IP 10.10.120.75 (Multi-Assay)
"HIV": "HIV1-VL", # Xpert HIV-1 Viral Load XC Version 3 "HIV": "HIV1-VL",
"TCM TB": "MTBRIF", # Xpert MTBRIF Assay G4 Version 6 "HBV": "HBVVL",
"TCM TB ULTRA": "MTBRIF", # Xpert MTBRIF Ultra Version 4 "TCM TB": "MTBRIF",
"TCM TB XDR": "MTB-XDR", # Xpert MTB-XDR Version 1 "TCM TB ULTRA": "MTBRIF",
"HCV VL": "HCV", # Xpert HCV Viral Load Version 1 "TCM TB XDR": "MTB-XDR 2",
"COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2 "HCV VL": "HCV",
"17.3.1 TCM COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2 "COVID-19": "SARSCOV2FLURSV",
"17.3.2 PCR COVID-19": "SARSCOV2FLURSV", # Xpert Xpress SARS-CoV-2 Version 2 "17.3.1 TCM COVID-19": "SARSCOV2FLURSV",
"17.3.2 PCR COVID-19": "SARSCOV2FLURSV",
"E.2.5 HCV TCM": "HCV", "E.2.5 HCV TCM": "HCV",
"18.1.1 TCM HCV": "HCV", "18.1.1 TCM HCV": "HCV",
"18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL", "18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL",
"18.1.4 TCM HPV": "HCV", "18.1.4 TCM HPV": "HCV",
"7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
"5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
"5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"3.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL) ": "MTB-XDR", "3.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL) ": "MTB-XDR 2",
"2.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "2.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"1.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "1.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"1.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "1.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"15.2.1 KULTUR TB MEDIA LJ": "MTB-XDR", "15.2.1 KULTUR TB MEDIA LJ": "MTB-XDR 2",
"8.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "8.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"9.3.5 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "9.3.5 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"9.3.6 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "9.3.6 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"12.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "12.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"H.2.5 PEMERIKSAAN KULTUR MYCROBACTERIUM TBC": "MTB-XDR", "H.2.5 PEMERIKSAAN KULTUR MYCROBACTERIUM TBC": "MTB-XDR 2",
"12.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "12.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"11.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "11.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"10.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "10.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"10.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "10.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"3.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "3.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"15.2.2 KULTUR TB MEDIA MGIT (AUTOMATIC)": "MTB-XDR", "15.2.2 KULTUR TB MEDIA MGIT (AUTOMATIC)": "MTB-XDR 2",
"11.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR", "11.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTB-XDR 2",
"8.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "8.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"7.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR", "7.3.6 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
"2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF", "2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF",
"12.3.8 TCM TB (GENE EXPERT)": "MTBRIF", "12.3.8 TCM TB (GENE EXPERT)": "MTBRIF",
"15.2.3 TCM TB (GENE EXPERT)": "MTBRIF", "15.2.3 TCM TB (GENE EXPERT)": "MTBRIF",
"15.2.3 TCM TB (GENE EXPERT) GENE EXPERT": "MTBRIF",
"2.3.9 TCM GENE EXPERT": "MTBRIF", "2.3.9 TCM GENE EXPERT": "MTBRIF",
"3.3.8 TCM GENE EXPERT": "MTBRIF", "3.3.8 TCM GENE EXPERT": "MTBRIF",
"3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF", "3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF",
@@ -142,11 +137,10 @@ GENEXPERT_TEST_MAPPING = {
} }
GENEXPERT_IP_CAPABILITIES = { GENEXPERT_IP_CAPABILITIES = {
"10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR", "HCV VL", "SARSCOV2FLURSV"], "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"],
"10.10.120.13": ["HCV", "HBV"], "10.10.120.73": ["MTBRIF", "HBVVL", "HIV1-VL", "HCV", "SARSCOV2FLURSV"],
"10.10.120.73": ["MTBRIF"] "10.10.120.108": ["MTBRIF", "HCV", "HIV1-VL", "HBVVL", "SARSCOV2FLURSV"],
} }
# Default code jika nama tes di database tidak dikenali
DEFAULT_GXP_CODE = "MTBRIF" DEFAULT_GXP_CODE = "MTBRIF"
DEVICE_CONFIGS = [ DEVICE_CONFIGS = [
@@ -452,10 +446,6 @@ def manage_tcp_server():
logging.critical(f"[TCP-SERVER] Gagal Start: {e}") logging.critical(f"[TCP-SERVER] Gagal Start: {e}")
print(f"[TCP-SERVER] Gagal Start: {e}") print(f"[TCP-SERVER] Gagal Start: {e}")
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 get_genexpert_host_application(ip_addr): def get_genexpert_host_application(ip_addr):
ip_addr = str(ip_addr or "").strip() ip_addr = str(ip_addr or "").strip()
host_app = GENEXPERT_HOST_APPLICATION_BY_IP.get(ip_addr, GENEXPERT_HOST_APPLICATION_DEFAULT) host_app = GENEXPERT_HOST_APPLICATION_BY_IP.get(ip_addr, GENEXPERT_HOST_APPLICATION_DEFAULT)
@@ -490,6 +480,28 @@ def parse_genexpert_astm_query(message_text):
query["query_sample_id"] = fields[2] if len(fields) > 2 else "" query["query_sample_id"] = fields[2] if len(fields) > 2 else ""
return query return query
def summarize_genexpert_astm_orders(records):
summaries = []
for rec in records:
if not rec.startswith("O|"):
continue
fields = rec.split("|")
sample_id = fields[2].strip() if len(fields) > 2 else ""
container_id = fields[3].strip() if len(fields) > 3 else ""
assay_code = ""
if len(fields) > 4:
assay_parts = [part.strip() for part in fields[4].split("^") if part.strip()]
assay_code = assay_parts[-1] if assay_parts else fields[4].strip()
priority = fields[5].strip() if len(fields) > 5 else ""
action_code = fields[11].strip() if len(fields) > 11 else ""
summaries.append(
f"sample_id={sample_id or '-'}, container_id={container_id or '-'}, "
f"assay={assay_code or '-'}, priority={priority or '-'}, action={action_code or '-'}"
)
return summaries
def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""): def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""):
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
records = [ records = [
@@ -521,7 +533,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}" 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(f"O|1|{sample_id}||^^^{assay_code}|R|{order_ts}|||||||||ORH||||||||||A")
records.append("L|1|N") records.append("L|1|N")
@@ -583,76 +595,166 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"):
session.close() session.close()
def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing): def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing):
records = parse_astm_records(clean_hl7) # ==========================================================
record_types = [rec.split("|", 1)[0] for rec in records if rec] # 1. BLOK PENANGANAN ASTM (Karena tidak diawali "MSH|")
print(f"[GENEXPERT-ASTM] ip={ip_addr}, record_types={record_types}") # ==========================================================
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}")
# A. Cek Jika Alat Meminta Order (Query) # A. Cek Jika Alat Meminta Order (Query)
if any(rec.startswith("Q|") for rec in records): if any(rec.startswith("Q|") for rec in records):
print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)") print(f"[GENEXPERT-ASTM] Alat meminta ORDER (Q Record)")
send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm") send_all_orders_astm(conn, ip_addr, clean_hl7, response_framing="astm")
return return
# B. Cek Jika Alat Mengirim Hasil Lab (Result) # B. Cek Jika Alat Mengirim Hasil Lab (Result)
if any(rec.startswith("R|") for rec in records): if any(rec.startswith("R|") for rec in records):
print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.") print(f"[RESULT] Menerima Hasil Lab ASTM dari {ip_addr}.")
# Memanggil fungsi parser Anda untuk menyimpan hasil ke DB # Memanggil fungsi parser Anda untuk menyimpan hasil ke DB
parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}") parse_genexpert_astm_records(clean_hl7, device_name=f"GeneXpert-{ip_addr}")
return return
# C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment) # C. [PERBAIKAN] Cek Jika Alat Mengirim Komentar/Penolakan (Comment)
if any(rec.startswith("C|") for rec in records): if any(rec.startswith("C|") for rec in records):
# 1. Ekstrak teks komentar untuk ditampilkan di log # 1. Ekstrak teks komentar untuk ditampilkan di log
comments = [rec for rec in records if rec.startswith("C|")] comments = [rec for rec in records if rec.startswith("C|")]
for c in comments: for c in comments:
parts = c.split('|') parts = c.split('|')
comment_text = parts[3] if len(parts) > 3 else c comment_text = parts[3] if len(parts) > 3 else c
print(f"[GENEXPERT-ASTM-INFO] Komentar dari Alat: {comment_text}") 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
if target_flag_col: # 2. Ekstrak NoReg (Nomor Order) dari record 'O'
try: rnoreg = None
# Buka sesi database dan update for rec in records:
with SessionLocal() as session: if rec.startswith("O|"):
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == rnoreg).first() o_parts = rec.split('|')
if order: if len(o_parts) > 2:
# Set flag mesin tersebut menjadi True agar tidak dikirim ulang rnoreg = o_parts[2].strip()
setattr(order, target_flag_col, True) break
session.commit()
print(f"[GENEXPERT-DB] Order {rnoreg} ditolak alat. Flag {target_flag_col} di-set True (Selesai).") # 3. Update Database PaslabOrder
else: if rnoreg:
print(f"[GENEXPERT-DB] Order {rnoreg} tidak ditemukan di database saat memproses penolakan.") # Cari kolom flag yang cocok dengan IP yang sedang terkoneksi
except Exception as e: target_flag_col = None
print(f"[GENEXPERT-DB-ERROR] Gagal update order duplikat {rnoreg}: {e}") 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}")
print(f"[GENEXPERT-ASTM] Transaksi penolakan order selesai diproses.") 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
# E. GeneXpert dapat mengirim status/echo order ASTM berisi H/P/O/L.
# Ini bukan hasil lab karena tidak ada R record, dan bukan query karena tidak ada Q record.
if "O" in record_types and set(record_types).issubset({'H', 'P', 'O', 'L'}):
order_summaries = summarize_genexpert_astm_orders(records)
print(
f"[GENEXPERT-ASTM] Menerima status/echo order tanpa hasil dari {ip_addr}. "
f"orders={order_summaries}"
)
return
log_genexpert_hl7("GENEXPERT-ASTM", ip_addr, clean_hl7, label="pesantidakdikenali")
print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}")
return return
# D. Jika hanya berisi H dan L tanpa ada transaksi berarti (Status Echo) # ==========================================================
if set(record_types).issubset({'H', 'L'}): # 2. BLOK PENANGANAN HL7 (Fallback / Cadangan)
print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.") # ==========================================================
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 return
print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}") if "ORU^" in clean_hl7:
return # --- [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:
print(f"[GENEXPERT-ERROR] Mengunci/Membatalkan Order {rnoreg_error} agar tidak terjadi Infinite Loop.")
# (OPSIONAL: Jalankan fungsi update ke DB untuk mengubah flag PaslabOrder menjadi True di sini)
# ...
# 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"[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|^~\\&|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): def handle_genexpert_client(conn, addr):
print(f"[GenExpert_TCP] Koneksi baru dari {addr}") print(f"[GenExpert_TCP] Koneksi baru dari {addr}")
@@ -693,13 +795,15 @@ def handle_genexpert_client(conn, addr):
# --- 1. HANDLE HANDSHAKE (ENQ) --- # --- 1. HANDLE HANDSHAKE (ENQ) ---
# Jika alat kirim ENQ (\x05/♣), langsung balas ACK (\x06) # Jika alat kirim ENQ (\x05/♣), langsung balas ACK (\x06)
if b'\x05' in buffer: if b'\x05' in buffer:
# logging.info(f"[TCP] Terima ENQ dari {addr}, kirim ACK.")
log_genexpert_handshake(addr[0], "ENQ-RX", detail=f"buffer_len={len(buffer)}") log_genexpert_handshake(addr[0], "ENQ-RX", detail=f"buffer_len={len(buffer)}")
conn.sendall(b'\x06') conn.sendall(b'\x06')
log_genexpert_handshake(addr[0], "ACK-TX", detail="reason=enq") log_genexpert_handshake(addr[0], "ACK-TX", detail="reason=enq")
# Hapus ENQ dari buffer agar tidak mengganggu
buffer = buffer.replace(b'\x05', b'') # [PERBAIKAN KURSIS 2]: KOSONGKAN TOTAL BUFFER SAAT ENQ!
# Alat meminta sesi baru, pastikan tidak ada sisa pesan lama yang nyangkut
buffer = b""
pending_astm_hl7 = ""
continue # Langsung lanjut ke recv() berikutnya
if b'\x15' in buffer: if b'\x15' in buffer:
log_genexpert_handshake(addr[0], "NAK-BUFFER-CLEAR", detail=f"buffer_len={len(buffer)}") log_genexpert_handshake(addr[0], "NAK-BUFFER-CLEAR", detail=f"buffer_len={len(buffer)}")
buffer = buffer.replace(b'\x15', b'') buffer = buffer.replace(b'\x15', b'')
@@ -719,13 +823,23 @@ def handle_genexpert_client(conn, addr):
if b'\x1c' in buffer: # Pola MLLP Standard if b'\x1c' in buffer: # Pola MLLP Standard
end_marker_pos = buffer.find(b'\x1c') end_marker_pos = buffer.find(b'\x1c')
msg_complete = True msg_complete = True
elif b'\x03' in buffer: # Pola ASTM (Ada Checksum setelahnya) elif b'\x03' in buffer or b'\x17' in buffer:
# ASTM: <STX>...<ETX>CS<CR><LF> # Cari di index mana letak ETX atau ETB
# Kita cari \x03, lalu ambil ETX + 2 checksum + CRLF secara penuh. pos_etx = buffer.find(b'\x03')
pos = buffer.find(b'\x03') pos_etb = buffer.find(b'\x17')
if len(buffer) >= pos + 5:
# Tentukan mana yang muncul lebih dulu di buffer
pos = -1
if pos_etx != -1 and pos_etb != -1:
pos = min(pos_etx, pos_etb)
else:
pos = max(pos_etx, pos_etb)
# Pastikan kita menerima 5 bytes penuh (ETB/ETX + C1 + C2 + CR + LF)
if pos != -1 and len(buffer) >= pos + 5:
end_marker_pos = pos + 5 end_marker_pos = pos + 5
msg_complete = True msg_complete = True
elif b'\x04' in buffer: # Pola EOT (Putus Koneksi/Selesai) elif b'\x04' in buffer: # Pola EOT (Putus Koneksi/Selesai)
end_marker_pos = buffer.find(b'\x04') end_marker_pos = buffer.find(b'\x04')
msg_complete = True msg_complete = True
@@ -748,6 +862,8 @@ def handle_genexpert_client(conn, addr):
full_message_bytes = buffer[:end_marker_pos] full_message_bytes = buffer[:end_marker_pos]
response_framing = detect_genexpert_message_framing(full_message_bytes) response_framing = detect_genexpert_message_framing(full_message_bytes)
if response_framing == "astm":
debug_genexpert_astm_frame(addr[0], full_message_bytes, direction="RX")
log_genexpert_handshake( log_genexpert_handshake(
addr[0], addr[0],
"FRAME-COMPLETE", "FRAME-COMPLETE",
@@ -770,22 +886,27 @@ def handle_genexpert_client(conn, addr):
# Decode ke string # Decode ke string
temp_str = full_message_bytes.decode('latin-1', errors='ignore') temp_str = full_message_bytes.decode('latin-1', errors='ignore')
astm_text = extract_astm_frame_text(full_message_bytes) if response_framing == "astm" else ""
# --- SANITIZING (PEMBERSIHAN) ---
# Cari MSH pertama
if "MSH|" in temp_str: if "MSH|" in temp_str:
msh_index = temp_str.find("MSH|") msh_index = temp_str.find("MSH|")
clean_hl7 = temp_str[msh_index:] 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": if response_framing == "astm":
pending_astm_hl7 = clean_hl7 pending_astm_hl7 = clean_hl7
pending_astm_framing = response_framing pending_astm_framing = response_framing
log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(clean_hl7)}") log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(clean_hl7)}")
continue continue
process_genexpert_hl7_message(conn, addr[0], clean_hl7, response_framing) process_genexpert_hl7_message(conn, addr[0], clean_hl7, response_framing)
elif response_framing == "astm" and astm_text:
pending_astm_hl7 = (pending_astm_hl7 or "") + astm_text
pending_astm_framing = response_framing
log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(pending_astm_hl7)}, mode=records")
continue
else:
# Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang)
pass
else: else:
if buffer: if buffer:
head_hex = buffer[:12].hex() head_hex = buffer[:12].hex()
@@ -1297,15 +1418,16 @@ def get_genexpert_response_mode(ip_addr):
def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""): def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""):
log_genexpert_hl7("OUT", ip_addr, hl7_message, label=label) log_genexpert_hl7("OUT", ip_addr, hl7_message, label=label)
log_genexpert_hl7_full("OUT", ip_addr, hl7_message, label=label) log_genexpert_hl7_full("OUT", ip_addr, hl7_message, label=label)
payload = frame_genexpert_response(hl7_message, framing)
response_mode = get_genexpert_response_mode(ip_addr) response_mode = get_genexpert_response_mode(ip_addr)
print(
f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, "
f"response_mode={response_mode}, label={label}, bytes={len(payload)}"
)
if framing == "astm" and response_mode == "astm_active": if framing == "astm" and response_mode == "astm_active":
print(
f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, "
f"response_mode={response_mode}, label={label}, bytes=multi-frame"
)
send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=label) send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=label)
return return
payload = frame_genexpert_response(hl7_message, framing)
if framing == "astm": if framing == "astm":
debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=label) debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=label)
else: else:
@@ -1313,6 +1435,10 @@ def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""):
f"[GENEXPERT-FRAME-TX] ip={ip_addr}, label={label}, framing={framing}, " f"[GENEXPERT-FRAME-TX] ip={ip_addr}, label={label}, framing={framing}, "
f"hex={_hex_bytes(payload)}, visible={_visible_bytes(payload)}" f"hex={_hex_bytes(payload)}, visible={_visible_bytes(payload)}"
) )
print(
f"[GENEXPERT-DEBUG] Send response ip={ip_addr}, framing={framing}, "
f"response_mode={response_mode}, label={label}, bytes={len(payload)}"
)
conn.sendall(payload) conn.sendall(payload)
def send_genexpert_transport_ack(conn, ip_addr, framing, reason="frame-received"): def send_genexpert_transport_ack(conn, ip_addr, framing, reason="frame-received"):
@@ -1423,15 +1549,7 @@ def debug_genexpert_order_message(hl7_message, ip_addr=None):
f"specimen_id='{specimen_id}', specimen_type='{specimen_type}', patient_id='{current_pid}', " f"specimen_id='{specimen_id}', specimen_type='{specimen_type}', patient_id='{current_pid}', "
f"patient_name='{current_patient_name}', raw='{segment}'" f"patient_name='{current_patient_name}', raw='{segment}'"
) )
def build_genexpert_result_query(accnumber, msg_control_id):
ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
query_msg = (
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
def get_active_genexpert_ips(): def get_active_genexpert_ips():
with connection_lock: with connection_lock:
return list(active_genexpert_connections.keys()) return list(active_genexpert_connections.keys())
@@ -1473,14 +1591,6 @@ def schedule_result_query_for_order(accnumber, register_no, target_ip=None, **kw
) )
return False return False
@app.route("/api/genexpert/query-result", methods=["POST"])
def api_query_genexpert_result():
return jsonify({
"ok": False,
"message": "Mode GeneXpert pasif aktif. Host hanya menjawab request dari alat.",
}), 409
def create_hl7_dsr_response(order, msg_control_id, qrd_segment): def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
""" """
Membuat pesan balasan DSR^Q03 (Data Response) untuk GeneXpert. Membuat pesan balasan DSR^Q03 (Data Response) untuk GeneXpert.
@@ -3240,12 +3350,7 @@ if __name__ == "__main__":
myla_inbound_thread.start() myla_inbound_thread.start()
all_threads.append(myla_inbound_thread) all_threads.append(myla_inbound_thread)
# 4. Start Thread HTTP API (Trigger dari Laravel) # 4. LOOP UTAMA (Keep-Alive & Monitoring)
t_http = threading.Thread(target=run_http_api_server, name="Manager-HTTP-API", daemon=True)
t_http.start()
all_threads.append(t_http)
# 5. LOOP UTAMA (Keep-Alive & Monitoring)
try: try:
while True: while True:
print(f"--- Monitoring {len(all_threads)} Threads ---") print(f"--- Monitoring {len(all_threads)} Threads ---")
+37 -2
View File
@@ -136,6 +136,7 @@ TARGET_MAPPING = {
GENEXPERT_TEST_MAPPING = { GENEXPERT_TEST_MAPPING = {
# Mapping untuk IP 10.10.120.75 (Multi-Assay) # Mapping untuk IP 10.10.120.75 (Multi-Assay)
"HIV": "HIV1-VL", "HIV": "HIV1-VL",
"HBV": "HBVVL",
"TCM TB": "MTBRIF", "TCM TB": "MTBRIF",
"TCM TB ULTRA": "MTBRIF", "TCM TB ULTRA": "MTBRIF",
"TCM TB XDR": "MTB-XDR 2", "TCM TB XDR": "MTB-XDR 2",
@@ -146,7 +147,7 @@ GENEXPERT_TEST_MAPPING = {
"E.2.5 HCV TCM": "HCV", "E.2.5 HCV TCM": "HCV",
"18.1.1 TCM HCV": "HCV", "18.1.1 TCM HCV": "HCV",
"18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL", "18.1.2 TCM HIV VIRAL LOAD": "HIV1-VL",
"18.1.4 TCM HPV": "HCV", "18.1.4 TCM HPV": "HPV",
"7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "7.3.7 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
"5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF", "5.3.8 KULTUR TBC MGIT (AUTOMATIC)": "MTBRIF",
"5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2", "5.3.7 KULTUR TBC MEDIA LJ (KONVENSIONAL)": "MTB-XDR 2",
@@ -172,6 +173,7 @@ GENEXPERT_TEST_MAPPING = {
"2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF", "2.3.10 TCM CLAMIDIA TRACHOMATIS / NEISSERIA GONORRHOE": "MTBRIF",
"12.3.8 TCM TB (GENE EXPERT)": "MTBRIF", "12.3.8 TCM TB (GENE EXPERT)": "MTBRIF",
"15.2.3 TCM TB (GENE EXPERT)": "MTBRIF", "15.2.3 TCM TB (GENE EXPERT)": "MTBRIF",
"15.2.3 TCM TB (GENE EXPERT) GENE EXPERT": "MTBRIF",
"2.3.9 TCM GENE EXPERT": "MTBRIF", "2.3.9 TCM GENE EXPERT": "MTBRIF",
"3.3.8 TCM GENE EXPERT": "MTBRIF", "3.3.8 TCM GENE EXPERT": "MTBRIF",
"3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF", "3.3.9 TCM MYCOBACTERIUM TUBERCULOSIS": "MTBRIF",
@@ -185,7 +187,7 @@ GENEXPERT_TEST_MAPPING = {
GENEXPERT_IP_CAPABILITIES = { GENEXPERT_IP_CAPABILITIES = {
"10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"], "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"],
"10.10.120.73": ["MTBRIF", "HBVVL", "HIV1-VL", "HCV", "SARSCOV2FLURSV"], "10.10.120.73": ["MTBRIF", "HBVVL", "HIV1-VL", "HCV", "SARSCOV2FLURSV"],
"10.10.120.108": ["HCV", "HIV1-VL", "HBVVL"], "10.10.120.108": ["MTBRIF", "HCV", "HIV1-VL", "HBVVL", "SARSCOV2FLURSV"],
} }
DEFAULT_GXP_CODE = "MTBRIF" DEFAULT_GXP_CODE = "MTBRIF"
@@ -379,6 +381,28 @@ def parse_genexpert_astm_query(message_text):
query["query_sample_id"] = fields[2] if len(fields) > 2 else "" query["query_sample_id"] = fields[2] if len(fields) > 2 else ""
return query return query
def summarize_genexpert_astm_orders(records):
summaries = []
for rec in records:
if not rec.startswith("O|"):
continue
fields = rec.split("|")
sample_id = fields[2].strip() if len(fields) > 2 else ""
container_id = fields[3].strip() if len(fields) > 3 else ""
assay_code = ""
if len(fields) > 4:
assay_parts = [part.strip() for part in fields[4].split("^") if part.strip()]
assay_code = assay_parts[-1] if assay_parts else fields[4].strip()
priority = fields[5].strip() if len(fields) > 5 else ""
action_code = fields[11].strip() if len(fields) > 11 else ""
summaries.append(
f"sample_id={sample_id or '-'}, container_id={container_id or '-'}, "
f"assay={assay_code or '-'}, priority={priority or '-'}, action={action_code or '-'}"
)
return summaries
def detect_genexpert_message_framing(message_bytes): def detect_genexpert_message_framing(message_bytes):
raw = message_bytes or b"" raw = message_bytes or b""
if b"\x1c" in raw or raw.startswith(b"\x0b"): if b"\x1c" in raw or raw.startswith(b"\x0b"):
@@ -2569,6 +2593,17 @@ def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing):
print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.") print(f"[GENEXPERT-ASTM] Menerima Heartbeat / Sesi Kosong dari alat.")
return return
# E. GeneXpert dapat mengirim status/echo order ASTM berisi H/P/O/L.
# Ini bukan hasil lab karena tidak ada R record, dan bukan query karena tidak ada Q record.
if "O" in record_types and set(record_types).issubset({'H', 'P', 'O', 'L'}):
order_summaries = summarize_genexpert_astm_orders(records)
print(
f"[GENEXPERT-ASTM] Menerima status/echo order tanpa hasil dari {ip_addr}. "
f"orders={order_summaries}"
)
return
log_genexpert_hl7("GENEXPERT-ASTM", ip_addr, clean_hl7, label="pesantidakdikenali")
print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}") print(f"[GENEXPERT-ASTM] Pesan ASTM tidak dikenali dari {ip_addr}. Isi: {clean_hl7[:50]}")
return return