diff --git a/gui/main.py b/gui/main.py index f98f4cb..b741194 100644 --- a/gui/main.py +++ b/gui/main.py @@ -6,6 +6,11 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout QGroupBox, QStatusBar, QMessageBox, QCheckBox) from PyQt6.QtCore import QTimer, Qt from PyQt6.QtGui import QFont +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.figure import Figure +import datetime +from collections import deque class PHControllerGUI(QMainWindow): def __init__(self): @@ -13,7 +18,7 @@ class PHControllerGUI(QMainWindow): self.serial_connection = None self.updating_tube_combo = False # Flag to prevent recursive tube commands self.setWindowTitle("Arduino pH Controller") - self.setGeometry(100, 100, 600, 400) + self.setGeometry(100, 100, 900, 800) # Larger window for plot # Central Widget central_widget = QWidget() @@ -136,6 +141,30 @@ class PHControllerGUI(QMainWindow): main_layout.addWidget(status_group) main_layout.addWidget(control_group) + # pH Plot Group + plot_group = QGroupBox("pH-Verlauf") + plot_layout = QVBoxLayout() + + # Plot controls + plot_controls_layout = QHBoxLayout() + self.clear_plot_btn = QPushButton("Diagramm löschen") + self.clear_plot_btn.clicked.connect(self.clear_ph_plot) + self.plot_enabled_checkbox = QCheckBox("pH-Aufzeichnung") + self.plot_enabled_checkbox.setChecked(True) + + plot_controls_layout.addWidget(self.plot_enabled_checkbox) + plot_controls_layout.addWidget(self.clear_plot_btn) + plot_controls_layout.addStretch() + + # pH Plot Widget + self.ph_plot = PHPlotWidget() + + plot_layout.addLayout(plot_controls_layout) + plot_layout.addWidget(self.ph_plot) + plot_group.setLayout(plot_layout) + + main_layout.addWidget(plot_group) + # Status Bar self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) @@ -260,6 +289,8 @@ class PHControllerGUI(QMainWindow): self.auto_dose_btn.setEnabled(enabled) self.volume_spin.setEnabled(enabled) self.reset_volume_checkbox.setEnabled(enabled) + self.clear_plot_btn.setEnabled(enabled) + self.plot_enabled_checkbox.setEnabled(enabled) def send_command(self, command): print(command) @@ -432,6 +463,9 @@ class PHControllerGUI(QMainWindow): try: ph_value = float(data[1:]) self.ph_label.setText(f"pH: {ph_value:.2f}") + # Add to plot if recording is enabled + if self.plot_enabled_checkbox.isChecked(): + self.ph_plot.add_ph_value(ph_value) except ValueError: pass elif data.startswith('P'): # Pump status @@ -488,6 +522,119 @@ class PHControllerGUI(QMainWindow): self.close_connection() event.accept() + def clear_ph_plot(self): + """Clear the pH plot data""" + self.ph_plot.clear_data() + self.status_bar.showMessage("pH-Diagramm gelöscht") + +class PHPlotWidget(QWidget): + def __init__(self): + super().__init__() + self.figure = Figure(figsize=(8, 4)) + self.canvas = FigureCanvas(self.figure) + self.ax = self.figure.add_subplot(111) + + # Data storage (keep last 300 points = 10 minutes at 2-second intervals) + self.times = deque(maxlen=300) + self.ph_values = deque(maxlen=300) + self.start_time = None # Track when recording started + + # Setup plot + self.ax.set_xlabel('Zeit') + self.ax.set_ylabel('pH-Wert') + self.ax.set_title('pH-Verlauf') + self.ax.grid(True, alpha=0.3) + self.ax.set_ylim(0, 14) # pH range 0-14 + + # Layout + layout = QVBoxLayout() + layout.addWidget(self.canvas) + self.setLayout(layout) + + # Plot line + self.line, = self.ax.plot([], [], 'b-', linewidth=2, label='pH-Wert') + self.ax.legend() + + # Initial draw + self.canvas.draw() + + def add_ph_value(self, ph_value): + """Add new pH value with current timestamp""" + current_time = datetime.datetime.now() + + # Set start time on first value + if self.start_time is None: + self.start_time = current_time + + # Calculate relative time in seconds + relative_time = (current_time - self.start_time).total_seconds() + + self.times.append(relative_time) + self.ph_values.append(ph_value) + self.update_plot() + + def update_plot(self): + """Update the plot with current data""" + if len(self.times) > 0 and len(self.ph_values) > 0: + # Update line data + self.line.set_data(self.times, self.ph_values) + + # Auto-scale x-axis to show recent data + if len(self.times) > 1: + self.ax.set_xlim(min(self.times), max(self.times)) + + # Auto-scale y-axis around data with some margin + if len(self.ph_values) > 0: + min_ph = min(self.ph_values) + max_ph = max(self.ph_values) + margin = 0.5 + self.ax.set_ylim(max(0, min_ph - margin), min(14, max_ph + margin)) + + # Format x-axis for time display (seconds/minutes/hours) + self.format_time_axis() + + # Redraw + self.canvas.draw() + + def format_time_axis(self): + """Format the x-axis to show time in appropriate units""" + if len(self.times) == 0: + return + + max_time = max(self.times) + + if max_time < 120: # Less than 2 minutes - show seconds + self.ax.set_xlabel('Zeit (Sekunden)') + # No need to change tick formatting for seconds + elif max_time < 7200: # Less than 2 hours - show minutes + self.ax.set_xlabel('Zeit (Minuten)') + # Convert tick labels to minutes + import matplotlib.ticker as ticker + def format_minutes(x, pos): + return f'{x/60:.1f}' + self.ax.xaxis.set_major_formatter(ticker.FuncFormatter(format_minutes)) + else: # Show hours + self.ax.set_xlabel('Zeit (Stunden)') + # Convert tick labels to hours + import matplotlib.ticker as ticker + def format_hours(x, pos): + return f'{x/3600:.1f}' + self.ax.xaxis.set_major_formatter(ticker.FuncFormatter(format_hours)) + + def clear_data(self): + """Clear all data points""" + self.times.clear() + self.ph_values.clear() + self.start_time = None # Reset start time + self.line.set_data([], []) + self.ax.set_xlim(0, 1) + self.ax.set_ylim(0, 14) + self.ax.set_xlabel('Zeit') # Reset to default label + # Reset tick formatter to default + import matplotlib.ticker as ticker + self.ax.xaxis.set_major_formatter(ticker.ScalarFormatter()) + self.canvas.draw() + def main(): app = QApplication(sys.argv) window = PHControllerGUI() diff --git a/requirements.txt b/requirements.txt index e0d5c5c..7030bdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ -PyQt6==6.9.0 -pyserial==3.5 \ No newline at end of file +PyQt6==6.7.0 +pyserial==3.5 +matplotlib==3.7.2 +numpy<2.0.0 \ No newline at end of file