# -*- coding: utf-8 -*- import os import time import csv import threading from datetime import datetime import numpy as np from collections import deque # Warnungen unterdrücken os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' os.environ['LIBUSB_DEBUG'] = '0' from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QLineEdit, QFrame, QCheckBox, QMessageBox, QFileDialog) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject, QThread from PyQt5.QtGui import QColor, QPalette import pysmu import matplotlib matplotlib.use('Qt5Agg') from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure class DeviceDisconnectedError(Exception): pass class MeasurementThread(QThread): update_signal = pyqtSignal(float, float, float) # voltage, current, timestamp error_signal = pyqtSignal(str) status_signal = pyqtSignal(str) # New: for status updates def __init__(self, device, interval=0.1): super().__init__() self.device = device self.interval = max(0.05, interval) # Ensure minimum interval self._running = False self._lock = threading.Lock() # Thread safety self.filter_window_size = 10 self.start_time = time.time() self.last_update_time = self.start_time def run(self): """Main measurement loop with enhanced error handling""" self._running = True voltage_window = deque(maxlen=self.filter_window_size) current_window = deque(maxlen=self.filter_window_size) self.status_signal.emit("Measurement started") while self._running: try: # 1. Read samples with timeout samples = self.device.read( self.filter_window_size, timeout=500, ) if not samples: raise DeviceDisconnectedError("No samples received - device may be disconnected") # 2. Process samples (thread-safe) with self._lock: raw_voltage = np.mean([s[1][0] for s in samples]) raw_current = np.mean([s[0][1] for s in samples]) voltage_window.append(raw_voltage) current_window.append(raw_current) voltage = np.mean(voltage_window) current = np.mean(current_window) current_time = time.time() - self.start_time # 3. Emit updates self.update_signal.emit(voltage, current, current_time) self.last_update_time = time.time() # 4. Dynamic sleep adjustment elapsed = time.time() - self.last_update_time sleep_time = max(0.01, self.interval - elapsed) time.sleep(sleep_time) except DeviceDisconnectedError as e: self.error_signal.emit(f"Device error: {str(e)}") break except Exception as e: self.error_signal.emit(f"Measurement error: {str(e)}") self.status_signal.emit(f"Error: {str(e)}") break self.status_signal.emit("Measurement stopped") self._running = False def stop(self): """Safe thread termination with timeout""" self._running = False if self.isRunning(): self.quit() if not self.wait(300): # 300ms grace period self.terminate() # Forceful termination if needed def is_active(self): """Check if thread is running and updating""" with self._lock: return self._running and (time.time() - self.last_update_time < 2.0) class BatteryTester(QMainWindow): def __init__(self): super().__init__() # Initialisierung aller Attribute self.time_data = deque() self.voltage_data = deque() self.current_data = deque() self.phase_data = deque() # Testvariablen self.test_phase = "Bereit" self.capacity_ah = 0.0 self.charge_capacity = 0.0 self.coulomb_efficiency = 0.0 self.cycle_count = 0 # Farbschema self.bg_color = QColor(46, 52, 64) self.fg_color = QColor(216, 222, 233) self.accent_color = QColor(94, 129, 172) self.warning_color = QColor(191, 97, 106) self.success_color = QColor(163, 190, 140) # Gerätestatus self.session_active = False self.measuring = False self.test_running = False self.continuous_mode = False self.request_stop = False self.interval = 0.1 self.log_dir = os.path.expanduser("~/adalm1000/logs") os.makedirs(self.log_dir, exist_ok=True) # Thread-Management self.measurement_thread = None self.test_thread = None # UI initialisieren self.setup_ui() # Gerät verzögert initialisieren QTimer.singleShot(100, self.safe_init_device) def setup_ui(self): """Konfiguriert die Benutzeroberfläche""" self.setWindowTitle("ADALM1000 - Batteriekapazitätstester (CC-Test)") self.resize(1000, 800) self.setMinimumSize(800, 700) # Hintergrundfarbe setzen palette = self.palette() palette.setColor(QPalette.Window, self.bg_color) palette.setColor(QPalette.WindowText, self.fg_color) palette.setColor(QPalette.Base, QColor(59, 66, 82)) palette.setColor(QPalette.Text, self.fg_color) palette.setColor(QPalette.Button, self.accent_color) palette.setColor(QPalette.ButtonText, self.fg_color) self.setPalette(palette) # Hauptwidget und Layout self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.main_layout = QVBoxLayout(self.central_widget) self.main_layout.setContentsMargins(10, 10, 10, 10) self.main_layout.setSpacing(10) # Kopfbereich header_frame = QWidget() header_layout = QHBoxLayout(header_frame) header_layout.setContentsMargins(0, 0, 0, 0) self.title_label = QLabel("ADALM1000 Batteriekapazitätstester (CC-Test)") self.title_label.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {self.accent_color.name()};") header_layout.addWidget(self.title_label, 1) # Statusindikator self.connection_label = QLabel("Getrennt") header_layout.addWidget(self.connection_label) self.status_light = QLabel() self.status_light.setFixedSize(20, 20) self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") header_layout.addWidget(self.status_light) # Verbinden-Button self.reconnect_btn = QPushButton("Verbinden") self.reconnect_btn.clicked.connect(self.reconnect_device) header_layout.addWidget(self.reconnect_btn) self.main_layout.addWidget(header_frame) # Messanzeige display_frame = QFrame() display_frame.setFrameShape(QFrame.StyledPanel) display_layout = QGridLayout(display_frame) measurement_labels = [ ("Spannung (V)", "V"), ("Strom (A)", "A"), ("Testphase", ""), ("Verstrichene Zeit", "s"), ("Entladekapazität", "Ah"), ("Ladekapazität", "Ah"), ("Coulomb-Eff.", "%"), ("Zykluszählung", ""), ] for i, (label, unit) in enumerate(measurement_labels): row = i // 2 col = (i % 2) * 2 lbl = QLabel(f"{label}:") lbl.setStyleSheet("font-size: 11px;") display_layout.addWidget(lbl, row, col) value_label = QLabel("0.000") value_label.setStyleSheet("font-size: 12px; font-weight: bold;") display_layout.addWidget(value_label, row, col + 1) if unit: unit_label = QLabel(unit) display_layout.addWidget(unit_label, row, col + 2) if i == 0: self.voltage_label = value_label elif i == 1: self.current_label = value_label elif i == 2: self.phase_label = value_label self.phase_label.setText(self.test_phase) elif i == 3: self.time_label = value_label elif i == 4: self.capacity_label = value_label elif i == 5: self.charge_capacity_label = value_label elif i == 6: self.efficiency_label = value_label elif i == 7: self.cycle_label = value_label self.main_layout.addWidget(display_frame) # Steuerbereich controls_frame = QWidget() controls_layout = QHBoxLayout(controls_frame) controls_layout.setContentsMargins(0, 0, 0, 0) # Parameterrahmen params_frame = QFrame() params_frame.setFrameShape(QFrame.StyledPanel) params_layout = QGridLayout(params_frame) # Batteriekapazität params_layout.addWidget(QLabel("Batteriekapazität (Ah):"), 0, 0) self.capacity_input = QLineEdit("0.2") self.capacity_input.setFixedWidth(80) params_layout.addWidget(self.capacity_input, 0, 1) # Lade-Endspannung params_layout.addWidget(QLabel("Lade-Endspannung (V):"), 1, 0) self.charge_cutoff_input = QLineEdit("1.43") self.charge_cutoff_input.setFixedWidth(80) params_layout.addWidget(self.charge_cutoff_input, 1, 1) # Entlade-Endspannung params_layout.addWidget(QLabel("Entlade-Endspannung (V):"), 2, 0) self.discharge_cutoff_input = QLineEdit("0.9") self.discharge_cutoff_input.setFixedWidth(80) params_layout.addWidget(self.discharge_cutoff_input, 2, 1) # Ruhezeit params_layout.addWidget(QLabel("Ruhezeit (Stunden):"), 3, 0) self.rest_time_input = QLineEdit("0.25") self.rest_time_input.setFixedWidth(80) params_layout.addWidget(self.rest_time_input, 3, 1) # C-Rate für Test params_layout.addWidget(QLabel("Test-C-Rate:"), 0, 2) self.c_rate_input = QLineEdit("0.1") self.c_rate_input.setFixedWidth(60) params_layout.addWidget(self.c_rate_input, 0, 3) params_layout.addWidget(QLabel("(z.B. 0.2 für C/5)"), 0, 4) controls_layout.addWidget(params_frame, 1) # Button-Bereich button_frame = QWidget() button_layout = QVBoxLayout(button_frame) button_layout.setContentsMargins(0, 0, 0, 0) self.start_button = QPushButton("TEST STARTEN") self.start_button.clicked.connect(self.start_test) self.start_button.setStyleSheet(f"background-color: {self.accent_color.name()}; font-weight: bold;") self.start_button.setEnabled(False) button_layout.addWidget(self.start_button) self.stop_button = QPushButton("TEST STOPPEN") self.stop_button.clicked.connect(self.stop_test) self.stop_button.setStyleSheet(f"background-color: {self.warning_color.name()}; font-weight: bold;") self.stop_button.setEnabled(False) button_layout.addWidget(self.stop_button) # Kontinuierlicher Modus self.continuous_check = QCheckBox("Kontinuierlicher Modus") self.continuous_check.setChecked(True) button_layout.addWidget(self.continuous_check) controls_layout.addWidget(button_frame) self.main_layout.addWidget(controls_frame) # Plotbereich self.setup_plot() self.main_layout.addWidget(self.plot_widget, 1) # Statusleiste self.status_bar = QLabel("Bereit") self.status_bar.setStyleSheet("font-size: 10px; padding: 5px;") self.main_layout.addWidget(self.status_bar) def setup_plot(self): """Konfiguriert den Matplotlib-Plot""" self.plot_widget = QWidget() plot_layout = QVBoxLayout(self.plot_widget) self.fig = Figure(figsize=(8, 5), dpi=100, facecolor='#2E3440') self.fig.subplots_adjust(left=0.1, right=0.88, top=0.9, bottom=0.15) self.ax = self.fig.add_subplot(111) self.ax.set_facecolor('#3B4252') # Initiale Spannungsbereich voltage_padding = 0.2 min_voltage = max(0, float(self.discharge_cutoff_input.text()) - voltage_padding) max_voltage = float(self.charge_cutoff_input.text()) + voltage_padding self.ax.set_ylim(min_voltage, max_voltage) # Spannungsplot self.line_voltage, = self.ax.plot([], [], color='#00BFFF', label='Spannung (V)', linewidth=2) self.ax.set_ylabel("Spannung (V)", color='#00BFFF') self.ax.tick_params(axis='y', labelcolor='#00BFFF') # Stromplot (rechte Achse) self.ax2 = self.ax.twinx() current_padding = 0.05 test_current = float(self.c_rate_input.text()) * float(self.capacity_input.text()) max_current = test_current * 1.5 self.ax2.set_ylim(-max_current - current_padding, max_current + current_padding) self.line_current, = self.ax2.plot([], [], 'r-', label='Strom (A)', linewidth=2) self.ax2.set_ylabel("Strom (A)", color='r') self.ax2.tick_params(axis='y', labelcolor='r') self.ax.set_xlabel('Zeit (s)', color=self.fg_color.name()) self.ax.set_title('Batterietest (CC)', color=self.fg_color.name()) self.ax.tick_params(axis='x', colors=self.fg_color.name()) self.ax.grid(True, color='#4C566A') # Legenden positionieren self.ax.legend(loc='upper left', bbox_to_anchor=(0.01, 0.99)) self.ax2.legend(loc='upper right', bbox_to_anchor=(0.99, 0.99)) # Plot einbetten self.canvas = FigureCanvas(self.fig) plot_layout.addWidget(self.canvas) def safe_init_device(self): """Sichere Geräteinitialisierung mit Fehlerbehandlung""" try: self.init_device() except Exception as e: self.handle_device_error(str(e)) def init_device(self): """Initialisiert das ADALM1000-Gerät""" # Temporarily enable USB debugging os.environ['LIBUSB_DEBUG'] = '3' # Set to 0 in production self.cleanup_device() try: # Verzögerung zur Vermeidung von "Device busy" Fehlern time.sleep(1.5) self.session = pysmu.Session(ignore_dataflow=True, queue_size=10000) if not self.session.devices: raise Exception("Kein ADALM1000 erkannt - Verbindung prüfen") self.dev = self.session.devices[0] # Kanäle zurücksetzen self.dev.channels['A'].mode = pysmu.Mode.HI_Z self.dev.channels['B'].mode = pysmu.Mode.HI_Z self.dev.channels['A'].constant(0) self.dev.channels['B'].constant(0) self.session.start(0) self.status_light.setStyleSheet("background-color: green; border-radius: 10px;") self.connection_label.setText("Verbunden") self.status_bar.setText("Gerät verbunden | Bereit zur Messung") self.session_active = True self.start_button.setEnabled(True) # Mess-Thread starten self.start_measurement_thread() except Exception as e: raise Exception(f"Geräteinitialisierung fehlgeschlagen: {str(e)}") def cleanup_device(self): """Bereinigt Geräteressourcen""" # Mess-Thread stoppen if self.measurement_thread is not None: try: self.measurement_thread.stop() # Wait for thread to finish if not self.measurement_thread.wait(1000): # 1 second timeout print("Warning: Measurement thread didn't stop cleanly") self.measurement_thread = None except Exception as e: print(f"Fehler beim Stoppen des Mess-Threads: {e}") # Sitzung bereinigen if hasattr(self, 'session'): try: if self.session_active: # Add small delay before ending session time.sleep(0.1) self.session.end() self.session_active = False del self.session except Exception as e: print(f"Fehler beim Bereinigen der Sitzung: {e}") finally: self.session_active = False # UI-Status zurücksetzen self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") self.connection_label.setText("Getrennt") self.start_button.setEnabled(False) self.stop_button.setEnabled(False) def start_measurement_thread(self): """Startet den Mess-Thread""" if self.measurement_thread is not None: self.measurement_thread.stop() self.measurement_thread = MeasurementThread(self.dev, self.interval) self.measurement_thread.update_signal.connect(self.update_measurements) self.measurement_thread.error_signal.connect(self.handle_device_error) self.measurement_thread.start() def reconnect_device(self): """Versucht, das Gerät erneut zu verbinden""" self.status_bar.setText("Versuche erneut zu verbinden...") self.cleanup_device() QTimer.singleShot(2000, self.safe_init_device) # Mit Verzögerung erneut versuchen def handle_device_error(self, error_msg): """Behandelt Gerätefehler""" print(f"Gerätefehler: {error_msg}") self.status_bar.setText(f"Gerätefehler: {error_msg}") self.cleanup_device() # Nur Meldung anzeigen, wenn Fenster sichtbar ist if self.isVisible(): QMessageBox.critical( self, "Gerätefehler", f"Gerätefehler aufgetreten:\n{error_msg}\n\n" "1. USB-Verbindung prüfen\n" "2. Manuell erneut verbinden\n" "3. Anwendung neu starten bei anhaltenden Problemen" ) def start_test(self): """Startet den vollständigen Batterietestzyklus""" if not self.test_running: try: # Eingabewerte holen und validieren capacity = float(self.capacity_input.text()) charge_cutoff = float(self.charge_cutoff_input.text()) discharge_cutoff = float(self.discharge_cutoff_input.text()) c_rate = float(self.c_rate_input.text()) if capacity <= 0: raise ValueError("Batteriekapazität muss positiv sein") if charge_cutoff <= discharge_cutoff: raise ValueError("Lade-Endspannung muss höher sein als Entlade-Endspannung") if c_rate <= 0: raise ValueError("C-Rate muss positiv sein") self.continuous_mode = self.continuous_check.isChecked() test_current = c_rate * capacity if test_current > 0.2: raise ValueError("Strom muss ≤200mA (0,2A) für ADALM1000 sein") # Daten zurücksetzen self.time_data.clear() self.voltage_data.clear() self.current_data.clear() self.capacity_ah = 0.0 self.charge_capacity = 0.0 self.coulomb_efficiency = 0.0 self.cycle_count = 0 self.start_time = time.time() self.time_label.setText("00:00:00") self.reset_plot() # Protokolldatei vorbereiten timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") self.base_filename = os.path.join(self.log_dir, f"battery_test_{timestamp}") # Test starten self.test_running = True self.start_time = time.time() self.test_phase = "Initiale Entladung" self.phase_label.setText(self.test_phase) self.start_button.setEnabled(False) self.stop_button.setEnabled(True) self.status_bar.setText(f"Test gestartet | Entladen auf {discharge_cutoff}V @ {test_current:.3f}A") # Testsequenz in eigenem Thread starten self.test_thread = threading.Thread(target=self.run_test_sequence, daemon=True) self.test_thread.start() except Exception as e: QMessageBox.critical(self, "Fehler", str(e)) def run_test_sequence(self): """Haupttestsequenz für Lade-/Entladezyklen""" try: test_current = float(self.c_rate_input.text()) * float(self.capacity_input.text()) charge_cutoff = float(self.charge_cutoff_input.text()) discharge_cutoff = float(self.discharge_cutoff_input.text()) while self.test_running and (self.continuous_mode or self.cycle_count == 0): self.request_stop = False self.cycle_count += 1 self.cycle_label.setText(f"{self.cycle_count}") # Neue Protokolldatei für diesen Zyklus if not self.create_cycle_log_file(): break # Ladephase self.execute_charge_phase(test_current, charge_cutoff) if self.request_stop or not self.test_running: break # Ruhephase nach Ladung self.execute_rest_phase("Nachladung") if self.request_stop or not self.test_running: break # Entladephase self.execute_discharge_phase(test_current, discharge_cutoff) if not self.continuous_check.isChecked(): self.test_running = False break # Ruhephase nach Entladung if self.test_running and not self.request_stop: self.execute_rest_phase("Nach Entladung") # Coulomb-Effizienz berechnen if not self.request_stop and self.charge_capacity > 0: self.coulomb_efficiency = (self.capacity_ah / self.charge_capacity) * 100 self.efficiency_label.setText(f"{self.coulomb_efficiency:.1f}") # Status aktualisieren self.status_bar.setText( f"Zyklus {self.cycle_count} abgeschlossen | " f"Entladung: {self.capacity_ah:.3f}Ah | " f"Ladung: {self.charge_capacity:.3f}Ah | " f"Effizienz: {self.coulomb_efficiency:.1f}%" ) # Zyklusprotokoll schreiben self.write_cycle_summary() # Test abschließen self.finalize_test() except Exception as e: self.handle_device_error(str(e)) self.finalize_test() def execute_charge_phase(self, current, target_voltage): """Führt die Ladephase durch""" self.test_phase = "Laden" self.phase_label.setText(self.test_phase) self.status_bar.setText(f"Laden auf {target_voltage}V @ {current:.3f}A") self.measuring = True self.dev.channels['A'].mode = pysmu.Mode.SIMV self.dev.channels['A'].constant(current) self.charge_capacity = 0.0 self.charge_capacity_label.setText("0.000") last_update = time.time() while self.test_running and not self.request_stop: if not self.voltage_data: time.sleep(0.1) continue now = time.time() delta_t = now - last_update last_update = now measured_current = abs(self.current_data[-1]) self.charge_capacity += measured_current * delta_t / 3600 self.charge_capacity_label.setText(f"{self.charge_capacity:.4f}") current_voltage = self.voltage_data[-1] if current_voltage >= target_voltage or self.request_stop: break time.sleep(0.1) def execute_discharge_phase(self, current, target_voltage): """Führt die Entladephase durch""" self.test_phase = "Entladen" self.phase_label.setText(self.test_phase) self.status_bar.setText(f"Entladen auf {target_voltage}V @ {current:.3f}A") self.measuring = True self.dev.channels['A'].mode = pysmu.Mode.SIMV self.dev.channels['A'].constant(-current) self.capacity_ah = 0.0 self.capacity_label.setText("0.000") last_update = time.time() while self.test_running and not self.request_stop: if not self.current_data: time.sleep(0.1) continue now = time.time() delta_t = now - last_update last_update = now measured_current = abs(self.current_data[-1]) self.capacity_ah += measured_current * delta_t / 3600 self.capacity_label.setText(f"{self.capacity_ah:.4f}") current_voltage = self.voltage_data[-1] if current_voltage <= target_voltage or self.request_stop: break time.sleep(0.1) def execute_rest_phase(self, phase_name): """Führt eine Ruhephase durch""" self.test_phase = f"Ruhen ({phase_name})" self.phase_label.setText(self.test_phase) self.measuring = False self.dev.channels['A'].mode = pysmu.Mode.HI_Z self.dev.channels['A'].constant(0) rest_time = float(self.rest_time_input.text()) * 3600 rest_end = time.time() + rest_time while time.time() < rest_end and self.test_running and not self.request_stop: time_left = max(0, rest_end - time.time()) self.status_bar.setText( f"Ruhen ({phase_name}) | " f"Verbleibende Zeit: {time_left/60:.1f} min" ) time.sleep(1) def create_cycle_log_file(self): """Erstellt eine neue Protokolldatei für den aktuellen Zyklus""" try: suffix = 1 while True: self.filename = f"{self.base_filename}_{suffix}.csv" if not os.path.exists(self.filename): break suffix += 1 self.current_cycle_file = open(self.filename, 'w', newline='') self.log_writer = csv.writer(self.current_cycle_file) self.log_writer.writerow([ "Zeit(s)", "Spannung(V)", "Strom(A)", "Phase", "Entladekapazität(Ah)", "Ladekapazität(Ah)", "Coulomb-Eff.(%)", "Zyklus" ]) self.log_buffer = [] return True except Exception as e: self.handle_device_error(f"Protokolldatei konnte nicht erstellt werden: {e}") return False def write_cycle_summary(self): """Schreibt eine Zykluszusammenfassung in die Protokolldatei""" if hasattr(self, 'current_cycle_file') and self.current_cycle_file: try: if self.log_buffer: self.log_writer.writerows(self.log_buffer) self.log_buffer.clear() summary = ( f"Zyklus {self.cycle_count} Zusammenfassung - " f"Entladung={self.capacity_ah:.4f}Ah, " f"Ladung={self.charge_capacity:.4f}Ah, " f"Effizienz={self.coulomb_efficiency:.1f}%" ) self.current_cycle_file.write(summary + "\n") self.current_cycle_file.flush() except Exception as e: print(f"Fehler beim Schreiben der Zykluszusammenfassung: {e}") def stop_test(self): """Stoppt den laufenden Test sicher""" if not self.test_running: return self.request_stop = True self.test_running = False self.measuring = False self.test_phase = "Bereit" self.phase_label.setText(self.test_phase) if hasattr(self, 'dev'): try: self.dev.channels['A'].mode = pysmu.Mode.HI_Z self.dev.channels['A'].constant(0) except Exception as e: print(f"Fehler beim Zurücksetzen des Geräts: {e}") # UI aktualisieren self.status_bar.setText("Test gestoppt - Bereit für neuen Test") self.stop_button.setEnabled(False) self.start_button.setEnabled(True) # Testdaten finalisieren QTimer.singleShot(100, self.finalize_test) def finalize_test(self): """Finale Bereinigung nach Testende mit verbessertem Fenster-Handling""" try: # 1. Protokolldaten schreiben (mit zusätzlichem Lock) if hasattr(self, 'log_buffer') and self.log_buffer: try: with threading.Lock(): # Thread-sicherer Zugriff self.log_writer.writerows(self.log_buffer) self.log_buffer.clear() except Exception as e: print(f"Fehler beim Schreiben des Protokollpuffers: {e}") # 2. Protokolldatei schließen (mit Fehlertoleranz) if hasattr(self, 'current_cycle_file') and self.current_cycle_file: try: self.current_cycle_file.flush() # Daten sicher schreiben os.fsync(self.current_cycle_file.fileno()) # Betriebssystem-Puffer leeren self.current_cycle_file.close() except Exception as e: print(f"Fehler beim Schließen der Protokolldatei: {e}") # 3. Benachrichtigung anzeigen (mit Fokus-Sicherung) msg_box = QMessageBox(self) # Expliziter Parent msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint | # Immer im Vordergrund Qt.MSWindowsFixedSizeDialogHint) # Besseres Verhalten unter Windows msg_box.setIcon(QMessageBox.Information) msg_box.setWindowTitle("Test abgeschlossen") msg_box.setText( f"Test wurde sicher beendet.\n\n" f"Entladekapazität: {self.capacity_ah:.3f}Ah\n" f"Abgeschlossene Zyklen: {self.cycle_count}" ) # Sicherstellen, dass das Fenster sichtbar wird msg_box.raise_() msg_box.activateWindow() # Non-blocking anzeigen und Fokus erzwingen QTimer.singleShot(100, lambda: ( msg_box.show(), msg_box.raise_(), msg_box.activateWindow() )) msg_box.exec_() except Exception as e: print(f"Kritischer Fehler in finalize_test: {e}") finally: # 4. Gerätestatus zurücksetzen self.test_running = False self.request_stop = True self.measuring = False def update_measurements(self, voltage, current, current_time): """Aktualisiert die Messwerte im UI""" self.time_data.append(current_time) self.voltage_data.append(voltage) self.current_data.append(current) # UI aktualisieren self.voltage_label.setText(f"{voltage:.4f}") self.current_label.setText(f"{current:.4f}") self.time_label.setText(self.format_time(current_time)) # Plot regelmäßig aktualisieren if len(self.time_data) % 10 == 0: self.update_plot() # Daten protokollieren, wenn Test läuft if self.test_running and hasattr(self, 'current_cycle_file'): self.log_buffer.append([ f"{current_time:.3f}", f"{voltage:.6f}", f"{current:.6f}", self.test_phase, f"{self.capacity_ah:.4f}", f"{self.charge_capacity:.4f}", f"{self.coulomb_efficiency:.1f}", f"{self.cycle_count}" ]) # Daten in Blöcken schreiben if len(self.log_buffer) >= 10: with open(self.filename, 'a', newline='') as f: writer = csv.writer(f) writer.writerows(self.log_buffer) self.log_buffer.clear() def update_plot(self): """Aktualisiert den Plot mit neuen Daten""" if not self.time_data: return self.line_voltage.set_data(self.time_data, self.voltage_data) self.line_current.set_data(self.time_data, self.current_data) self.auto_scale_axes() self.canvas.draw_idle() def auto_scale_axes(self): """Passt die Plotachsen automatisch an""" if not self.time_data: return # X-Achse anpassen max_time = self.time_data[-1] current_xlim = self.ax.get_xlim() if max_time > current_xlim[1] * 0.95: self.ax.set_xlim(0, max_time * 1.05) self.ax2.set_xlim(0, max_time * 1.05) # Y-Achsen anpassen if self.voltage_data: voltage_padding = 0.2 min_v = max(0, min(self.voltage_data) - voltage_padding) max_v = min(5.0, max(self.voltage_data) + voltage_padding) self.ax.set_ylim(min_v, max_v) if self.current_data: current_padding = 0.05 min_c = max(-0.25, min(self.current_data) - current_padding) max_c = min(0.25, max(self.current_data) + current_padding) self.ax2.set_ylim(min_c, max_c) @staticmethod def format_time(seconds): """Formatiert Sekunden als HH:MM:SS""" hours = int(seconds // 3600) minutes = int((seconds % 3600) // 60) seconds = int(seconds % 60) return f"{hours:02d}:{minutes:02d}:{seconds:02d}" def reset_plot(self): """Setzt den Plot zurück""" self.line_voltage.set_data([], []) self.line_current.set_data([], []) # Achsen auf Startwerte zurücksetzen self.ax.set_xlim(0, 10) # X-Achse (Zeit) bei 0 beginnen voltage_padding = 0.2 min_voltage = max(0, float(self.discharge_cutoff_input.text()) - voltage_padding) max_voltage = float(self.charge_cutoff_input.text()) + voltage_padding self.ax.set_ylim(min_voltage, max_voltage) self.canvas.draw() def closeEvent(self, event): """Bereinigt beim Schließen des Fensters""" self.cleanup_device() event.accept() if __name__ == "__main__": app = QApplication([]) app.setStyle('Fusion') window = BatteryTester() window.show() app.exec_()