From 16b0273ac83ef568440a0abd223438b6145eed23 Mon Sep 17 00:00:00 2001 From: jens Date: Sun, 13 Jul 2025 18:17:17 +0200 Subject: [PATCH] titration start --- gui/main.py | 336 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 301 insertions(+), 35 deletions(-) diff --git a/gui/main.py b/gui/main.py index 44fb45c..44d582f 100644 --- a/gui/main.py +++ b/gui/main.py @@ -11,6 +11,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import datetime from collections import deque +import numpy as np class PHControllerGUI(QMainWindow): def __init__(self): @@ -165,6 +166,47 @@ class PHControllerGUI(QMainWindow): main_layout.addWidget(plot_group) + # Titration Plot Group + titration_group = QGroupBox("Titration") + titration_layout = QVBoxLayout() + + # Titration controls + titration_controls_layout = QHBoxLayout() + + # Titration parameters + self.titration_step_spin = QDoubleSpinBox() + self.titration_step_spin.setRange(0.1, 10.0) + self.titration_step_spin.setValue(0.5) + self.titration_step_spin.setSuffix(" ml") + self.titration_step_spin.setDecimals(1) + + self.titration_wait_spin = QDoubleSpinBox() + self.titration_wait_spin.setRange(5.0, 60.0) + self.titration_wait_spin.setValue(10.0) + self.titration_wait_spin.setSuffix(" s") + self.titration_wait_spin.setDecimals(1) + + self.start_titration_btn = QPushButton("Automatische Titration starten") + self.start_titration_btn.clicked.connect(self.toggle_auto_titration) + self.clear_titration_btn = QPushButton("Titrationsdaten löschen") + self.clear_titration_btn.clicked.connect(self.clear_titration_plot) + + titration_controls_layout.addWidget(QLabel("Schritt:")) + titration_controls_layout.addWidget(self.titration_step_spin) + titration_controls_layout.addWidget(QLabel("Warten:")) + titration_controls_layout.addWidget(self.titration_wait_spin) + titration_controls_layout.addWidget(self.start_titration_btn) + titration_controls_layout.addWidget(self.clear_titration_btn) + + # Titration Plot Widget + self.titration_plot = TitrationPlotWidget() + + titration_layout.addLayout(titration_controls_layout) + titration_layout.addWidget(self.titration_plot) + titration_group.setLayout(titration_layout) + + main_layout.addWidget(titration_group) + # Status Bar self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) @@ -200,6 +242,17 @@ class PHControllerGUI(QMainWindow): self.auto_dose_timer = QTimer() self.auto_dose_timer.timeout.connect(self.check_auto_dose_progress) + # Titration tracking + self.titration_active = False + self.titration_start_volume = 0.0 + self.auto_titration_active = False + self.titration_step_size = 0.5 # ml per step + self.titration_wait_time = 10.0 # seconds to wait between steps + self.titration_timer = QTimer() + self.titration_timer.timeout.connect(self.titration_step) + self.titration_current_step = 0 + self.titration_target_volume = 0.0 + # Enable/disable controls based on connection self.set_controls_enabled(False) @@ -266,6 +319,10 @@ class PHControllerGUI(QMainWindow): self.auto_dose_timer.stop() self.auto_dosing = False + # Stop titration timer + self.titration_timer.stop() + self.auto_titration_active = False + self.connect_btn.setText("Verbinden") self.status_bar.showMessage("Getrennt") self.set_controls_enabled(False) @@ -278,6 +335,8 @@ class PHControllerGUI(QMainWindow): self.last_flow_rate = 0.0 self.auto_dosing = False self.auto_dose_timer.stop() + self.auto_titration_active = False + self.titration_timer.stop() def set_controls_enabled(self, enabled): self.pump_on_btn.setEnabled(enabled) @@ -291,6 +350,10 @@ class PHControllerGUI(QMainWindow): self.reset_volume_checkbox.setEnabled(enabled) self.clear_plot_btn.setEnabled(enabled) self.plot_enabled_checkbox.setEnabled(enabled) + self.start_titration_btn.setEnabled(enabled) + self.clear_titration_btn.setEnabled(enabled) + self.titration_step_spin.setEnabled(enabled) + self.titration_wait_spin.setEnabled(enabled) def send_command(self, command): print(command) @@ -339,8 +402,8 @@ class PHControllerGUI(QMainWindow): self.total_volume_label.setVisible(True) # Check if volume should be reset or continue accumulating - if self.reset_volume_checkbox.isChecked() or self.auto_dosing: - # Reset volume counter (always reset for auto dosing) + if (self.reset_volume_checkbox.isChecked() or self.auto_dosing) and not self.auto_titration_active: + # Reset volume counter (always reset for auto dosing, but NOT for titration) self.total_volume = 0.0 self.total_volume_label.setText("Gesamt: 0.0 ml") else: @@ -399,6 +462,11 @@ class PHControllerGUI(QMainWindow): self.pump_off_btn.setEnabled(True) return + # Don't start if auto titration is running + if self.auto_titration_active: + QMessageBox.warning(self, "Warnung", "Automatische Titration läuft bereits!") + return + volume = self.volume_spin.value() self.target_volume = volume self.auto_dosing = True @@ -419,39 +487,6 @@ class PHControllerGUI(QMainWindow): self.status_bar.showMessage(f"Automatische Dosierung läuft... Ziel: {volume} ml") - def check_auto_dose_progress(self): - """Check if target volume has been reached during auto dosing""" - if not self.auto_dosing: - return - - # Check if we've reached or exceeded the target volume - if self.total_volume >= self.target_volume: - # Stop dosing - self.auto_dosing = False - self.auto_dose_timer.stop() - self.send_pump_command(0) # Turn off pump - - # Re-enable manual controls - self.pump_on_btn.setEnabled(True) - self.pump_off_btn.setEnabled(True) - self.auto_dose_btn.setText("Automatisch dosieren") - - # Show completion message - actual_volume = self.total_volume - difference = actual_volume - self.target_volume - - message = f"Dosierung abgeschlossen!\n" - message += f"Zielvolumen: {self.target_volume:.2f} ml\n" - message += f"Tatsächliches Volumen: {actual_volume:.2f} ml\n" - message += f"Abweichung: {difference:+.2f} ml" - - QMessageBox.information(self, "Automatische Dosierung", message) - self.status_bar.showMessage(f"Dosierung abgeschlossen: {actual_volume:.2f} ml von {self.target_volume:.2f} ml") - else: - # Update progress in status bar - progress = (self.total_volume / self.target_volume) * 100 - self.status_bar.showMessage(f"Dosierung: {self.total_volume:.2f}/{self.target_volume:.2f} ml ({progress:.1f}%)") - def read_serial_data(self): if not self.serial_connection or not self.serial_connection.is_open: return @@ -469,6 +504,10 @@ class PHControllerGUI(QMainWindow): # Add to plot if recording is enabled if self.plot_enabled_checkbox.isChecked(): self.ph_plot.add_ph_value(ph_value) + # Add to titration plot if titration is active + if self.titration_active or self.auto_titration_active: + titrated_volume = self.total_volume - self.titration_start_volume + self.titration_plot.add_titration_point(titrated_volume, ph_value) except ValueError: pass elif data.startswith('P'): # Pump status @@ -522,6 +561,7 @@ class PHControllerGUI(QMainWindow): self.tube_request_timer.stop() self.flow_rate_timer.stop() self.auto_dose_timer.stop() + self.titration_timer.stop() self.close_connection() event.accept() @@ -530,6 +570,148 @@ class PHControllerGUI(QMainWindow): self.ph_plot.clear_data() self.status_bar.showMessage("pH-Diagramm gelöscht") + def toggle_titration(self): + """Start or stop titration recording""" + if self.titration_active: + # Stop titration + self.titration_active = False + self.start_titration_btn.setText("Titration starten") + self.status_bar.showMessage("Titration gestoppt") + else: + # Start titration + self.titration_active = True + self.titration_start_volume = self.total_volume + self.start_titration_btn.setText("Titration stoppen") + self.status_bar.showMessage("Titration gestartet") + + def toggle_auto_titration(self): + """Start or stop automatic titration""" + if self.auto_titration_active: + # Stop auto titration + self.auto_titration_active = False + self.titration_timer.stop() + self.send_pump_command(0) # Turn off pump + self.start_titration_btn.setText("Automatische Titration starten") + # Re-enable controls + self.pump_on_btn.setEnabled(True) + self.pump_off_btn.setEnabled(True) + self.auto_dose_btn.setEnabled(True) + self.status_bar.showMessage("Automatische Titration gestoppt") + return + + # Don't start if auto dosing is running + if self.auto_dosing: + QMessageBox.warning(self, "Warnung", "Automatische Dosierung läuft bereits!") + return + + # Get parameters + self.titration_step_size = self.titration_step_spin.value() + self.titration_wait_time = self.titration_wait_spin.value() + + # Start auto titration + self.auto_titration_active = True + self.titration_start_volume = self.total_volume # Remember starting volume + self.titration_current_step = 0 + + # Update button and disable controls + self.start_titration_btn.setText("Automatische Titration stoppen") + self.pump_on_btn.setEnabled(False) + self.pump_off_btn.setEnabled(False) + self.auto_dose_btn.setEnabled(False) + + # Start first titration step + self.titration_step() + + self.status_bar.showMessage(f"Automatische Titration gestartet - Schritt: {self.titration_step_size} ml, Warten: {self.titration_wait_time} s") + + def titration_step(self): + """Perform one titration step""" + if not self.auto_titration_active: + return + + self.titration_current_step += 1 + + # Calculate step target volume (only the additional volume for this step) + step_target_volume = self.titration_step_size + + # Calculate absolute target volume (total from start of titration) + absolute_target_volume = self.titration_start_volume + (self.titration_current_step * self.titration_step_size) + + # Start pumping for this step + self.auto_dosing = True # Use auto dosing mechanism + self.target_volume = absolute_target_volume # Set absolute target + self.send_pump_command(1) + self.auto_dose_timer.start(500) + + current_titrated_volume = self.total_volume - self.titration_start_volume + total_target_titrated = self.titration_current_step * self.titration_step_size + + self.status_bar.showMessage(f"Titration Schritt {self.titration_current_step}: Dosiere {step_target_volume} ml (Titration gesamt: {total_target_titrated} ml)") + + def check_auto_dose_progress(self): + """Check if target volume has been reached during auto dosing""" + if not self.auto_dosing: + return + + # Check if we've reached or exceeded the target volume + if self.total_volume >= self.target_volume: + # Stop dosing + self.auto_dosing = False + self.auto_dose_timer.stop() + self.send_pump_command(0) # Turn off pump + + # If this was part of auto titration, schedule next step + if self.auto_titration_active: + # Wait for pH to stabilize, then continue with next step + wait_time_ms = int(self.titration_wait_time * 1000) + QTimer.singleShot(wait_time_ms, self.titration_step) + self.status_bar.showMessage(f"Warte {self.titration_wait_time} s auf pH-Stabilisierung...") + else: + # Re-enable manual controls for regular auto dosing + self.pump_on_btn.setEnabled(True) + self.pump_off_btn.setEnabled(True) + self.auto_dose_btn.setText("Automatisch dosieren") + + # Show completion message + actual_volume = self.total_volume + difference = actual_volume - self.target_volume + + message = f"Dosierung abgeschlossen!\n" + message += f"Zielvolumen: {self.target_volume:.2f} ml\n" + message += f"Tatsächliches Volumen: {actual_volume:.2f} ml\n" + message += f"Abweichung: {difference:+.2f} ml" + + QMessageBox.information(self, "Automatische Dosierung", message) + self.status_bar.showMessage(f"Dosierung abgeschlossen: {actual_volume:.2f} ml von {self.target_volume:.2f} ml") + else: + # Update progress in status bar + progress = (self.total_volume / self.target_volume) * 100 + if self.auto_titration_active: + current_titrated_volume = self.total_volume - self.titration_start_volume + target_titrated_volume = self.titration_current_step * self.titration_step_size + self.status_bar.showMessage(f"Titration Schritt {self.titration_current_step}: Gesamt {current_titrated_volume:.2f}/{target_titrated_volume:.2f} ml titriert ({progress:.1f}%)") + else: + self.status_bar.showMessage(f"Dosierung: {self.total_volume:.2f}/{self.target_volume:.2f} ml ({progress:.1f}%)") + + def clear_titration_plot(self): + """Clear the titration plot data""" + try: + # Clear the plot data + self.titration_plot.clear_data() + + # Reset titration state variables + self.titration_active = False + self.titration_start_volume = 0.0 + + # Update button text if not in auto titration mode + if not self.auto_titration_active: + self.start_titration_btn.setText("Automatische Titration starten") + + self.status_bar.showMessage("Titrationsdaten gelöscht") + except Exception as e: + self.status_bar.showMessage(f"Fehler beim Löschen der Titrationsdaten: {str(e)}") + QMessageBox.critical(self, "Fehler", f"Fehler beim Löschen der Titrationsdaten:\n{str(e)}") + class PHPlotWidget(QWidget): def __init__(self): super().__init__() @@ -638,6 +820,90 @@ class PHPlotWidget(QWidget): self.ax.xaxis.set_major_formatter(ticker.ScalarFormatter()) self.canvas.draw() +class TitrationPlotWidget(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 + self.volumes = [] + self.ph_values = [] + + # Setup plot + self.ax.set_xlabel('Dosierte Menge (ml)') + self.ax.set_ylabel('pH-Wert') + self.ax.set_title('Titrationskurve') + self.ax.grid(True, alpha=0.3) + self.ax.set_ylim(0, 14) # pH range 0-14 + self.ax.set_xlim(0, 1) # Initial volume range + + # Layout + layout = QVBoxLayout() + layout.addWidget(self.canvas) + self.setLayout(layout) + + # Plot line and points + self.line, = self.ax.plot([], [], 'r-', linewidth=2, label='pH-Verlauf') + self.points = self.ax.scatter([], [], c='red', s=30, zorder=5, label='Messpunkte') + self.ax.legend() + + # Initial draw + self.canvas.draw() + + def add_titration_point(self, volume, ph_value): + """Add new titration point (volume vs pH)""" + self.volumes.append(volume) + self.ph_values.append(ph_value) + self.update_plot() + + def update_plot(self): + """Update the plot with current data""" + if len(self.volumes) > 0 and len(self.ph_values) > 0: + # Update line data + self.line.set_data(self.volumes, self.ph_values) + + # Update scatter points + self.points.set_offsets(list(zip(self.volumes, self.ph_values))) + + # Auto-scale x-axis + if len(self.volumes) > 1: + max_vol = max(self.volumes) + self.ax.set_xlim(0, max_vol * 1.1) # Add 10% margin + + # 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)) + + # Redraw + self.canvas.draw() + + def clear_data(self): + """Clear all titration data points""" + try: + # Clear data lists + self.volumes.clear() + self.ph_values.clear() + + # Reset plot elements safely + self.line.set_data([], []) + + # Clear scatter points properly - use empty 2D array + self.points.set_offsets(np.empty((0, 2))) + + # Reset axis limits + self.ax.set_xlim(0, 1) + self.ax.set_ylim(0, 14) + + # Redraw canvas + self.canvas.draw() + except Exception as e: + print(f"Fehler beim Löschen der Titrationsdaten: {str(e)}") + def main(): app = QApplication(sys.argv) window = PHControllerGUI()