@@ -497,12 +527,39 @@
}
$(document).on('click', '.js-slot', function () {
+ $('#existing_specimen_id').val('');
$('#rack_id').val($(this).data('rack-id'));
$('#shelfnomor').val($(this).data('shelf'));
$('#raknomor').val($(this).data('rackno'));
$('#slotnomor').val($(this).data('slot'));
$('#boxnomor').val($(this).data('box'));
$('#tubenomor').val($(this).data('tube'));
+ $('#nmbakteri').val('').prop('readonly', false);
+ $('#strain').val('Gram Negatif').prop('disabled', false);
+ $('#volume').val('');
+ $('#volume_ambil').val('');
+ $('#group-volume-awal').show();
+ $('#group-volume-ambil').hide();
+ $('#volume_sekarang_text').text('0');
+ buildSampleCodePreview();
+ $('#modalIsiSlot').modal('show');
+ });
+
+ $(document).on('click', '.js-slot-filled', function () {
+ $('#existing_specimen_id').val($(this).data('specimen-id'));
+ $('#rack_id').val($(this).data('rack-id'));
+ $('#shelfnomor').val($(this).data('shelf'));
+ $('#raknomor').val($(this).data('rackno'));
+ $('#slotnomor').val($(this).data('slot'));
+ $('#boxnomor').val($(this).data('box'));
+ $('#tubenomor').val($(this).data('tube'));
+ $('#nmbakteri').val($(this).data('bakteri')).prop('readonly', true);
+ $('#strain').val($(this).data('strain')).prop('disabled', true);
+ $('#volume').val('');
+ $('#volume_ambil').val('');
+ $('#group-volume-awal').hide();
+ $('#group-volume-ambil').show();
+ $('#volume_sekarang_text').text($(this).data('volume'));
buildSampleCodePreview();
$('#modalIsiSlot').modal('show');
});
diff --git a/listener/app.py b/listener/app.py
index ab715a40..395cf234 100644
--- a/listener/app.py
+++ b/listener/app.py
@@ -17,11 +17,28 @@ from sqlalchemy import create_engine, Column, Integer, String, Boolean, Text # t
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
+log_handler = TimedRotatingFileHandler(
+ filename="app.log",
+ when="midnight",
+ interval=1,
+ backupCount=7,
+ encoding="utf-8"
+)
+formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
+log_handler.setFormatter(formatter)
+logging.basicConfig(level=logging.INFO, handlers=[log_handler])
# ==========================================
# 1. KONFIGURASI SISTEM
# ==========================================
-
+# Global Variables
+app = Flask(__name__)
+active_genexpert_connections = {}
+connection_lock = threading.Lock()
+pending_result_queries = {}
+pending_query_lock = threading.Lock()
# Network Configuration
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
@@ -61,25 +78,6 @@ GENEXPERT_IP_CAPABILITIES = {
# Default code jika nama tes di database tidak dikenali
DEFAULT_GXP_CODE = "MTB-RIF"
-# Logging Setup
-# Konfigurasi logging per hari
-log_handler = TimedRotatingFileHandler(
- filename="app.log",
- when="midnight",
- interval=1,
- backupCount=7,
- encoding="utf-8"
-)
-formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(threadName)s - %(message)s')
-log_handler.setFormatter(formatter)
-logging.basicConfig(level=logging.INFO, handlers=[log_handler])
-
-# Global Variables
-app = Flask(__name__)
-active_genexpert_connections = {}
-connection_lock = threading.Lock()
-pending_result_queries = {}
-pending_query_lock = threading.Lock()
DEVICE_CONFIGS = [
{
'port': 'COM6', 'baud_rate': 9600, 'device_type': 'vitek', 'alat_name': 'Vitek 1',
@@ -99,6 +97,8 @@ DEVICE_CONFIGS = [
# 'protocol': 'serial', 'flag_column': 'flg_bd2'
#},
]
+MYLA_HOST = '0.0.0.0'
+MYLA_PORT = 60090
# Karakter kontrol standar
STX, ETX, ACK, NAK, EOT, ENQ = b'\x02', b'\x03', b'\x06', b'\x15', b'\x04', b'\x05'
@@ -556,10 +556,186 @@ def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
# 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"
-
+
+def parse_myla_result(hl7_message, device_name="MYLA"):
+ """
+ Parser khusus untuk HL7 dari bioMérieux MYLA.
+ Menangani hasil BACT/ALERT (Pos/Neg) dan VITEK (Organisme & AST/Sensitivitas).
+ """
+ session = SessionLocal()
+ try:
+ segments = hl7_message.strip().split('\r')
+
+ sample_id = None
+ patient_id = ""
+ patient_name = ""
+ result_date = datetime.datetime.now()
+
+ # Penampung Hasil
+ kultur_id = [] # Untuk hasil organisme / Positif/Negatif
+ ast_results = [] # Untuk hasil sensitivitas antibiotik
+
+ for segment in segments:
+ fields = segment.split('|')
+ if not fields: continue
+ seg_type = fields[0]
+
+ if seg_type == 'MSH':
+ if len(fields) > 6 and fields[6]:
+ try:
+ result_date = datetime.datetime.strptime(fields[6][:14], "%Y%m%d%H%M%S")
+ except: pass
+
+ elif seg_type == 'PID':
+ if len(fields) > 3: patient_id = fields[3].replace('^', '')
+ if len(fields) > 5: patient_name = fields[5].replace('^', ' ').strip()
+
+ elif seg_type == 'OBR':
+ # OBR-3 (Filler Order Number) atau OBR-2 (Placer) biasanya berisi Sample ID
+ if len(fields) > 3 and fields[3]:
+ sample_id = fields[3].replace('^', '')
+ elif len(fields) > 2 and fields[2]:
+ sample_id = fields[2].replace('^', '')
+
+ elif seg_type == 'OBX':
+ # Parsing detail OBX
+ # OBX-3: Parameter (Misal: Organism / Nama Antibiotik)
+ # OBX-5: Nilai Hasil / MIC
+ # OBX-8: Interpretasi (S / I / R / + / -)
+
+ if len(fields) > 5:
+ test_param = fields[3].split('^')[1] if '^' in fields[3] else fields[3]
+ result_val = fields[5].replace('^', ' ').strip()
+
+ interpretation = ""
+ if len(fields) > 8 and fields[8]:
+ interpretation = fields[8].strip()
+
+ # Logika Pemisahan (Tergantung mapping kata kunci MYLA)
+ # Biasanya VITEK mengirim interpretasi 'S', 'I', 'R' untuk antibiotik
+ if interpretation in ['S', 'I', 'R']:
+ ast_results.append(f"{test_param}: {result_val} ({interpretation})")
+ else:
+ # Ini kemungkinan hasil Kultur / Identifikasi Organisme
+ kultur_id.append(f"{test_param}: {result_val}")
+
+ # --- GABUNGKAN HASIL ---
+ final_results = []
+ if kultur_id:
+ final_results.append("ID: " + ", ".join(kultur_id))
+ if ast_results:
+ final_results.append("AST: " + "; ".join(ast_results))
+
+ final_result_str = " | ".join(final_results) if final_results else "No Data"
+
+ # --- SIMPAN KE DB ---
+ if not sample_id:
+ # Fallback jika tidak ada ID
+ sample_id = f"ERR_MYLA_{datetime.datetime.now().strftime('%H%M%S')}"
+ final_result_str = f"[NO_ID] {final_result_str}"
+
+ logging.info(f"[MYLA] Save DB -> Sample: {sample_id}, Hasil: {final_result_str}")
+
+ new_entry = LisPhoenix(
+ no_id=sample_id,
+ seq_no=patient_id,
+ rnmpas=patient_name,
+ tgl_data=result_date,
+ rawdt=hl7_message,
+ organisme=final_result_str[:255], # Potong jika field database terbatas
+ alat=device_name
+ )
+ session.add(new_entry)
+ session.commit()
+
+ except Exception as e:
+ logging.error(f"[MYLA Parser] Error: {e}")
+ session.rollback()
+ finally:
+ session.close()
# ==========================================
# 4. NETWORK & COMMUNICATION LOGIC
# ==========================================
+def handle_myla_client(conn, addr):
+ """
+ TCP Handler untuk bioMérieux MYLA.
+ Hanya menerima HL7 berbungkus MLLP (\x0b ... \x1c\r).
+ """
+ logging.info(f"[MYLA-TCP] Koneksi baru dari {addr}")
+ buffer = b""
+
+ try:
+ while True:
+ data = conn.recv(4096)
+ if not data: break
+
+ buffer += data
+
+ # Cek penutup MLLP (\x1c\r)
+ if b'\x1c\r' in buffer:
+ # Pisahkan pesan jika ada beberapa pesan yang nempel
+ messages = buffer.split(b'\x1c\r')
+
+ for msg in messages[:-1]: # Abaikan yang terakhir (karena string kosong atau pesan belum selesai)
+ # Hapus pembuka MLLP (\x0b)
+ clean_msg_bytes = msg.replace(b'\x0b', b'')
+ hl7_str = clean_msg_bytes.decode('latin-1', errors='ignore')
+
+ if "MSH|" in hl7_str:
+ # Potong tepat dari MSH
+ hl7_str = hl7_str[hl7_str.find("MSH|"):]
+
+ # Ambil Control ID untuk ACK
+ incoming_control_id = ""
+ try:
+ msh_fields = hl7_str.split('\r')[0].split('|')
+ if len(msh_fields) > 9:
+ incoming_control_id = msh_fields[9]
+ except: pass
+
+ # --- PROSES HASIL (ORU) ---
+ if "ORU^" in hl7_str:
+ logging.info(f"[MYLA] Menerima hasil (ORU) ID: {incoming_control_id}")
+ parse_myla_result(hl7_str, device_name=f"MYLA-{addr[0]}")
+
+ # --- PROSES QUERY (QRY/QBP) - JIKA MYLA BERTANYA ORDER ---
+ elif "QRY^" in hl7_str or "QBP^" in hl7_str:
+ logging.info(f"[MYLA] Menerima Query (Belum diimplementasikan detail)")
+ # Anda bisa menambahkan logika lookup order disini mirip dengan GeneXpert
+ pass
+
+ # --- KIRIM ACK ---
+ if incoming_control_id:
+ ack_time = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
+ ack_msg = f"MSH|^~\\&|LIS|LAB|MYLA|bioMerieux|{ack_time}||ACK|{incoming_control_id}|P|2.5\rMSA|AA|{incoming_control_id}\r"
+ full_ack = f"\x0b{ack_msg}\x1c\r"
+ conn.sendall(full_ack.encode('utf-8'))
+ logging.info(f"[MYLA ACK] Terkirim untuk ID {incoming_control_id}")
+
+ # Sisakan bagian terakhir di buffer (kalau ada pesan yang terpotong)
+ buffer = messages[-1]
+
+ except Exception as e:
+ logging.error(f"[MYLA-TCP] Error koneksi {addr}: {e}")
+ finally:
+ conn.close()
+ logging.info(f"[MYLA-TCP] Koneksi {addr} ditutup.")
+
+# Tambahkan fungsi starter ini di atas blok if __name__ == '__main__':
+def start_myla_server(host, port):
+ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server.bind((host, port))
+ server.listen(5)
+ logging.info(f"[START] MYLA TCP Server berjalan di {host}:{port}")
+
+ while True:
+ client_socket, addr = server.accept()
+ # Jalankan di thread terpisah agar bisa tangani banyak koneksi
+ client_thread = threading.Thread(target=handle_myla_client, args=(client_socket, addr))
+ client_thread.daemon = True
+ client_thread.start()
+
+
# ==========================================
# HL7 TCP LISTENER FOR GENEXPERT
# ==========================================
@@ -1731,6 +1907,11 @@ if __name__ == "__main__":
t_tcp.start()
all_threads.append(t_tcp)
+ # 3. Start Thread TCP Server (MyLA)
+ myla_thread = threading.Thread(target=start_myla_server, args=(MYLA_HOST, MYLA_PORT))
+ myla_thread.start()
+ all_threads.append(myla_thread)
+
# 4. Start Thread HTTP API (Trigger dari Laravel)
t_http = threading.Thread(target=run_http_api_server, name="Manager-HTTP-API", daemon=True)
t_http.start()