From 2103601efa79f83a301be1a14a2b940c1312ad89 Mon Sep 17 00:00:00 2001 From: jens Date: Sun, 13 Jul 2025 21:01:19 +0200 Subject: [PATCH] next step --- gui/main.py | 132 ++++++++++++++++++++++++++++++++++--- pH_Filter_Dokumentation.md | 88 +++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 9 deletions(-) create mode 100644 pH_Filter_Dokumentation.md diff --git a/gui/main.py b/gui/main.py index 44d582f..ce05541 100644 --- a/gui/main.py +++ b/gui/main.py @@ -155,6 +155,14 @@ class PHControllerGUI(QMainWindow): plot_controls_layout.addWidget(self.plot_enabled_checkbox) plot_controls_layout.addWidget(self.clear_plot_btn) + + # pH Filter Control + self.ph_filter_checkbox = QCheckBox("pH-Filter (Motorstörungen)") + self.ph_filter_checkbox.setChecked(True) + self.ph_filter_checkbox.setToolTip("Filtert pH-Sprünge beim Motor Ein-/Ausschalten") + self.ph_filter_checkbox.toggled.connect(self.toggle_ph_filter) + plot_controls_layout.addWidget(self.ph_filter_checkbox) + plot_controls_layout.addStretch() # pH Plot Widget @@ -236,6 +244,14 @@ class PHControllerGUI(QMainWindow): self.total_volume = 0.0 # Total volume pumped in ml self.last_flow_rate = 0.0 # Last known flow rate + # pH filtering for motor interference + self.ph_history = deque(maxlen=5) # Store last 5 pH values + self.motor_state_changed_time = None # Track when motor state changed + self.motor_interference_duration = 3.0 # seconds to filter after motor state change + self.ph_filter_enabled = True # Enable/disable pH filtering + self.outlier_threshold = 2.0 # pH units - values changing more than this are considered outliers + self.interference_threshold = 1.0 # pH units - stricter threshold during motor interference + # Auto dosing self.auto_dosing = False self.target_volume = 0.0 @@ -354,6 +370,7 @@ class PHControllerGUI(QMainWindow): self.clear_titration_btn.setEnabled(enabled) self.titration_step_spin.setEnabled(enabled) self.titration_wait_spin.setEnabled(enabled) + self.ph_filter_checkbox.setEnabled(enabled) def send_command(self, command): print(command) @@ -394,6 +411,9 @@ class PHControllerGUI(QMainWindow): self.last_flow_rate = flow_rate def send_pump_command(self, state): + # Record motor state change time for interference filtering + self.motor_state_changed_time = datetime.datetime.now() + if state == 1: self.send_command("<11>") # Pump on self.pump_status.setText("Pumpe: Ein") @@ -487,6 +507,86 @@ class PHControllerGUI(QMainWindow): self.status_bar.showMessage(f"Automatische Dosierung läuft... Ziel: {volume} ml") + def toggle_ph_filter(self, enabled): + """Enable or disable pH filtering""" + self.ph_filter_enabled = enabled + if enabled: + self.status_bar.showMessage("pH-Filter aktiviert - Motorstörungen werden gefiltert") + else: + self.status_bar.showMessage("pH-Filter deaktiviert - Rohe pH-Werte werden angezeigt") + # Clear history when disabling filter + self.ph_history.clear() + + def configure_ph_filter(self, interference_duration=3.0, outlier_threshold=2.0, interference_threshold=1.0): + """ + Configure pH filter parameters + + Args: + interference_duration: Seconds to apply enhanced filtering after motor state change + outlier_threshold: pH units - normal outlier detection threshold + interference_threshold: pH units - stricter threshold during motor interference + """ + self.motor_interference_duration = interference_duration + self.outlier_threshold = outlier_threshold + self.interference_threshold = interference_threshold + + if self.ph_filter_enabled: + self.status_bar.showMessage(f"pH-Filter konfiguriert: Störzeit={interference_duration}s, " + f"Normal={outlier_threshold}pH, Störung={interference_threshold}pH") + + def filter_ph_value(self, ph_value): + """ + Filter pH values to reduce motor interference and outliers + Returns the filtered pH value or None if value should be ignored + """ + # If filtering is disabled, return raw value + if not self.ph_filter_enabled: + return ph_value + + current_time = datetime.datetime.now() + + # Add to history + self.ph_history.append(ph_value) + + # If motor state changed recently, apply enhanced filtering + if (self.motor_state_changed_time and + (current_time - self.motor_state_changed_time).total_seconds() < self.motor_interference_duration): + + # During motor interference period, use more aggressive filtering + if len(self.ph_history) >= 3: + # Use median filter to remove spikes + recent_values = list(self.ph_history)[-3:] + filtered_value = sorted(recent_values)[1] # Median of last 3 values + + # Additional outlier detection during interference period + if len(self.ph_history) >= 5: + older_values = list(self.ph_history)[:-1] # All except current + avg_older = sum(older_values) / len(older_values) + + # If current value deviates more than threshold from recent average + if abs(ph_value - avg_older) > self.interference_threshold: + # Use the median instead of the raw value + return filtered_value + + return filtered_value + else: + # Not enough history during interference, return current value + return ph_value + + # Normal operation - light filtering + if len(self.ph_history) >= 2: + # Simple outlier detection + previous_value = self.ph_history[-2] + + # If change is more than threshold, it's likely an outlier + if abs(ph_value - previous_value) > self.outlier_threshold: + # Use moving average of last few values instead + if len(self.ph_history) >= 3: + recent_values = list(self.ph_history)[:-1] # Exclude current outlier + return sum(recent_values) / len(recent_values) + + return ph_value + def read_serial_data(self): if not self.serial_connection or not self.serial_connection.is_open: return @@ -499,15 +599,29 @@ class PHControllerGUI(QMainWindow): data = line[1:-1] if data.startswith('#'): # pH value 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) - # 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) + raw_ph_value = float(data[1:]) + + # Apply filtering to reduce motor interference + filtered_ph_value = self.filter_ph_value(raw_ph_value) + + if filtered_ph_value is not None: + # Update display with filtered value + self.ph_label.setText(f"pH: {filtered_ph_value:.2f}") + + # Show filtering status if motor interference is active + if (self.motor_state_changed_time and + (datetime.datetime.now() - self.motor_state_changed_time).total_seconds() < self.motor_interference_duration): + if abs(raw_ph_value - filtered_ph_value) > 0.1: + self.status_bar.showMessage(f"pH gefiltert: {raw_ph_value:.2f} → {filtered_ph_value:.2f} (Motorstörung)") + + # Add to plot if recording is enabled + if self.plot_enabled_checkbox.isChecked(): + self.ph_plot.add_ph_value(filtered_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, filtered_ph_value) except ValueError: pass elif data.startswith('P'): # Pump status diff --git a/pH_Filter_Dokumentation.md b/pH_Filter_Dokumentation.md new file mode 100644 index 0000000..9797114 --- /dev/null +++ b/pH_Filter_Dokumentation.md @@ -0,0 +1,88 @@ +# pH-Filter für Motorstörungen - Dokumentation + +## Problem +Beim Ein- und Ausschalten des Pumpmotors können kurzzeitige starke pH-Sprünge auftreten. Diese sind meist durch elektromagnetische Störungen (EMI) oder Spannungsspitzen verursacht und verfälschen die Messwerte. + +## Lösung +Die Software wurde um einen intelligenten pH-Filter erweitert, der solche Störungen automatisch erkennt und filtert. + +## Features + +### 1. Automatische Störungserkennung +- Der Filter erkennt automatisch, wann der Motor ein- oder ausgeschaltet wird +- Für 3 Sekunden nach einer Motorschaltung wird verstärkte Filterung angewendet + +### 2. Mehrstufige Filterung + +#### Normale Betriebszeit: +- **Ausreißer-Erkennung**: pH-Sprünge > 2.0 Einheiten werden als Ausreißer erkannt +- **Gleitender Mittelwert**: Ausreißer werden durch den Durchschnitt der letzten Werte ersetzt + +#### Motor-Störzeit (3 Sekunden nach Schalten): +- **Median-Filter**: Verwendet den Median der letzten 3 Werte +- **Verschärfte Schwelle**: pH-Sprünge > 1.0 Einheiten werden gefiltert +- **Status-Anzeige**: Zeigt in der Statusleiste an, wenn gefiltert wird + +### 3. Benutzersteuerung +- **Ein/Aus-Schalter**: Checkbox "pH-Filter (Motorstörungen)" im Plot-Bereich +- **Echtzeit-Feedback**: Status wird in der Statusleiste angezeigt +- **Transparenz**: Zeigt sowohl rohe als auch gefilterte Werte an + +## Konfiguration + +### Standard-Parameter: +```python +# Störungszeit nach Motorschaltung +motor_interference_duration = 3.0 # Sekunden + +# Normale Ausreißer-Schwelle +outlier_threshold = 2.0 # pH-Einheiten + +# Verschärfte Schwelle während Störzeit +interference_threshold = 1.0 # pH-Einheiten +``` + +### Anpassung der Parameter: +```python +# Beispiel für empfindlichere Filterung +gui.configure_ph_filter( + interference_duration=5.0, # Längere Störzeit + outlier_threshold=1.5, # Empfindlichere normale Filterung + interference_threshold=0.5 # Sehr empfindliche Störzeit-Filterung +) +``` + +## Verwendung + +1. **Aktivierung**: Checkbox "pH-Filter (Motorstörungen)" aktivieren (Standard: Ein) +2. **Betrieb**: Filter arbeitet automatisch im Hintergrund +3. **Überwachung**: Statusleiste zeigt Filteraktivität an +4. **Deaktivierung**: Checkbox deaktivieren für ungefilterte Rohwerte + +## Technische Details + +### Algorithmus: +1. **Wertspeicher**: Letzte 5 pH-Werte werden gespeichert +2. **Zeitüberwachung**: Motor-Schaltzeit wird aufgezeichnet +3. **Dynamische Filterung**: + - Normal: Einfache Ausreißer-Erkennung + - Störzeit: Median-Filter + verschärfte Schwellen + +### Vorteile: +- ✅ Reduziert falsche pH-Sprünge erheblich +- ✅ Erhält echte pH-Änderungen +- ✅ Transparente Anzeige der Filterung +- ✅ Ein-/ausschaltbar je nach Bedarf +- ✅ Konfigurierbare Parameter + +### Anwendungsfälle: +- **Titrationsmessungen**: Vermeidet falsche Äquivalenzpunkte +- **Kontinuierliche Überwachung**: Glättere pH-Kurven +- **Automatische Dosierung**: Zuverlässigere pH-Regelung + +## Beispiel-Ausgabe +``` +Status: pH gefiltert: 8.45 → 7.82 (Motorstörung) +``` + +Dies zeigt, dass ein pH-Sprung von 8.45 auf den gefilterten Wert 7.82 reduziert wurde.