diff --git a/listener/app.py b/listener/app.py index 99941761..f27acd7f 100644 --- a/listener/app.py +++ b/listener/app.py @@ -19,22 +19,64 @@ from sqlalchemy import DateTime as SqDateTime # type: ignore from sqlalchemy import Date as SqDate # type: ignore from sqlalchemy.orm import declarative_base, sessionmaker # type: ignore # Logging Setup -# Konfigurasi logging per hari + +def get_app_dir(): + """ + Mengembalikan direktori aplikasi. + + Python: + folder tempat app.py berada + + PyInstaller EXE: + folder tempat app.exe berada + """ + if getattr(sys, "frozen", False): + return os.path.dirname(os.path.abspath(sys.executable)) + + return os.path.dirname(os.path.abspath(__file__)) + + +APP_DIR = get_app_dir() + +THREAD_LOG_DIR = os.path.join(APP_DIR, "thread_logs") +APP_LOG_FILE = os.path.join(APP_DIR, "app.log") + +thread_log_lock = threading.Lock() + + +# Buat folder sejak startup +try: + os.makedirs(THREAD_LOG_DIR, exist_ok=True) +except Exception as exc: + builtins.print( + f"[LOG-INIT-ERROR] Tidak bisa membuat " + f"{THREAD_LOG_DIR}: {exc}" + ) + + +# Logging Setup log_handler = TimedRotatingFileHandler( - filename="app.log", + filename=APP_LOG_FILE, when="midnight", interval=1, backupCount=7, encoding="utf-8" ) -formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s') + +formatter = logging.Formatter( + '%(asctime)s - %(levelname)s - %(threadName)s - %(message)s' +) + log_handler.setFormatter(formatter) log_handler.setLevel(logging.ERROR) -logging.basicConfig(level=logging.ERROR, handlers=[log_handler]) + +logging.basicConfig( + level=logging.ERROR, + handlers=[log_handler] +) + logging.getLogger().setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR) - -THREAD_LOG_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "thread_logs") thread_log_lock = threading.Lock() ERROR_LOG_KEYWORDS = ( "critical", @@ -66,7 +108,7 @@ 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 + "10.10.120.73": "GE01", # GenExpert Besar } # Mapping Flag ke IP Address GeneXpert # Pastikan IP ini SESUAI dengan settingan "Server IP" di masing-masing alat (Client Mode) @@ -75,14 +117,14 @@ TARGET_MAPPING = { 'flg_gxp2': '10.10.120.108', 'flg_gxp3': '10.10.120.75' } +GENEXPERT_ORDER_LOCKS = { + "10.10.120.73": threading.Lock(), + "10.10.120.108": threading.Lock(), + "10.10.120.75": threading.Lock(), +} # GeneXpert Configuration # ========================================== -# KONFIGURASI MAPPING TES (DATABASE -> GENEXPERT) -# ========================================== -# Kiri: Nama di kolom 'tes' database Anda -# Kanan: 'Host Test Code' dari Dokumen Word Anda GENEXPERT_TEST_MAPPING = { - # Mapping untuk IP 10.10.120.75 (Multi-Assay) "HIV": "HIV1-VL", "HBV": "HBVVL", "TCM TB": "MTBRIF", @@ -130,7 +172,6 @@ GENEXPERT_TEST_MAPPING = { "8.3.8 TCM TBC (GENE EXPERT)": "MTBRIF", "10.3.8 TCM TBC (GENE EXPERT)": "MTBRIF", "11.3.9 TCM TB (GENE EXPERT)": "MTBRIF", - } GENEXPERT_IP_CAPABILITIES = { "10.10.120.75": ["MTBRIF", "HBVVL", "HIV1-VL", "MTB-XDR 2", "HCV", "SARSCOV2FLURSV"], @@ -177,13 +218,16 @@ active_serial_ports = {} order_queues = {config['port']: Queue() for config in DEVICE_CONFIGS if config['protocol'] == 'serial'} # ========================================== -# 2. DATABASE MODEL +# 2. DATABASE CONNECTION # ========================================== DATABASE_URL = "postgresql://lismikro:lismikro@10.10.123.193:5002/lismikro" engine = create_engine(DATABASE_URL, pool_recycle=3600) SessionLocal = sessionmaker(bind=engine) Base = declarative_base() +# ========================================== +# 2. DATABASE MODEL +# ========================================== class Sample(Base): __tablename__ = 'samples' id = Column(Integer, primary_key=True) @@ -278,8 +322,12 @@ def _write_thread_log(message): with thread_log_lock: with open(log_path, "a", encoding="utf-8") as fh: fh.write(f"{timestamp} | {message}\n") - except Exception: - pass + except Exception as exc: + builtins.print( + f"[THREAD-LOG-ERROR] " + f"Gagal menulis thread log ke " + f"{THREAD_LOG_DIR}: {exc}" + ) def print(*args, **kwargs): sep = kwargs.get("sep", " ") @@ -323,18 +371,6 @@ def get_flag_by_device(ip_addr): return flag return None -def get_pending_orders(ip_addr): - flag = get_flag_by_device(ip_addr) - if not flag: - return [] - - session = SessionLocal() - try: - q = session.query(PaslabOrder).filter(getattr(PaslabOrder, flag) == False) - return q.all() - finally: - session.close() - def mark_genexpert_order_flag(rnoreg, ip_addr, reason="processed"): rnoreg = str(rnoreg or "").strip() flag_name = get_flag_by_device(str(ip_addr or "").strip()) @@ -424,16 +460,6 @@ def build_hl7_segment(segment_name, fields): values = [str(fields.get(index, "")) for index in range(1, max_field + 1)] return f"{segment_name}|" + "|".join(values) -def map_hl7_sex(value): - text = str(value or "").strip().upper() - if not text: - return "" - if text.startswith("L") or "LAKI" in text or text == "M": - return "M" - if text.startswith("P") or "PEREM" in text or text == "F": - return "F" - return "" - def extract_msg_control_id(hl7_message): try: segments = hl7_message.split('\r') @@ -588,60 +614,164 @@ def create_genexpert_astm_order_message(orders, ip_addr=None, query_tag=""): return message def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"): - query = parse_genexpert_astm_query(astm_msg) - requested_sample_id = str(query.get("query_sample_id") or "").strip() - query_tag = str(query.get("query_tag") or "").strip() - flag = get_flag_by_device(ip_addr) - if not flag: - print(f"[GENEXPERT] ASTM query diabaikan, flag untuk {ip_addr} tidak ditemukan.") - return + lock = GENEXPERT_ORDER_LOCKS.setdefault(ip_addr, threading.Lock()) - session = SessionLocal() - try: - flag_attr = getattr(PaslabOrder, flag, None) - if flag_attr is None: - print(f"[GENEXPERT] ASTM query diabaikan, atribut flag {flag} tidak ada.") + print(f"[GENEXPERT-LOCK] ip={ip_addr}, waiting") + + with lock: + print(f"[GENEXPERT-LOCK] ip={ip_addr}, acquired") + + query = parse_genexpert_astm_query(astm_msg) + requested_sample_id = str(query.get("query_sample_id") or "").strip() + query_tag = str(query.get("query_tag") or "").strip() + + flag = get_flag_by_device(ip_addr) + + if not flag: + print( + f"[GENEXPERT] ASTM query diabaikan, " + f"flag untuk {ip_addr} tidak ditemukan." + ) return - base_orders = session.query(PaslabOrder).filter( - (flag_attr == False) | (flag_attr == None) - ).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all() - - selected_orders = [] - if requested_sample_id and requested_sample_id.upper() != "ALL": - for order in base_orders: - if str(order.rnoreg or "").strip() != requested_sample_id: - continue - assay_code, _, _ = resolve_genexpert_assay(order, ip_addr) - if assay_code: - selected_orders = [order] - break - else: - for order in base_orders: - assay_code, _, _ = resolve_genexpert_assay(order, ip_addr) - if assay_code: - selected_orders = [order] - break - print( - f"[GENEXPERT-ASTM-QUERY] ip={ip_addr}, query_tag={query_tag}, requested_sample_id='{requested_sample_id}', " - f"selected_rnoreg={[str(order.rnoreg or '').strip() for order in selected_orders]}" + f"[GENEXPERT-ORDER-SELECT] " + f"ip={ip_addr}, flag={flag}, " + f"requested_sample_id='{requested_sample_id}'" ) - if not selected_orders: - reply = f"H|\\^&|||{sanitize_astm_field(get_genexpert_host_application(ip_addr), max_len=20)}|||||GeneXpert Host||P|1394-97|{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}\rL|1|N\r" - send_genexpert_response(conn, ip_addr, reply, response_framing, label="astm-q-empty") - return + session = SessionLocal() - reply = create_genexpert_astm_order_message(selected_orders, ip_addr=ip_addr, query_tag=query_tag) - sent_ok = send_genexpert_response(conn, ip_addr, reply, response_framing, label=f"astm-q-order:{selected_orders[0].rnoreg}") - for order in selected_orders: - rnoreg = str(order.rnoreg or "").strip() - print(f"[GENEXPERT] Order ASTM ditawarkan ke {ip_addr}: {rnoreg}, sent_ok={sent_ok}") - if sent_ok: - mark_genexpert_order_flag(rnoreg, ip_addr, reason="astm-order-sent") - finally: - session.close() + try: + flag_attr = getattr(PaslabOrder, flag, None) + + if flag_attr is None: + print( + f"[GENEXPERT] ASTM query diabaikan, " + f"atribut flag {flag} tidak ada." + ) + return + + base_orders = ( + session.query(PaslabOrder) + .filter( + (flag_attr == False) | (flag_attr == None) + ) + .order_by( + PaslabOrder.rtglast.desc().nullslast(), + PaslabOrder.urut.desc() + ) + .all() + ) + + selected_orders = [] + + if requested_sample_id and requested_sample_id.upper() != "ALL": + + for order in base_orders: + + if str(order.rnoreg or "").strip() != requested_sample_id: + continue + + assay_code, _, _ = resolve_genexpert_assay( + order, + ip_addr + ) + + if assay_code: + selected_orders = [order] + break + + else: + + for order in base_orders: + + assay_code, _, _ = resolve_genexpert_assay( + order, + ip_addr + ) + + if assay_code: + selected_orders = [order] + break + + print( + f"[GENEXPERT-ASTM-QUERY] " + f"ip={ip_addr}, " + f"flag={flag}, " + f"query_tag={query_tag}, " + f"requested_sample_id='{requested_sample_id}', " + f"selected_rnoreg=" + f"{[str(order.rnoreg or '').strip() for order in selected_orders]}" + ) + + if not selected_orders: + + reply = ( + f"H|\\^&|||" + f"{sanitize_astm_field(get_genexpert_host_application(ip_addr), max_len=20)}" + f"|||||GeneXpert Host||P|1394-97|" + f"{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" + f"\rL|1|N\r" + ) + + send_genexpert_response( + conn, + ip_addr, + reply, + response_framing, + label="astm-q-empty" + ) + + return + + reply = create_genexpert_astm_order_message( + selected_orders, + ip_addr=ip_addr, + query_tag=query_tag + ) + + rnoreg_first = str( + selected_orders[0].rnoreg or "" + ).strip() + + sent_ok = send_genexpert_response( + conn, + ip_addr, + reply, + response_framing, + label=f"astm-q-order:{rnoreg_first}" + ) + + for order in selected_orders: + + rnoreg = str(order.rnoreg or "").strip() + + print( + f"[GENEXPERT] Order ASTM ditawarkan ke " + f"{ip_addr}: {rnoreg}, sent_ok={sent_ok}" + ) + + if sent_ok: + + flag_ok = mark_genexpert_order_flag( + rnoreg, + ip_addr, + reason="astm-order-sent" + ) + + print( + f"[GENEXPERT-ORDER-COMMIT] " + f"ip={ip_addr}, " + f"rnoreg={rnoreg}, " + f"flag={flag}, " + f"success={flag_ok}" + ) + + finally: + session.close() + + print(f"[GENEXPERT-LOCK] ip={ip_addr}, released") def process_genexpert_hl7_message(conn, ip_addr, clean_hl7, response_framing): # ========================================================== @@ -971,40 +1101,6 @@ def handle_genexpert_client(conn, addr): pass logging.info(f"[GenExpert_TCP] Koneksi {addr} ditutup.") -def send_order_via_active_connection(target_ip, hl7_message): - conn = None - with connection_lock: - conn = active_genexpert_connections.get(target_ip) - - if not conn: - logging.warning(f"Gagal kirim Order: GeneXpert dengan IP {target_ip} BELUM TERKONEKSI ke Listener.") - print(f"Gagal kirim Order: GeneXpert dengan IP {target_ip} BELUM TERKONEKSI ke Listener.") - return False - - try: - # Bungkus pesan dengan MLLP (Minimal Lower Layer Protocol) standard HL7 - # Format: message - mllp_msg = f"\x0b{hl7_message}\x1c\r" - - logging.info(f"Mengirim Order ke {target_ip}...") - print(f"Mengirim Order ke {target_ip}...") - conn.sendall(mllp_msg.encode('utf-8')) - - # Opsi: Jika ingin menunggu ACK balasan untuk Order - # Namun hati-hati ini bisa blocking jika alat lambat - ack = conn.recv(1024) - logging.info(f"Dapat ACK Order dari {target_ip}: {ack}") - print(f"Dapat ACK Order dari {target_ip}: {ack}") - return True - except Exception as e: - logging.error(f"Error mengirim ke {target_ip}: {e}") - print(f"Error mengirim ke {target_ip}: {e}") - # Jika error saat kirim, anggap koneksi rusak - with connection_lock: - if target_ip in active_genexpert_connections: - del active_genexpert_connections[target_ip] - return False - def parse_genexpert_astm_records(astm_string, device_name): """ Parser khusus untuk membaca hasil ASTM dari instrumen GeneXpert @@ -1100,28 +1196,35 @@ def parse_genexpert_qpd(qpd_segment): def resolve_genexpert_assay(order, ip_addr=None): assay_name = str(getattr(order, "tes", "") or "").strip() specimen_code = str(getattr(order, "kd_spesimen", "") or "").strip() + assay_code = GENEXPERT_TEST_MAPPING.get(assay_name) assay_source = "mapping:tes" - if not assay_code and specimen_code: - assay_code = specimen_code - assay_source = "fallback:kd_spesimen" - - supported_codes = GENEXPERT_IP_CAPABILITIES.get(str(ip_addr or "").strip(), []) if ip_addr else [] - capability_match = True if not supported_codes else assay_code in supported_codes - if not assay_code: - print( - f"[GENEXPERT-DEBUG] rnoreg={getattr(order, 'rnoreg', '')}, ip={ip_addr}, " - f"tes='{assay_name}', kd_spesimen='{specimen_code}', assay_code=EMPTY" - ) - return None, "none", capability_match + assay_code = DEFAULT_GXP_CODE + assay_source = "default" + + supported_codes = ( + GENEXPERT_IP_CAPABILITIES.get(str(ip_addr or "").strip(), []) + if ip_addr else [] + ) + + capability_match = ( + True if not supported_codes + else assay_code in supported_codes + ) print( - f"[GENEXPERT-DEBUG] rnoreg={getattr(order, 'rnoreg', '')}, ip={ip_addr}, " - f"tes='{assay_name}', kd_spesimen='{specimen_code}', assay_code='{assay_code}', " - f"assay_source={assay_source}, capability_match={capability_match}" + f"[GENEXPERT-DEBUG] " + f"rnoreg={getattr(order, 'rnoreg', '')}, " + f"ip={ip_addr}, " + f"tes='{assay_name}', " + f"kd_spesimen='{specimen_code}', " + f"assay_code='{assay_code}', " + f"assay_source={assay_source}, " + f"capability_match={capability_match}" ) + return assay_code, assay_source, capability_match def get_genexpert_query_orders(ip_addr, hl7_msg): @@ -1563,73 +1666,6 @@ def debug_genexpert_order_message(hl7_message, ip_addr=None): f"patient_name='{current_patient_name}', raw='{segment}'" ) -def get_active_genexpert_ips(): - with connection_lock: - return list(active_genexpert_connections.keys()) - -def create_hl7_dsr_response(order, msg_control_id, qrd_segment): - """ - Membuat pesan balasan DSR^Q03 (Data Response) untuk GeneXpert. - """ - timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') - - # --- 1. HEADER (MSH) --- - # Field 9 (Message Type) adalah DSR^Q03 - # Field 10 (Control ID) kita generate baru - # Field 6 (Receiving Fac) harusnya GeneXpert - resp_control_id = f"RESP{timestamp}" - msh = f"MSH|^~\\&|LIS|LAB|GeneXpert|Cepheid|{timestamp}||DSR^Q03|{resp_control_id}|P|2.5" - - # --- 2. ACKNOWLEDGEMENT (MSA) --- - # AA = Application Accept (Kita mengerti pertanyaannya) - # msg_control_id = ID dari pesan QRY yang dikirim GeneXpert (Supaya dia tahu ini jawaban untuk pertanyaan yg mana) - msa = f"MSA|AA|{msg_control_id}" - - # --- 3. QUERY DEFINITION (QRD) --- - # Kita kembalikan segmen QRD yang dikirim alat (Echo back) - # qrd_segment harus string raw dari pesan masuk - qrd = qrd_segment - - # --- 4. QUERY RESPONSE STATUS (QRF) --- - # Opsional, tapi baik untuk konfirmasi - qrf = f"QRF|LIS|{timestamp}||||" - - # --- 5. DATA PASIEN & ORDER (Jika Order Ditemukan) --- - if order: - # Mapping Data - sample_id = str(order.rnoreg) - pid_norm = order.norm if order.norm else "" - first_name, last_name = split_patient_name(order.nama) - pid_nama = f"{last_name}^{first_name}" - room = str(getattr(order, "ruangan", "") or "RSSA MALANG").strip().upper() - room = room.replace("|", " ").replace("^", " ")[:30] - - # Gender - raw_gender = str(order.rjenis).upper() - pid_gender = 'M' if 'LAKI' in raw_gender or raw_gender == 'L' else 'F' - - # Test Code (Pakai Mapping yang tadi kita buat) - nama_tes_db = str(order.tes).strip() if order.tes else "" - test_code = GENEXPERT_TEST_MAPPING.get(nama_tes_db, DEFAULT_GXP_CODE) # Default: MTBRIF - - # Segmen Data - # DSP/PID/ORC/OBR tergantung setting alat. - # GeneXpert standar biasanya terima format ORM di dalam DSR atau sequence PID-ORC-OBR - - pid = f"PID|1||{pid_norm}||{pid_nama}|||{pid_gender}" - pv1 = f"PV1|1|I|{room}" - orc = f"ORC|NW|{sample_id}" - obr = f"OBR|1|{sample_id}||{test_code}^{nama_tes_db}^L|||{timestamp}" - - # Gabungkan - return f"{msh}\r{msa}\r{qrd}\r{qrf}\r{pid}\r{pv1}\r{orc}\r{obr}\r" - - else: - # Jika TIDAK ADA ORDER (Not Found) - # Kita kirim QAK (Query Acknowledge) dengan status NF (Not Found) di QRF/QAK - # Atau cukup kirim MSA|AA tapi tanpa segmen Order - return f"{msh}\r{msa}\r{qrd}\r{qrf}\r" - # ========================================== # bioMérieux MYLA TCP Server Handler # ========================================== @@ -1753,65 +1789,6 @@ def mark_myla_order_sent(order_id): finally: session.close() -def create_myla_astm_order_message(order): - """ - ASTM E1394/LIS2-A2 style: - H, P, C, O, R, L - """ - pid = sanitize_astm_field(order.norm, max_len=32) - sid = sanitize_astm_field(order.rnoreg, max_len=32) - - first_name, last_name = split_patient_name(sanitize_astm_field(order.nama, max_len=80)) - first_name = sanitize_astm_field(first_name, uppercase=True, max_len=20) - last_name = sanitize_astm_field(last_name, uppercase=True, max_len=20) - p_name = f"{last_name}^{first_name}" - - sex_raw = sanitize_astm_field(order.rjenis, uppercase=True, max_len=10) - sex = "M" if sex_raw.startswith("L") else "F" - - raw_location = sanitize_astm_field(getattr(order, 'ruangan', "UT"), uppercase=True, max_len=40) - location = re.sub(r"[^A-Z0-9]", "", raw_location)[:10] or "UT" - - diagnosis = sanitize_astm_field(getattr(order, 'diagnosa', "Unspecified"), max_len=60) or "Unspecified" - specimen_type = sanitize_astm_field(order.kd_spesimen, uppercase=True, max_len=20) if order.kd_spesimen else "BLOOD" - specimen_field = f"{specimen_type}^VENA^^BAIK" - test_code = sanitize_astm_field(order.tes, uppercase=True, max_len=40) or "GENERAL" - - head = r"H|\^&|||LIS|||||||||P|1" - - p_rec = [""] * 36 - p_rec[0] = "P" - p_rec[1] = "1" - p_rec[2] = pid - p_rec[3] = pid - p_rec[5] = p_name - p_rec[8] = sex - p_rec[25] = location - pat_str = "|".join(p_rec[:36]) - - com_str = f"C|1|L|{diagnosis}|G" - - o_rec = [""] * 32 - o_rec[0] = "O" - o_rec[1] = "1" - o_rec[2] = sid - o_rec[4] = f"^^^{test_code}" - o_rec[5] = "R" - o_rec[11] = "A" - o_rec[15] = specimen_field - ord_str = "|".join(o_rec[:32]) - - # Placeholder R record agar struktur record lengkap H,P,C,O,R,L. - rcd_str = "R|1|^^^ORDER_STATUS|PENDING|||N|||||F" - term = "L|1|N" - - message_content = f"{head}\r{pat_str}\r{com_str}\r{ord_str}\r{rcd_str}\r{term}\r" - seq = "1" - frame_body = f"{seq}{message_content}\x03" - chk = calculate_astm_checksum(frame_body) - full_frame = f"\x02{frame_body}{chk}\r\n" - return [full_frame.encode("latin-1")] - def parse_myla_astm_records(raw_message, device_name="MYLA"): session = SessionLocal() try: @@ -1939,59 +1916,6 @@ def receive_myla_astm_transmission(conn, peer_ip, first_control=ENQ): raw_message = "".join(assembled) parse_myla_astm_records(raw_message, device_name=f"MYLA-{peer_ip}") -def wait_for_astm_control(conn, expected_controls, peer_ip, timeout_seconds=MYLA_CONTROL_TIMEOUT_SECONDS): - deadline = time.time() + timeout_seconds - while time.time() < deadline: - try: - conn.settimeout(max(0.1, deadline - time.time())) - b = conn.recv(1) - if not b: - return None - if b in expected_controls: - return b - if b == ENQ: - receive_myla_astm_transmission(conn, peer_ip, first_control=ENQ) - elif b == STX: - receive_myla_astm_transmission(conn, peer_ip, first_control=STX) - except socket.timeout: - continue - except Exception as e: - logging.error(f"[MYLA-ASTM] Error wait control: {e}") - print(f"[MYLA-ASTM] Error wait control: {e}") - return None - return None - -def send_order_to_myla_astm(conn, order, peer_ip): - frames = create_myla_astm_order_message(order) - conn.sendall(ENQ) - hs = wait_for_astm_control(conn, {ACK}, peer_ip, timeout_seconds=MYLA_CONTROL_TIMEOUT_SECONDS) - if hs != ACK: - logging.warning(f"[MYLA-ASTM] Handshake gagal untuk rnoreg={order.rnoreg}, respon={hs}") - print(f"[MYLA-ASTM] Handshake gagal untuk rnoreg={order.rnoreg}, respon={hs}") - return False - - for i, frame in enumerate(frames, start=1): - frame_ok = False - for attempt in range(1, 4): - conn.sendall(frame) - resp = wait_for_astm_control(conn, {ACK, NAK}, peer_ip, timeout_seconds=MYLA_CONTROL_TIMEOUT_SECONDS) - if resp == ACK: - frame_ok = True - break - if resp == NAK: - logging.warning(f"[MYLA-ASTM] Frame {i} NAK rnoreg={order.rnoreg}, retry={attempt}") - print(f"[MYLA-ASTM] Frame {i} NAK rnoreg={order.rnoreg}, retry={attempt}") - time.sleep(0.5) - continue - logging.warning(f"[MYLA-ASTM] Frame {i} timeout rnoreg={order.rnoreg}, retry={attempt}") - print(f"[MYLA-ASTM] Frame {i} timeout rnoreg={order.rnoreg}, retry={attempt}") - if not frame_ok: - conn.sendall(EOT) - return False - - conn.sendall(EOT) - return True - def create_myla_hl7_order_message(order, msg_control_id): timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S') order_timestamp = format_hl7_datetime(getattr(order, "rtglast", None)) or timestamp @@ -2389,13 +2313,6 @@ def start_myla_inbound_server(host, port): # VITEK PARSER # ========================================== -def calculate_vitek_checksum(data_str): - """ - Menghitung Checksum Vitek (Sum of bytes % 256). - """ - total = sum(ord(c) for c in data_str) - return f"{total % 256:02X}" - def parse_and_save_vitek_result(raw_data, port_name="VITEK"): session = SessionLocal() try: