titration start

This commit is contained in:
jens 2025-07-13 18:17:17 +02:00
parent 918e102a0c
commit 16b0273ac8

View File

@ -11,6 +11,7 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure from matplotlib.figure import Figure
import datetime import datetime
from collections import deque from collections import deque
import numpy as np
class PHControllerGUI(QMainWindow): class PHControllerGUI(QMainWindow):
def __init__(self): def __init__(self):
@ -165,6 +166,47 @@ class PHControllerGUI(QMainWindow):
main_layout.addWidget(plot_group) 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 # Status Bar
self.status_bar = QStatusBar() self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar) self.setStatusBar(self.status_bar)
@ -200,6 +242,17 @@ class PHControllerGUI(QMainWindow):
self.auto_dose_timer = QTimer() self.auto_dose_timer = QTimer()
self.auto_dose_timer.timeout.connect(self.check_auto_dose_progress) 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 # Enable/disable controls based on connection
self.set_controls_enabled(False) self.set_controls_enabled(False)
@ -266,6 +319,10 @@ class PHControllerGUI(QMainWindow):
self.auto_dose_timer.stop() self.auto_dose_timer.stop()
self.auto_dosing = False self.auto_dosing = False
# Stop titration timer
self.titration_timer.stop()
self.auto_titration_active = False
self.connect_btn.setText("Verbinden") self.connect_btn.setText("Verbinden")
self.status_bar.showMessage("Getrennt") self.status_bar.showMessage("Getrennt")
self.set_controls_enabled(False) self.set_controls_enabled(False)
@ -278,6 +335,8 @@ class PHControllerGUI(QMainWindow):
self.last_flow_rate = 0.0 self.last_flow_rate = 0.0
self.auto_dosing = False self.auto_dosing = False
self.auto_dose_timer.stop() self.auto_dose_timer.stop()
self.auto_titration_active = False
self.titration_timer.stop()
def set_controls_enabled(self, enabled): def set_controls_enabled(self, enabled):
self.pump_on_btn.setEnabled(enabled) self.pump_on_btn.setEnabled(enabled)
@ -291,6 +350,10 @@ class PHControllerGUI(QMainWindow):
self.reset_volume_checkbox.setEnabled(enabled) self.reset_volume_checkbox.setEnabled(enabled)
self.clear_plot_btn.setEnabled(enabled) self.clear_plot_btn.setEnabled(enabled)
self.plot_enabled_checkbox.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): def send_command(self, command):
print(command) print(command)
@ -339,8 +402,8 @@ class PHControllerGUI(QMainWindow):
self.total_volume_label.setVisible(True) self.total_volume_label.setVisible(True)
# Check if volume should be reset or continue accumulating # Check if volume should be reset or continue accumulating
if self.reset_volume_checkbox.isChecked() or self.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) # Reset volume counter (always reset for auto dosing, but NOT for titration)
self.total_volume = 0.0 self.total_volume = 0.0
self.total_volume_label.setText("Gesamt: 0.0 ml") self.total_volume_label.setText("Gesamt: 0.0 ml")
else: else:
@ -399,6 +462,11 @@ class PHControllerGUI(QMainWindow):
self.pump_off_btn.setEnabled(True) self.pump_off_btn.setEnabled(True)
return 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() volume = self.volume_spin.value()
self.target_volume = volume self.target_volume = volume
self.auto_dosing = True self.auto_dosing = True
@ -419,39 +487,6 @@ class PHControllerGUI(QMainWindow):
self.status_bar.showMessage(f"Automatische Dosierung läuft... Ziel: {volume} ml") 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): def read_serial_data(self):
if not self.serial_connection or not self.serial_connection.is_open: if not self.serial_connection or not self.serial_connection.is_open:
return return
@ -469,6 +504,10 @@ class PHControllerGUI(QMainWindow):
# Add to plot if recording is enabled # Add to plot if recording is enabled
if self.plot_enabled_checkbox.isChecked(): if self.plot_enabled_checkbox.isChecked():
self.ph_plot.add_ph_value(ph_value) 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: except ValueError:
pass pass
elif data.startswith('P'): # Pump status elif data.startswith('P'): # Pump status
@ -522,6 +561,7 @@ class PHControllerGUI(QMainWindow):
self.tube_request_timer.stop() self.tube_request_timer.stop()
self.flow_rate_timer.stop() self.flow_rate_timer.stop()
self.auto_dose_timer.stop() self.auto_dose_timer.stop()
self.titration_timer.stop()
self.close_connection() self.close_connection()
event.accept() event.accept()
@ -530,6 +570,148 @@ class PHControllerGUI(QMainWindow):
self.ph_plot.clear_data() self.ph_plot.clear_data()
self.status_bar.showMessage("pH-Diagramm gelöscht") 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): class PHPlotWidget(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -638,6 +820,90 @@ class PHPlotWidget(QWidget):
self.ax.xaxis.set_major_formatter(ticker.ScalarFormatter()) self.ax.xaxis.set_major_formatter(ticker.ScalarFormatter())
self.canvas.draw() 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(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = PHControllerGUI() window = PHControllerGUI()