LeakTest_HMI/HMI/python_hmi/ehg_leaktest.py

493 lines
20 KiB
Python

import sys
import os
import serial
import struct
import serial.tools.list_ports
import pyqtgraph as pg
import pandas as pd
import time
import json
from PyQt6.QtWidgets import QApplication, QVBoxLayout, QWidget, QLineEdit, QPushButton, QLabel, QHBoxLayout
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QIcon
from PyQt6.QtCore import QSettings
# -------------------------- Arduino detection --------------------------
def find_arduino():
ports = list(serial.tools.list_ports.comports())
print(f"Found ports: {[p.description for p in ports]}") # DEBUG LOG
for port in ports:
print(f"Checking port: {port.device} - {port.description}") # DEBUG LOG
if any(kw in port.description for kw in ["Arduino", "CH340", "USB Serial", "FTDI"]):
print(f"Arduino found at: {port.device}")
return port.device
print("No Arduino found")
return None
arduino_port = find_arduino()
ser = serial.Serial(arduino_port, 9600, timeout=10) if arduino_port else None
# --------------------------- Config JSON helpers ---------------------------
CONFIG_JSON_FILE = "presets.json"
DEFAULT_PRESETS = {
"Anode": {"setpoint_start": 160, "setpoint_end": 150, "setwindow": 60, "marker_interval": 10},
"Cathode": {"setpoint_start": 160, "setpoint_end": 150, "setwindow": 60, "marker_interval": 10},
"Coolant": {"setpoint_start": 160, "setpoint_end": 150, "setwindow": 60, "marker_interval": 10},
}
def load_presets():
if os.path.exists(CONFIG_JSON_FILE):
try:
with open(CONFIG_JSON_FILE, "r") as f:
data = json.load(f)
return data
except Exception as e:
print("Failed to load preset JSON, using defaults:", e)
return DEFAULT_PRESETS.copy()
def save_presets(presets: dict):
try:
with open(CONFIG_JSON_FILE, "w") as f:
json.dump(presets, f, indent=4)
print("Presets saved to JSON.")
except Exception as e:
print("Failed to save presets:", e)
# -------------------------- HMI Class --------------------------
class HMI(QWidget):
def __init__(self):
super().__init__()
# -------------------------- Data folder --------------------------
self.data_dir = os.path.join(os.getcwd(), "Data_log")
os.makedirs(self.data_dir, exist_ok=True)
# -------------------------- Window Setup --------------------------
self.setWindowTitle("Leak Test")
self.setWindowIcon(QIcon("ehg_icon.png"))
self.setGeometry(100, 100, 900, 550)
self.settings = QSettings("EHG", "LeakTestHMI")
self.current_preset = None # Track the currently selected preset
# -------------------------- Data Storage --------------------------
self.start_time = time.time()
self.time_data = []
self.pressure_data = []
self.marker_items = []
# -------------------------- Default / Saved Configurations --------------------------
self.setpoint_start = float(self.settings.value("setpoint_start", 25))
self.setpoint_end = float(self.settings.value("setpoint_end", 12))
self.setwindow = float(self.settings.value("setwindow", 900))
self.marker_interval = float(self.settings.value("marker_interval", 20))
# Load presets from JSON file
self.presets = load_presets()
# -------------------------- Layouts --------------------------
main_layout = QVBoxLayout()
self.setLayout(main_layout)
# Date display
self.date_label = QLabel()
self.date_label.setStyleSheet("font-size: 12px; color: gray;")
self.update_date_label()
date_layout = QHBoxLayout()
date_layout.addStretch()
date_layout.addWidget(self.date_label)
main_layout.addLayout(date_layout)
self.datetime_timer = QTimer()
self.datetime_timer.timeout.connect(self.update_date_label)
self.datetime_timer.start(1000)
# Plot Widget
self.plot_widget = pg.PlotWidget()
main_layout.addWidget(self.plot_widget)
self.plot_widget.setTitle("Pressure")
self.plot_widget.setLabel("left", "Pressure [kPa]")
self.plot_widget.setLabel("bottom", "Time [s]")
self.plot_widget.setBackground("w")
self.plot = self.plot_widget.plot([], [], pen=pg.mkPen(color="k", width=2))
# Setpoint Lines
self.setpoint_start_line = pg.InfiniteLine(pos=self.setpoint_start, angle=0,
pen=pg.mkPen(color="r", width=2, style=pg.QtCore.Qt.PenStyle.DashLine))
self.setpoint_end_line = pg.InfiniteLine(pos=self.setpoint_end, angle=0,
pen=pg.mkPen(color="b", width=2, style=pg.QtCore.Qt.PenStyle.DashLine))
self.plot_widget.addItem(self.setpoint_start_line)
self.plot_widget.addItem(self.setpoint_end_line)
# Cursors
self.cursor_start = pg.InfiniteLine(pos=10, angle=90,
pen=pg.mkPen(color="g", width=2, style=pg.QtCore.Qt.PenStyle.SolidLine),
movable=True)
self.cursor_end = pg.InfiniteLine(pos=910, angle=90,
pen=pg.mkPen(color="g", width=2, style=pg.QtCore.Qt.PenStyle.SolidLine),
movable=True)
self.plot_widget.addItem(self.cursor_start)
self.plot_widget.addItem(self.cursor_end)
self.cursor_start.sigPositionChanged.connect(lambda: self.sync_cursors(self.cursor_start, self.cursor_end))
self.cursor_end.sigPositionChanged.connect(lambda: self.sync_cursors(self.cursor_end, self.cursor_start))
# -------------------------- Controls --------------------------
control_layout = QHBoxLayout()
# Setpoints and dT
self.setpoint_start_input = QLineEdit(str(self.setpoint_start))
self.setpoint_start_input.setFixedWidth(50)
self.setpoint_start_input.editingFinished.connect(self.update_setpoint_start)
self.setpoint_end_input = QLineEdit(str(self.setpoint_end))
self.setpoint_end_input.setFixedWidth(50)
self.setpoint_end_input.editingFinished.connect(self.update_setpoint_end)
self.setwindow_input = QLineEdit(str(self.setwindow))
self.setwindow_input.setFixedWidth(50)
self.setwindow_input.editingFinished.connect(self.update_setwindow)
control_layout.addWidget(QLabel("Setpoint_start [kPa]:"))
control_layout.addWidget(self.setpoint_start_input)
control_layout.addWidget(QLabel("Setpoint_end [kPa]:"))
control_layout.addWidget(self.setpoint_end_input)
control_layout.addWidget(QLabel("dT [sec]:"))
control_layout.addWidget(self.setwindow_input)
# Marker Interval
self.marker_input = QLineEdit(str(self.marker_interval))
self.marker_input.setFixedWidth(50)
self.marker_input.setPlaceholderText("Sample Time [sec]")
self.marker_input.editingFinished.connect(self.update_marker_interval)
control_layout.addWidget(QLabel("Sample Time [sec]:"))
control_layout.addWidget(self.marker_input)
# Save Configuration Button
self.save_config_button = QPushButton("Save Configuration")
self.save_config_button.clicked.connect(self.save_configuration)
control_layout.addWidget(self.save_config_button)
# Analyze Button
self.save_button = QPushButton("Analyze")
self.save_button.clicked.connect(self.analyze_and_save)
control_layout.addWidget(self.save_button)
main_layout.addLayout(control_layout)
# Stack, Cell & Comments text fields
stack_layout = QHBoxLayout()
stack_layout.addWidget(QLabel("Stack No [ST#]:"))
self.stack_input = QLineEdit("0")
self.stack_input.setFixedWidth(80)
stack_layout.addWidget(self.stack_input)
stack_layout.addWidget(QLabel("Cell No [C#]:"))
self.cell_input = QLineEdit("0")
self.cell_input.setFixedWidth(80)
stack_layout.addWidget(self.cell_input)
stack_layout.addWidget(QLabel("Config:"))
self.config_input = QLineEdit("")
self.config_input.setFixedWidth(100)
#self.config_input.setReadOnly(True) # auto-filled from preset
stack_layout.addWidget(self.config_input)
stack_layout.addWidget(QLabel("Comments:"))
self.comments_input = QLineEdit("")
self.comments_input.setFixedWidth(260)
stack_layout.addWidget(self.comments_input)
stack_layout.addStretch()
main_layout.addLayout(stack_layout)
# Preset Buttons
preset_layout = QHBoxLayout()
for name in self.presets:
btn = QPushButton(f"Config {name}")
btn.clicked.connect(lambda checked=False, n=name: self.reset_with_preset(n))
preset_layout.addWidget(btn)
preset_layout.addStretch()
main_layout.addLayout(preset_layout)
# Current Configuration Label
self.current_config_label = QLabel("Current Config: None")
self.current_config_label.setStyleSheet("font-size: 14px; color: blue;")
main_layout.addWidget(self.current_config_label)
# Last Value Display
self.actual_value_label = pg.TextItem("", anchor=(0, 1), color="black", fill=pg.mkBrush(255, 255, 255, 150))
self.plot_widget.addItem(self.actual_value_label)
# Test Result Label
self.result_label = QLabel("Test Result: N/A")
self.result_label.setStyleSheet("font-size: 16px; font-weight: bold; color: black;")
main_layout.addWidget(self.result_label)
# -------------------------- Timers --------------------------
self.marker_timer = QTimer()
self.marker_timer.timeout.connect(self.add_marker)
self.marker_timer.start(int(self.marker_interval * 1000))
self.timer = QTimer()
self.timer.timeout.connect(self.update_plot)
self.timer.start(500)
# -------------------------- Update Methods --------------------------
def update_date_label(self):
self.date_label.setText(time.strftime("%Y-%m-%d %H:%M:%S"))
def moving_average(self, data, window_size=5):
if len(data) < window_size:
return sum(data) / len(data)
return sum(data[-window_size:]) / window_size
def update_setpoint_start(self):
try:
self.setpoint_start = float(self.setpoint_start_input.text())
self.setpoint_start_line.setValue(self.setpoint_start)
except ValueError:
self.setpoint_start_input.setText("")
def update_setpoint_end(self):
try:
self.setpoint_end = float(self.setpoint_end_input.text())
self.setpoint_end_line.setValue(self.setpoint_end)
except ValueError:
self.setpoint_end_input.setText("")
def update_setwindow(self):
try:
self.setwindow = float(self.setwindow_input.text())
self.sync_cursors(self.cursor_start, self.cursor_end)
except ValueError:
self.setwindow_input.setText("")
def update_marker_interval(self):
try:
self.marker_interval = float(self.marker_input.text())
self.marker_timer.setInterval(int(self.marker_interval * 1000))
except ValueError:
self.marker_input.setText("")
# -------------------------- Plot & Cursor Methods --------------------------
def update_plot(self):
if ser and ser.in_waiting > 0:
data = ser.read(2)
if len(data) < 2:
return
try:
raw_pressure = struct.unpack('<H', data)[0]
eng_pressure = raw_pressure * 1.219 - 246.676
elapsed_time = time.time() - self.start_time
self.time_data.append(elapsed_time)
self.pressure_data.append(eng_pressure)
if len(self.time_data) > 100000:
self.time_data.pop(0)
self.pressure_data.pop(0)
self.plot.setData([t for t in self.time_data],
[self.moving_average(self.pressure_data[:i + 1]) for i in range(len(self.pressure_data))])
if self.time_data and self.pressure_data:
self.actual_value_label.setText(f"{eng_pressure:.2f} kPa")
self.actual_value_label.setPos(self.time_data[-1], self.pressure_data[-1])
except ValueError:
pass
def sync_cursors(self, moved_cursor, other_cursor):
new_pos = moved_cursor.value() + self.setwindow if moved_cursor is self.cursor_start else moved_cursor.value() - self.setwindow
other_cursor.blockSignals(True)
other_cursor.setValue(new_pos)
other_cursor.blockSignals(False)
def add_marker(self):
t = time.time() - self.start_time
marker = pg.InfiniteLine(
pos=t,
angle=90,
pen=pg.mkPen("m", width=1, style=pg.QtCore.Qt.PenStyle.DashLine)
)
self.plot_widget.addItem(marker)
self.marker_items.append(marker)
def clear_markers(self):
for marker in self.marker_items:
try:
self.plot_widget.removeItem(marker)
except Exception:
pass
self.marker_items.clear()
def autorange_plot(self):
vb = self.plot_widget.getViewBox()
vb.enableAutoRange(axis=pg.ViewBox.XYAxes, enable=True)
# -------------------------- Preset & Configuration --------------------------
def reset_with_preset(self, preset_name):
self.current_preset = preset_name
self.current_config_label.setText(f"Current Config: {preset_name}")
self.config_input.setText(preset_name)
preset = self.presets[preset_name]
self.setpoint_start_input.setText(str(preset["setpoint_start"]))
self.setpoint_end_input.setText(str(preset["setpoint_end"]))
self.setwindow_input.setText(str(preset["setwindow"]))
self.marker_input.setText(str(preset["marker_interval"]))
self.update_setpoint_start()
self.update_setpoint_end()
self.update_setwindow()
self.update_marker_interval()
self.reset_hmi()
def save_configuration(self):
try:
if self.current_preset is None:
print("No preset selected, nothing saved.")
return
# update preset values
self.presets[self.current_preset]["setpoint_start"] = float(self.setpoint_start_input.text())
self.presets[self.current_preset]["setpoint_end"] = float(self.setpoint_end_input.text())
self.presets[self.current_preset]["setwindow"] = float(self.setwindow_input.text())
self.presets[self.current_preset]["marker_interval"] = float(self.marker_input.text())
# save to JSON file
save_presets(self.presets)
self.current_config_label.setText(f"Current Config: {self.current_preset} (saved)")
print(f"Preset '{self.current_preset}' saved.")
except ValueError:
print("Invalid configuration values (not saved).")
# -------------------------- Reset HMI --------------------------
def reset_hmi(self):
self.start_time = time.time()
self.time_data.clear()
self.pressure_data.clear()
self.plot.clear()
self.cursor_start.setValue(0)
self.cursor_end.setValue(self.setwindow)
self.result_label.setText("Test Result: N/A")
self.result_label.setStyleSheet("color: black;")
self.autorange_plot()
self.clear_markers()
# -------------------------- Analyze & Save --------------------------
def analyze_and_save(self):
try:
start_time = self.cursor_start.value()
end_time = self.cursor_end.value()
if start_time >= end_time:
self.result_label.setText("Invalid dT")
return
# Sample marker data
marker_times = []
t = start_time
while t <= end_time:
marker_times.append(t)
t += self.marker_interval
sampled_times, sampled_pressures = [], []
for mt in marker_times:
if not self.time_data:
continue
idx = min(range(len(self.time_data)), key=lambda i: abs(self.time_data[i] - mt))
sampled_times.append(self.time_data[idx])
sampled_pressures.append(self.pressure_data[idx])
if not sampled_pressures:
self.result_label.setText("No data at marker points")
return
start_pressure = sampled_pressures[0]
end_pressure = sampled_pressures[-1]
pressure_drop = start_pressure - end_pressure
time_interval = end_time - start_time
drop_rate = pressure_drop / time_interval
threshold = (self.setpoint_start - self.setpoint_end) / time_interval
test_result = "PASS" if drop_rate <= threshold else "FAIL"
self.result_label.setText(f"Test Result: {test_result} (Drop Rate: {drop_rate:.2f} kPa/s)")
self.result_label.setStyleSheet("color: green;" if test_result == "PASS" else "color: red;")
stack = self.stack_input.text().strip()
cell = self.cell_input.text().strip()
config = self.config_input.text().strip() or "NA"
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
summary_df = pd.DataFrame([[
timestamp,
round(start_pressure, 1),
round(end_pressure, 1),
round(drop_rate, 3),
test_result,
stack,
cell,
config,
self.comments_input.text()
]], columns=[
"Time",
"Start Pressure",
"End Pressure",
"Drop Rate (kPa/s)",
"Result",
"Stack",
"Cell",
"Config",
"Comments"
])
summary_path = os.path.join(self.data_dir, "leak_test_summary.csv")
summary_df.to_csv(summary_path, mode="a", header=not os.path.exists(summary_path), index=False)
detailed_df = pd.DataFrame({"Time (s)": [round(t, 1) for t in sampled_times],
"Pressure (kPa)": [round(p, 1) for p in sampled_pressures]})
detailed_filename = (
f"leak_test_data_"
f"ST{self.stack_input.text()}_"
f"CELL{self.cell_input.text()}_"
f"CFG{self.current_preset}_"
f"{timestamp.replace(':', '-').replace(' ', '_')}.csv"
)
detailed_path = os.path.join(self.data_dir, detailed_filename)
detailed_df.to_csv(detailed_path, index=False)
screenshot = self.grab()
sc_filename = f"screenshot_{os.path.splitext(detailed_filename)[0]}.png"
sc_path = os.path.join(self.data_dir, sc_filename)
screenshot.save(sc_path)
print(f"Screenshot saved to {sc_filename}")
print(f"Saved test summary and sampled data.")
except ValueError:
self.result_label.setText("Invalid Criteria Input")
# -------------------------- Close Event --------------------------
def closeEvent(self, event):
if ser and ser.is_open:
ser.close()
print("Serial connection closed.")
self.datetime_timer.stop()
self.timer.stop()
self.marker_timer.stop()
print("Application closed.")
event.accept()
# -------------------------- Run Application --------------------------
if __name__ == "__main__":
app = QApplication(sys.argv)
window = HMI()
window.show()
sys.exit(app.exec())