Add MyLA Vitek3 flag handling
This commit is contained in:
No files matched your search
@@ -1078,6 +1078,7 @@ class FrontpageController extends Controller
|
|||||||
'flg_gxp1' => false,
|
'flg_gxp1' => false,
|
||||||
'flg_gxp2' => false,
|
'flg_gxp2' => false,
|
||||||
'flg_gxp3' => false,
|
'flg_gxp3' => false,
|
||||||
|
'flg_vitek3' => false,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
if ($pesan != ''){
|
if ($pesan != ''){
|
||||||
@@ -1295,6 +1296,7 @@ class FrontpageController extends Controller
|
|||||||
'flg_gxp1' => false,
|
'flg_gxp1' => false,
|
||||||
'flg_gxp2' => false,
|
'flg_gxp2' => false,
|
||||||
'flg_gxp3' => false,
|
'flg_gxp3' => false,
|
||||||
|
'flg_vitek3' => false,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
return response()->json(['status' => 'Sukses', 'message' => $pesan], 201);
|
return response()->json(['status' => 'Sukses', 'message' => $pesan], 201);
|
||||||
@@ -1544,6 +1546,7 @@ class FrontpageController extends Controller
|
|||||||
'flg_gxp1' => false,
|
'flg_gxp1' => false,
|
||||||
'flg_gxp2' => false,
|
'flg_gxp2' => false,
|
||||||
'flg_gxp3' => false,
|
'flg_gxp3' => false,
|
||||||
|
'flg_vitek3' => false,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1668,6 +1671,7 @@ class FrontpageController extends Controller
|
|||||||
'flg_gxp1' => false,
|
'flg_gxp1' => false,
|
||||||
'flg_gxp2' => false,
|
'flg_gxp2' => false,
|
||||||
'flg_gxp3' => false,
|
'flg_gxp3' => false,
|
||||||
|
'flg_vitek3' => false,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
if ($pesan != ''){
|
if ($pesan != ''){
|
||||||
@@ -1736,6 +1740,7 @@ class FrontpageController extends Controller
|
|||||||
'flg_gxp1' => false,
|
'flg_gxp1' => false,
|
||||||
'flg_gxp2' => false,
|
'flg_gxp2' => false,
|
||||||
'flg_gxp3' => false,
|
'flg_gxp3' => false,
|
||||||
|
'flg_vitek3' => false,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}catch (Exception $e) {
|
}catch (Exception $e) {
|
||||||
|
|||||||
+170
-76
@@ -102,7 +102,7 @@ MYLA_PORT = 60090
|
|||||||
|
|
||||||
# Karakter kontrol standar
|
# Karakter kontrol standar
|
||||||
STX, ETX, ACK, NAK, EOT, ENQ = b'\x02', b'\x03', b'\x06', b'\x15', b'\x04', b'\x05'
|
STX, ETX, ACK, NAK, EOT, ENQ = b'\x02', b'\x03', b'\x06', b'\x15', b'\x04', b'\x05'
|
||||||
RS, GS = b'\x1e', b'\x1d' # Record Separator, Group Separator
|
RS, GS = b'\x1e', b'\x1d'
|
||||||
ports_lock = threading.Lock()
|
ports_lock = threading.Lock()
|
||||||
active_serial_ports = {}
|
active_serial_ports = {}
|
||||||
|
|
||||||
@@ -183,6 +183,7 @@ class PaslabOrder(Base):
|
|||||||
flg_gxp1 = Column(Boolean, default=False)
|
flg_gxp1 = Column(Boolean, default=False)
|
||||||
flg_gxp2 = Column(Boolean, default=False)
|
flg_gxp2 = Column(Boolean, default=False)
|
||||||
flg_gxp3 = Column(Boolean, default=False)
|
flg_gxp3 = Column(Boolean, default=False)
|
||||||
|
flg_vitek3 = Column(Boolean, default=False)
|
||||||
created_at = Column(SqDateTime, default=datetime.datetime.now)
|
created_at = Column(SqDateTime, default=datetime.datetime.now)
|
||||||
updated_at = Column(SqDateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
|
updated_at = Column(SqDateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
|
||||||
|
|
||||||
@@ -271,7 +272,6 @@ def trigger_result_query_to_genexpert(accnumber, register_no, target_ip=None, wa
|
|||||||
if not target_ips:
|
if not target_ips:
|
||||||
return {"ok": False, "message": f"Koneksi GeneXpert {target_ip} tidak aktif."}
|
return {"ok": False, "message": f"Koneksi GeneXpert {target_ip} tidak aktif."}
|
||||||
else:
|
else:
|
||||||
# Default: kirim query ke semua GeneXpert yang sedang terkoneksi.
|
|
||||||
target_ips = active_ips
|
target_ips = active_ips
|
||||||
|
|
||||||
ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
||||||
@@ -301,7 +301,6 @@ def trigger_result_query_to_genexpert(accnumber, register_no, target_ip=None, wa
|
|||||||
try:
|
try:
|
||||||
conn.sendall(mllp_payload)
|
conn.sendall(mllp_payload)
|
||||||
sent_ips.append(ip)
|
sent_ips.append(ip)
|
||||||
logging.info(f"[GENEXPERT-QUERY] Kirim query hasil accnumber={accnumber} ke {ip}")
|
|
||||||
print(f"[GENEXPERT-QUERY] Kirim query hasil accnumber={accnumber} ke {ip}")
|
print(f"[GENEXPERT-QUERY] Kirim query hasil accnumber={accnumber} ke {ip}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failed_ips.append({"ip": ip, "error": str(e)})
|
failed_ips.append({"ip": ip, "error": str(e)})
|
||||||
@@ -530,7 +529,10 @@ def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
|
|||||||
# Mapping Data
|
# Mapping Data
|
||||||
sample_id = str(order.rnoreg)
|
sample_id = str(order.rnoreg)
|
||||||
pid_norm = order.norm if order.norm else ""
|
pid_norm = order.norm if order.norm else ""
|
||||||
pid_nama = order.nama if order.nama else "No Name"
|
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
|
# Gender
|
||||||
raw_gender = str(order.rjenis).upper()
|
raw_gender = str(order.rjenis).upper()
|
||||||
@@ -545,11 +547,12 @@ def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
|
|||||||
# GeneXpert standar biasanya terima format ORM di dalam DSR atau sequence PID-ORC-OBR
|
# GeneXpert standar biasanya terima format ORM di dalam DSR atau sequence PID-ORC-OBR
|
||||||
|
|
||||||
pid = f"PID|1||{pid_norm}||{pid_nama}|||{pid_gender}"
|
pid = f"PID|1||{pid_norm}||{pid_nama}|||{pid_gender}"
|
||||||
|
pv1 = f"PV1|1|I|{room}"
|
||||||
orc = f"ORC|NW|{sample_id}"
|
orc = f"ORC|NW|{sample_id}"
|
||||||
obr = f"OBR|1|{sample_id}||{test_code}^{nama_tes_db}^L|||{timestamp}"
|
obr = f"OBR|1|{sample_id}||{test_code}^{nama_tes_db}^L|||{timestamp}"
|
||||||
|
|
||||||
# Gabungkan
|
# Gabungkan
|
||||||
return f"{msh}\r{msa}\r{qrd}\r{qrf}\r{pid}\r{orc}\r{obr}\r"
|
return f"{msh}\r{msa}\r{qrd}\r{qrf}\r{pid}\r{pv1}\r{orc}\r{obr}\r"
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Jika TIDAK ADA ORDER (Not Found)
|
# Jika TIDAK ADA ORDER (Not Found)
|
||||||
@@ -559,8 +562,8 @@ def create_hl7_dsr_response(order, msg_control_id, qrd_segment):
|
|||||||
|
|
||||||
def parse_myla_result(hl7_message, device_name="MYLA"):
|
def parse_myla_result(hl7_message, device_name="MYLA"):
|
||||||
"""
|
"""
|
||||||
Parser khusus untuk HL7 dari bioMérieux MYLA.
|
Parser khusus untuk HL7 dari bioMérieux MYLA (Kalibrasi V4.9).
|
||||||
Menangani hasil BACT/ALERT (Pos/Neg) dan VITEK (Organisme & AST/Sensitivitas).
|
Menangani hasil BACT/ALERT (Kultur Darah) dan VITEK (Identifikasi & AST).
|
||||||
"""
|
"""
|
||||||
session = SessionLocal()
|
session = SessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -571,9 +574,8 @@ def parse_myla_result(hl7_message, device_name="MYLA"):
|
|||||||
patient_name = ""
|
patient_name = ""
|
||||||
result_date = datetime.datetime.now()
|
result_date = datetime.datetime.now()
|
||||||
|
|
||||||
# Penampung Hasil
|
kultur_id = []
|
||||||
kultur_id = [] # Untuk hasil organisme / Positif/Negatif
|
ast_results = []
|
||||||
ast_results = [] # Untuk hasil sensitivitas antibiotik
|
|
||||||
|
|
||||||
for segment in segments:
|
for segment in segments:
|
||||||
fields = segment.split('|')
|
fields = segment.split('|')
|
||||||
@@ -591,35 +593,38 @@ def parse_myla_result(hl7_message, device_name="MYLA"):
|
|||||||
if len(fields) > 5: patient_name = fields[5].replace('^', ' ').strip()
|
if len(fields) > 5: patient_name = fields[5].replace('^', ' ').strip()
|
||||||
|
|
||||||
elif seg_type == 'OBR':
|
elif seg_type == 'OBR':
|
||||||
# OBR-3 (Filler Order Number) atau OBR-2 (Placer) biasanya berisi Sample ID
|
|
||||||
if len(fields) > 3 and fields[3]:
|
if len(fields) > 3 and fields[3]:
|
||||||
sample_id = fields[3].replace('^', '')
|
sample_id = fields[3].replace('^', '')
|
||||||
elif len(fields) > 2 and fields[2]:
|
elif len(fields) > 2 and fields[2]:
|
||||||
sample_id = fields[2].replace('^', '')
|
sample_id = fields[2].replace('^', '')
|
||||||
|
|
||||||
elif seg_type == 'OBX':
|
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:
|
if len(fields) > 5:
|
||||||
test_param = fields[3].split('^')[1] if '^' in fields[3] else fields[3]
|
test_param = fields[3].split('^')[1] if '^' in fields[3] else fields[3]
|
||||||
result_val = fields[5].replace('^', ' ').strip()
|
|
||||||
|
if "Time To Detection" in test_param:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw_val = fields[5]
|
||||||
|
val_parts = raw_val.split('^')
|
||||||
|
|
||||||
|
if len(val_parts) > 1:
|
||||||
|
if val_parts[0] in ['<', '<=', '>', '>=', '=']:
|
||||||
|
result_val = f"{val_parts[0]} {val_parts[1]}"
|
||||||
|
else:
|
||||||
|
result_val = val_parts[1]
|
||||||
|
else:
|
||||||
|
result_val = raw_val.strip()
|
||||||
|
|
||||||
interpretation = ""
|
interpretation = ""
|
||||||
if len(fields) > 8 and fields[8]:
|
if len(fields) > 8 and fields[8]:
|
||||||
interpretation = fields[8].strip()
|
interpretation = fields[8].split('^')[0].strip().upper()
|
||||||
|
|
||||||
# Logika Pemisahan (Tergantung mapping kata kunci MYLA)
|
if interpretation in ['S', 'I', 'R', 'NS']:
|
||||||
# Biasanya VITEK mengirim interpretasi 'S', 'I', 'R' untuk antibiotik
|
|
||||||
if interpretation in ['S', 'I', 'R']:
|
|
||||||
ast_results.append(f"{test_param}: {result_val} ({interpretation})")
|
ast_results.append(f"{test_param}: {result_val} ({interpretation})")
|
||||||
else:
|
else:
|
||||||
# Ini kemungkinan hasil Kultur / Identifikasi Organisme
|
|
||||||
kultur_id.append(f"{test_param}: {result_val}")
|
kultur_id.append(f"{test_param}: {result_val}")
|
||||||
|
|
||||||
# --- GABUNGKAN HASIL ---
|
|
||||||
final_results = []
|
final_results = []
|
||||||
if kultur_id:
|
if kultur_id:
|
||||||
final_results.append("ID: " + ", ".join(kultur_id))
|
final_results.append("ID: " + ", ".join(kultur_id))
|
||||||
@@ -628,9 +633,7 @@ def parse_myla_result(hl7_message, device_name="MYLA"):
|
|||||||
|
|
||||||
final_result_str = " | ".join(final_results) if final_results else "No Data"
|
final_result_str = " | ".join(final_results) if final_results else "No Data"
|
||||||
|
|
||||||
# --- SIMPAN KE DB ---
|
|
||||||
if not sample_id:
|
if not sample_id:
|
||||||
# Fallback jika tidak ada ID
|
|
||||||
sample_id = f"ERR_MYLA_{datetime.datetime.now().strftime('%H%M%S')}"
|
sample_id = f"ERR_MYLA_{datetime.datetime.now().strftime('%H%M%S')}"
|
||||||
final_result_str = f"[NO_ID] {final_result_str}"
|
final_result_str = f"[NO_ID] {final_result_str}"
|
||||||
|
|
||||||
@@ -642,7 +645,7 @@ def parse_myla_result(hl7_message, device_name="MYLA"):
|
|||||||
rnmpas=patient_name,
|
rnmpas=patient_name,
|
||||||
tgl_data=result_date,
|
tgl_data=result_date,
|
||||||
rawdt=hl7_message,
|
rawdt=hl7_message,
|
||||||
organisme=final_result_str[:255], # Potong jika field database terbatas
|
organisme=final_result_str[:255],
|
||||||
alat=device_name
|
alat=device_name
|
||||||
)
|
)
|
||||||
session.add(new_entry)
|
session.add(new_entry)
|
||||||
@@ -700,9 +703,73 @@ def handle_myla_client(conn, addr):
|
|||||||
|
|
||||||
# --- PROSES QUERY (QRY/QBP) - JIKA MYLA BERTANYA ORDER ---
|
# --- PROSES QUERY (QRY/QBP) - JIKA MYLA BERTANYA ORDER ---
|
||||||
elif "QRY^" in hl7_str or "QBP^" in hl7_str:
|
elif "QRY^" in hl7_str or "QBP^" in hl7_str:
|
||||||
logging.info(f"[MYLA] Menerima Query (Belum diimplementasikan detail)")
|
client_ip = addr[0]
|
||||||
# Anda bisa menambahkan logika lookup order disini mirip dengan GeneXpert
|
logging.info(f"[MYLA] Menerima Query dari {client_ip} (Control ID: {incoming_control_id})")
|
||||||
pass
|
|
||||||
|
# Khusus MyLA gunakan flag VITEK3.
|
||||||
|
target_flag_col = "flg_vitek3"
|
||||||
|
|
||||||
|
lines = hl7_str.split('\r')
|
||||||
|
search_sample_id = None
|
||||||
|
qrd_line = ""
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith("QRD|"):
|
||||||
|
qrd_line = line
|
||||||
|
fields = line.split('|')
|
||||||
|
if len(fields) > 8 and fields[8]:
|
||||||
|
search_sample_id = fields[8].strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not search_sample_id:
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith("QPD|"):
|
||||||
|
qpd_fields = line.split('|')
|
||||||
|
for candidate in qpd_fields[3:]:
|
||||||
|
value = candidate.strip()
|
||||||
|
if value:
|
||||||
|
search_sample_id = value.split('^')[0].strip()
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
session = SessionLocal()
|
||||||
|
try:
|
||||||
|
flag_attr = getattr(PaslabOrder, target_flag_col, None)
|
||||||
|
if flag_attr is None:
|
||||||
|
raise Exception(f"Kolom flag tidak valid: {target_flag_col}")
|
||||||
|
|
||||||
|
order = None
|
||||||
|
if search_sample_id:
|
||||||
|
order = session.query(PaslabOrder).filter(
|
||||||
|
PaslabOrder.rnoreg == search_sample_id,
|
||||||
|
(flag_attr == False) | (flag_attr == None)
|
||||||
|
).first()
|
||||||
|
else:
|
||||||
|
# Fallback: jika query tidak membawa sample ID, kirim order pending paling awal.
|
||||||
|
order = session.query(PaslabOrder).filter(
|
||||||
|
(flag_attr == False) | (flag_attr == None)
|
||||||
|
).order_by(PaslabOrder.urut.asc()).first()
|
||||||
|
|
||||||
|
if order:
|
||||||
|
reply_msg = create_hl7_dsr_response(order, incoming_control_id, qrd_line)
|
||||||
|
conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
|
||||||
|
|
||||||
|
setattr(order, target_flag_col, True)
|
||||||
|
session.commit()
|
||||||
|
logging.info(f"[MYLA] Order terkirim rnoreg={order.rnoreg}, set {target_flag_col}=TRUE")
|
||||||
|
else:
|
||||||
|
reply_msg = create_hl7_dsr_response(None, incoming_control_id, qrd_line)
|
||||||
|
conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
|
||||||
|
logging.info(f"[MYLA] Tidak ada order pending untuk sample={search_sample_id or '-'}")
|
||||||
|
|
||||||
|
except Exception as db_err:
|
||||||
|
session.rollback()
|
||||||
|
logging.error(f"[MYLA] Gagal proses query: {db_err}")
|
||||||
|
reply_msg = create_hl7_dsr_response(None, incoming_control_id, qrd_line)
|
||||||
|
conn.sendall(f"\x0b{reply_msg}\x1c\r".encode('utf-8'))
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
continue
|
||||||
|
|
||||||
# --- KIRIM ACK ---
|
# --- KIRIM ACK ---
|
||||||
if incoming_control_id:
|
if incoming_control_id:
|
||||||
@@ -721,20 +788,29 @@ def handle_myla_client(conn, addr):
|
|||||||
conn.close()
|
conn.close()
|
||||||
logging.info(f"[MYLA-TCP] Koneksi {addr} ditutup.")
|
logging.info(f"[MYLA-TCP] Koneksi {addr} ditutup.")
|
||||||
|
|
||||||
# Tambahkan fungsi starter ini di atas blok if __name__ == '__main__':
|
|
||||||
def start_myla_server(host, port):
|
def start_myla_server(host, port):
|
||||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
server.bind((host, port))
|
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
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()
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
server.bind((host, port))
|
||||||
|
server.listen(5)
|
||||||
|
logging.info(f"[START] MYLA TCP Server berjalan di {host}:{port}")
|
||||||
|
print(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), daemon=True)
|
||||||
|
client_thread.start()
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical(f"[START] Gagal start MYLA TCP Server di {host}:{port}: {e}")
|
||||||
|
print(f"[START] Gagal start MYLA TCP Server di {host}:{port}: {e}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
server.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# HL7 TCP LISTENER FOR GENEXPERT
|
# HL7 TCP LISTENER FOR GENEXPERT
|
||||||
@@ -1201,6 +1277,41 @@ def parse_and_save_vitek_result(raw_data, port_name="VITEK"):
|
|||||||
finally:
|
finally:
|
||||||
session.close()
|
session.close()
|
||||||
|
|
||||||
|
def split_patient_name(full_name):
|
||||||
|
"""
|
||||||
|
Pecah nama pasien menjadi first_name dan last_name.
|
||||||
|
- Jika ada '^', diasumsikan format ASTM/HL7: Last^First.
|
||||||
|
- Jika nama biasa, token terakhir = last_name, sisanya = first_name.
|
||||||
|
"""
|
||||||
|
if not full_name:
|
||||||
|
return "NO", "NAME"
|
||||||
|
|
||||||
|
raw = str(full_name).strip()
|
||||||
|
if not raw:
|
||||||
|
return "NO", "NAME"
|
||||||
|
|
||||||
|
first_name = ""
|
||||||
|
last_name = ""
|
||||||
|
|
||||||
|
if "^" in raw:
|
||||||
|
parts = [p.strip() for p in raw.split("^") if p.strip()]
|
||||||
|
if len(parts) >= 2:
|
||||||
|
last_name = parts[0]
|
||||||
|
first_name = parts[1]
|
||||||
|
elif len(parts) == 1:
|
||||||
|
first_name = parts[0]
|
||||||
|
else:
|
||||||
|
tokens = raw.split()
|
||||||
|
if len(tokens) >= 2:
|
||||||
|
last_name = tokens[-1]
|
||||||
|
first_name = " ".join(tokens[:-1])
|
||||||
|
elif len(tokens) == 1:
|
||||||
|
first_name = tokens[0]
|
||||||
|
|
||||||
|
first_name = (first_name or "NO").upper()[:20]
|
||||||
|
last_name = (last_name or "NAME").upper()[:20]
|
||||||
|
return first_name, last_name
|
||||||
|
|
||||||
def create_vitek_order_message(order):
|
def create_vitek_order_message(order):
|
||||||
"""
|
"""
|
||||||
Membuat Frame Order Vitek sesuai Manual Ref 514937.
|
Membuat Frame Order Vitek sesuai Manual Ref 514937.
|
||||||
@@ -1210,8 +1321,11 @@ def create_vitek_order_message(order):
|
|||||||
# Field Delimiter menggunakan Pipe '|'
|
# Field Delimiter menggunakan Pipe '|'
|
||||||
pid = str(order.norm).strip() if order.norm else ""
|
pid = str(order.norm).strip() if order.norm else ""
|
||||||
sid = str(order.rnoreg).strip() if order.rnoreg else ""
|
sid = str(order.rnoreg).strip() if order.rnoreg else ""
|
||||||
p_name = str(order.nama).strip().replace('^', ' ').upper()[:20] if order.nama else "NO NAME"
|
first_name, last_name = split_patient_name(order.nama)
|
||||||
|
p_name = f"{last_name}^{first_name}"
|
||||||
specimen = str(order.kd_spesimen).upper() if order.kd_spesimen else "BLOOD"
|
specimen = str(order.kd_spesimen).upper() if order.kd_spesimen else "BLOOD"
|
||||||
|
room = str(getattr(order, 'ruangan', "") or "RSSA MALANG").strip().upper()
|
||||||
|
room = room.replace("|", " ").replace("^", " ")[:30]
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
date_str = now.strftime("%m/%d/%Y")
|
date_str = now.strftime("%m/%d/%Y")
|
||||||
@@ -1222,6 +1336,7 @@ def create_vitek_order_message(order):
|
|||||||
content_body = (
|
content_body = (
|
||||||
f"mtmpr|pi{pid}|pn{p_name}"
|
f"mtmpr|pi{pid}|pn{p_name}"
|
||||||
f"|si|ss{specimen}"
|
f"|si|ss{specimen}"
|
||||||
|
f"|lo{room}"
|
||||||
f"|s1{date_str}|s2{time_str}"
|
f"|s1{date_str}|s2{time_str}"
|
||||||
f"|ci{sid}|t11|zz"
|
f"|ci{sid}|t11|zz"
|
||||||
)
|
)
|
||||||
@@ -1255,9 +1370,7 @@ def manage_vitek_port(config):
|
|||||||
flag_col = config.get('flag_column')
|
flag_col = config.get('flag_column')
|
||||||
alat_name = config.get('alat_name', 'VITEK')
|
alat_name = config.get('alat_name', 'VITEK')
|
||||||
|
|
||||||
logging.info(f"[{port_name}] START VITEK SERVICE (Relaxed Mode)...")
|
|
||||||
print(f"[{port_name}] START VITEK SERVICE (Relaxed Mode)...")
|
print(f"[{port_name}] START VITEK SERVICE (Relaxed Mode)...")
|
||||||
# Deteksi stuck dalam window waktu tertentu.
|
|
||||||
STUCK_WINDOW_SEC = 120
|
STUCK_WINDOW_SEC = 120
|
||||||
stuck_count = 0
|
stuck_count = 0
|
||||||
stuck_window_start = None
|
stuck_window_start = None
|
||||||
@@ -1276,7 +1389,7 @@ def manage_vitek_port(config):
|
|||||||
) as ser:
|
) as ser:
|
||||||
|
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
logging.info(f"[{port_name}] Ready & Listening.")
|
print(f"[{port_name}] Ready & Listening.")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@@ -1287,14 +1400,12 @@ def manage_vitek_port(config):
|
|||||||
|
|
||||||
# --- HANDSHAKE ---
|
# --- HANDSHAKE ---
|
||||||
if header == b'\x05':
|
if header == b'\x05':
|
||||||
logging.info(f"[{port_name}] Got ENQ -> Reply ACK")
|
|
||||||
print(f"[{port_name}] Got ENQ -> Reply ACK")
|
print(f"[{port_name}] Got ENQ -> Reply ACK")
|
||||||
ser.write(b'\x06')
|
ser.write(b'\x06')
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
# --- DATA FRAME ---
|
# --- DATA FRAME ---
|
||||||
elif header == b'\x02':
|
elif header == b'\x02':
|
||||||
logging.info(f"[{port_name}] Frame Start. Reading...")
|
|
||||||
print(f"[{port_name}] Frame Start. Reading...")
|
print(f"[{port_name}] Frame Start. Reading...")
|
||||||
original_timeout = ser.timeout
|
original_timeout = ser.timeout
|
||||||
ser.timeout = 8
|
ser.timeout = 8
|
||||||
@@ -1310,12 +1421,9 @@ def manage_vitek_port(config):
|
|||||||
is_valid = True
|
is_valid = True
|
||||||
elif b'\x1d' in full_frame[-20:]:
|
elif b'\x1d' in full_frame[-20:]:
|
||||||
is_valid = True
|
is_valid = True
|
||||||
logging.warning(f"[{port_name}] Frame tanpa ETX tapi ada Checksum. Menerima paksa...")
|
|
||||||
print(f"[{port_name}] Frame tanpa ETX tapi ada Checksum. Menerima paksa...")
|
print(f"[{port_name}] Frame tanpa ETX tapi ada Checksum. Menerima paksa...")
|
||||||
if is_valid:
|
if is_valid:
|
||||||
logging.info(f"[{port_name}] Frame Accepted. Sending ACK.")
|
|
||||||
print(f"[{port_name}] Frame OK -> ACK Sent.")
|
print(f"[{port_name}] Frame OK -> ACK Sent.")
|
||||||
print(f"[{port_name}] FULL FRAME: {full_frame}")
|
|
||||||
# 1. KIRIM ACK (WAJIB)
|
# 1. KIRIM ACK (WAJIB)
|
||||||
ser.write(b'\x06')
|
ser.write(b'\x06')
|
||||||
stuck_count = 0
|
stuck_count = 0
|
||||||
@@ -1362,7 +1470,6 @@ def manage_vitek_port(config):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if pending_order:
|
if pending_order:
|
||||||
logging.info(f"[{port_name}] Ada Order: {pending_order.rnoreg}. Coba Handshake...")
|
|
||||||
print(f"[{port_name}] Ada Order: {pending_order.rnoreg}...")
|
print(f"[{port_name}] Ada Order: {pending_order.rnoreg}...")
|
||||||
|
|
||||||
# --- LOGIC HANDSHAKE DENGAN RETRY ---
|
# --- LOGIC HANDSHAKE DENGAN RETRY ---
|
||||||
@@ -1389,7 +1496,7 @@ def manage_vitek_port(config):
|
|||||||
stuck_count = 0
|
stuck_count = 0
|
||||||
stuck_window_start = None
|
stuck_window_start = None
|
||||||
# === KIRIM DATA ORDER ===
|
# === KIRIM DATA ORDER ===
|
||||||
logging.info(f"[{port_name}] Handshake OK. Kirim Frames...")
|
print(f"[{port_name}] Handshake OK. Kirim Frames...")
|
||||||
frames = create_vitek_order_message(pending_order)
|
frames = create_vitek_order_message(pending_order)
|
||||||
all_sent = True
|
all_sent = True
|
||||||
|
|
||||||
@@ -1412,7 +1519,6 @@ def manage_vitek_port(config):
|
|||||||
ser.write(b'\x04') # EOT
|
ser.write(b'\x04') # EOT
|
||||||
|
|
||||||
if all_sent:
|
if all_sent:
|
||||||
logging.info(f"[{port_name}] Order SELESAI Terkirim.")
|
|
||||||
print(f"[{port_name}] Order SELESAI Terkirim.")
|
print(f"[{port_name}] Order SELESAI Terkirim.")
|
||||||
setattr(pending_order, flag_col, True)
|
setattr(pending_order, flag_col, True)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -1420,12 +1526,12 @@ def manage_vitek_port(config):
|
|||||||
stuck_window_start = None
|
stuck_window_start = None
|
||||||
else:
|
else:
|
||||||
logging.error(f"[{port_name}] Order Gagal (No ACK).")
|
logging.error(f"[{port_name}] Order Gagal (No ACK).")
|
||||||
|
print(f"[{port_name}] Order Gagal (No ACK).")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# === FORCE RESET (ANTI-STUCK) ===
|
# === FORCE RESET (ANTI-STUCK) ===
|
||||||
# Jika sudah 3x ENQ tidak dibalas, anggap alat 'bengong'
|
# Jika sudah 3x ENQ tidak dibalas, anggap alat 'bengong'
|
||||||
# Kirim EOT untuk mereset status alat
|
# Kirim EOT untuk mereset status alat
|
||||||
logging.warning(f"[{port_name}] Alat Sibuk/Stuck. Kirim Force EOT.")
|
|
||||||
print(f"[{port_name}] Alat Sibuk/Stuck. Kirim Force EOT.")
|
print(f"[{port_name}] Alat Sibuk/Stuck. Kirim Force EOT.")
|
||||||
ser.write(b'\x04')
|
ser.write(b'\x04')
|
||||||
time.sleep(2.0)
|
time.sleep(2.0)
|
||||||
@@ -1584,8 +1690,9 @@ def create_astm_order_message(order):
|
|||||||
# --- 1. PERSIAPAN DATA ---
|
# --- 1. PERSIAPAN DATA ---
|
||||||
pid = str(order.norm).strip() if order.norm else ""
|
pid = str(order.norm).strip() if order.norm else ""
|
||||||
sid = str(order.rnoreg).strip() if order.rnoreg else ""
|
sid = str(order.rnoreg).strip() if order.rnoreg else ""
|
||||||
# Nama Pasien: Ganti karakter topi '^' dengan spasi agar tidak merusak format
|
# Nama pasien dipisah agar mengikuti format ASTM: Last^First
|
||||||
p_name = str(order.nama).strip().replace('^', ' ')[:20] if order.nama else "No Name"
|
first_name, last_name = split_patient_name(order.nama)
|
||||||
|
p_name = f"{last_name}^{first_name}"
|
||||||
sex = "M" if str(order.rjenis).upper().startswith("L") else "F"
|
sex = "M" if str(order.rjenis).upper().startswith("L") else "F"
|
||||||
|
|
||||||
# Lokasi / Ruangan (Field 26)
|
# Lokasi / Ruangan (Field 26)
|
||||||
@@ -1669,7 +1776,7 @@ def manage_bd_port(config):
|
|||||||
port_name = config['port']
|
port_name = config['port']
|
||||||
flag_col = config.get('flag_column')
|
flag_col = config.get('flag_column')
|
||||||
alat_name = config.get('alat_name', 'BD')
|
alat_name = config.get('alat_name', 'BD')
|
||||||
logging.info(f"[{port_name}] Membuka port untuk alat {alat_name}...")
|
print(f"[{port_name}] Membuka port untuk alat {alat_name}...")
|
||||||
|
|
||||||
# Buffer untuk menampung pecahan data
|
# Buffer untuk menampung pecahan data
|
||||||
rx_buffer = b""
|
rx_buffer = b""
|
||||||
@@ -1695,7 +1802,6 @@ def manage_bd_port(config):
|
|||||||
if data_chunk:
|
if data_chunk:
|
||||||
# 1. Handle Handshake Awal (ENQ)
|
# 1. Handle Handshake Awal (ENQ)
|
||||||
if b'\x05' in data_chunk:
|
if b'\x05' in data_chunk:
|
||||||
logging.info(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.")
|
|
||||||
print(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.")
|
print(f"[{port_name}] Terima ENQ (Alat mau kirim data). Kirim ACK.")
|
||||||
ser.write(b'\x06') # ACK
|
ser.write(b'\x06') # ACK
|
||||||
rx_buffer = "" # Reset buffer untuk data baru
|
rx_buffer = "" # Reset buffer untuk data baru
|
||||||
@@ -1703,7 +1809,6 @@ def manage_bd_port(config):
|
|||||||
|
|
||||||
# 2. Handle Akhir Transmisi (EOT)
|
# 2. Handle Akhir Transmisi (EOT)
|
||||||
if b'\x04' in data_chunk:
|
if b'\x04' in data_chunk:
|
||||||
logging.info(f"[{port_name}] Terima EOT (Selesai). Memproses data...")
|
|
||||||
print(f"[{port_name}] Terima EOT (Selesai). Memproses data...")
|
print(f"[{port_name}] Terima EOT (Selesai). Memproses data...")
|
||||||
# Proses semua data yang terkumpul di buffer
|
# Proses semua data yang terkumpul di buffer
|
||||||
parse_and_save_bd_result(rx_buffer, alat_name)
|
parse_and_save_bd_result(rx_buffer, alat_name)
|
||||||
@@ -1748,7 +1853,6 @@ def manage_bd_port(config):
|
|||||||
|
|
||||||
if pending_order:
|
if pending_order:
|
||||||
has_activity = True # Jangan sleep lama-lama
|
has_activity = True # Jangan sleep lama-lama
|
||||||
logging.info(f"[{port_name}] Menemukan Order: {pending_order.rnoreg}. Memulai Handshake...")
|
|
||||||
print(f"[{port_name}] Menemukan Order: {pending_order.rnoreg}. Memulai Handshake...")
|
print(f"[{port_name}] Menemukan Order: {pending_order.rnoreg}. Memulai Handshake...")
|
||||||
# --- STEP 1: HANDSHAKE (ENQ) ---
|
# --- STEP 1: HANDSHAKE (ENQ) ---
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
@@ -1758,7 +1862,6 @@ def manage_bd_port(config):
|
|||||||
ack_response = ser.read(1)
|
ack_response = ser.read(1)
|
||||||
|
|
||||||
if ack_response == b'\x06':
|
if ack_response == b'\x06':
|
||||||
logging.info(f"[{port_name}] Handshake Sukses (Dapat ACK). Menunggu alat siap...")
|
|
||||||
print(f"[{port_name}] Handshake Sukses (Dapat ACK). Menunggu alat siap...")
|
print(f"[{port_name}] Handshake Sukses (Dapat ACK). Menunggu alat siap...")
|
||||||
# --- PERBAIKAN 1: BERI JEDA SETELAH HANDSHAKE ---
|
# --- PERBAIKAN 1: BERI JEDA SETELAH HANDSHAKE ---
|
||||||
# Mesin butuh napas sebelum terima data panjang
|
# Mesin butuh napas sebelum terima data panjang
|
||||||
@@ -1778,7 +1881,6 @@ def manage_bd_port(config):
|
|||||||
while retry_count < max_retries:
|
while retry_count < max_retries:
|
||||||
ser.reset_input_buffer()
|
ser.reset_input_buffer()
|
||||||
|
|
||||||
logging.info(f"[{port_name}] Kirim Frame {i+1} (Percobaan {retry_count+1})...")
|
|
||||||
print(f"[{port_name}] Kirim Frame {i+1} (Percobaan {retry_count+1})...")
|
print(f"[{port_name}] Kirim Frame {i+1} (Percobaan {retry_count+1})...")
|
||||||
ser.write(frame)
|
ser.write(frame)
|
||||||
|
|
||||||
@@ -1790,21 +1892,18 @@ def manage_bd_port(config):
|
|||||||
|
|
||||||
if frame_ack == b'\x06': # ACK (Sukses)
|
if frame_ack == b'\x06': # ACK (Sukses)
|
||||||
frame_success = True
|
frame_success = True
|
||||||
logging.info(f"[{port_name}] Frame {i+1} ACK diterima.")
|
|
||||||
print(f"[{port_name}] Frame {i+1} ACK diterima.")
|
print(f"[{port_name}] Frame {i+1} ACK diterima.")
|
||||||
# Beri jeda dikit sebelum kirim frame berikutnya
|
# Beri jeda dikit sebelum kirim frame berikutnya
|
||||||
time.sleep(0.2)
|
time.sleep(0.2)
|
||||||
break
|
break
|
||||||
|
|
||||||
elif frame_ack == b'\x15': # NAK (Ditolak - Checksum Salah)
|
elif frame_ack == b'\x15': # NAK (Ditolak - Checksum Salah)
|
||||||
logging.warning(f"[{port_name}] Frame ditolak (NAK). Checksum mungkin salah.")
|
|
||||||
print(f"[{port_name}] Frame ditolak (NAK). Checksum mungkin salah.")
|
print(f"[{port_name}] Frame ditolak (NAK). Checksum mungkin salah.")
|
||||||
time.sleep(2) # Tunggu 2 detik
|
time.sleep(2) # Tunggu 2 detik
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
|
|
||||||
elif not frame_ack: # Timeout (Sepi)
|
elif not frame_ack: # Timeout (Sepi)
|
||||||
# --- PERBAIKAN 2: BERI JEDA SAAT TIMEOUT ---
|
# --- PERBAIKAN 2: BERI JEDA SAAT TIMEOUT ---
|
||||||
logging.warning(f"[{port_name}] Timeout (Alat diam). Menunggu sebelum retry...")
|
|
||||||
print(f"[{port_name}] Timeout (Alat diam). Menunggu sebelum retry...")
|
print(f"[{port_name}] Timeout (Alat diam). Menunggu sebelum retry...")
|
||||||
time.sleep(2) # Tunggu 2 detik agar alat recover
|
time.sleep(2) # Tunggu 2 detik agar alat recover
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
@@ -1825,7 +1924,6 @@ def manage_bd_port(config):
|
|||||||
# ---------------------------------------------------
|
# ---------------------------------------------------
|
||||||
if all_frames_sent:
|
if all_frames_sent:
|
||||||
ser.write(b'\x04') # EOT (End of Transmission)
|
ser.write(b'\x04') # EOT (End of Transmission)
|
||||||
logging.info(f"[{port_name}] Order {pending_order.rnoreg} SUKSES Terkirim.")
|
|
||||||
print(f"[{port_name}] Order {pending_order.rnoreg} SUKSES Terkirim.")
|
print(f"[{port_name}] Order {pending_order.rnoreg} SUKSES Terkirim.")
|
||||||
# Update Database
|
# Update Database
|
||||||
setattr(pending_order, flag_col, True)
|
setattr(pending_order, flag_col, True)
|
||||||
@@ -1908,7 +2006,12 @@ if __name__ == "__main__":
|
|||||||
all_threads.append(t_tcp)
|
all_threads.append(t_tcp)
|
||||||
|
|
||||||
# 3. Start Thread TCP Server (MyLA)
|
# 3. Start Thread TCP Server (MyLA)
|
||||||
myla_thread = threading.Thread(target=start_myla_server, args=(MYLA_HOST, MYLA_PORT))
|
myla_thread = threading.Thread(
|
||||||
|
target=start_myla_server,
|
||||||
|
args=(MYLA_HOST, MYLA_PORT),
|
||||||
|
name="Manager-TCP-MyLA",
|
||||||
|
daemon=True
|
||||||
|
)
|
||||||
myla_thread.start()
|
myla_thread.start()
|
||||||
all_threads.append(myla_thread)
|
all_threads.append(myla_thread)
|
||||||
|
|
||||||
@@ -1918,31 +2021,22 @@ if __name__ == "__main__":
|
|||||||
all_threads.append(t_http)
|
all_threads.append(t_http)
|
||||||
|
|
||||||
# 5. LOOP UTAMA (Keep-Alive & Monitoring)
|
# 5. LOOP UTAMA (Keep-Alive & Monitoring)
|
||||||
# Ini sekarang bisa berjalan karena Flask sudah dipindah ke thread
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
logging.debug(f"--- Monitoring {len(all_threads)} Threads ---")
|
|
||||||
print(f"--- Monitoring {len(all_threads)} Threads ---")
|
print(f"--- Monitoring {len(all_threads)} Threads ---")
|
||||||
# Cek status setiap thread
|
|
||||||
alive_count = 0
|
alive_count = 0
|
||||||
for t in all_threads:
|
for t in all_threads:
|
||||||
if t.is_alive():
|
if t.is_alive():
|
||||||
alive_count += 1
|
alive_count += 1
|
||||||
else:
|
else:
|
||||||
logging.warning(f"!!! THREAD MATI: {t.name} !!!")
|
|
||||||
print(f"!!! THREAD MATI: {t.name} !!!")
|
print(f"!!! THREAD MATI: {t.name} !!!")
|
||||||
# Disini Anda bisa menambahkan logika restart thread jika mati
|
|
||||||
|
|
||||||
# Jika semua mati, exit (atau restart service)
|
|
||||||
if alive_count == 0:
|
if alive_count == 0:
|
||||||
logging.critical("Semua thread mati. System Shutdown.")
|
|
||||||
print("Semua thread mati. System Shutdown.")
|
print("Semua thread mati. System Shutdown.")
|
||||||
break
|
break
|
||||||
|
|
||||||
time.sleep(10) # Cek setiap 10 detik (Hemat CPU)
|
time.sleep(10)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
logging.info("Mematikan Service (Ctrl+C)...")
|
|
||||||
print("Mematikan Service (Ctrl+C)...")
|
print("Mematikan Service (Ctrl+C)...")
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
# Thread daemon akan mati otomatis saat main exit
|
|
||||||
Reference in New Issue
Block a user