diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py index b35d172..e42d501 100644 --- a/MainCode/adalm1000_logger.py +++ b/MainCode/adalm1000_logger.py @@ -3,6 +3,7 @@ import os import time import csv import threading +import traceback from datetime import datetime import numpy as np import matplotlib @@ -12,9 +13,11 @@ from matplotlib.figure import Figure from collections import deque from queue import Queue, Full, Empty -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, - QPushButton, QLineEdit, QCheckBox, QFrame, QMessageBox, QFileDialog, QComboBox) +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QGridLayout, QLabel, QPushButton, QLineEdit, QCheckBox, + QFrame, QMessageBox, QFileDialog, QComboBox) from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot, QObject, QThread +from PyQt5.QtGui import QDoubleValidator from PyQt5 import sip import pysmu @@ -74,17 +77,28 @@ class DeviceManager: return True # Under threshold - continue def start_measurement(self, interval=0.1): - if self.measurement_thread and self.measurement_thread.isRunning(): - self.measurement_thread.stop() - - self.measurement_thread = MeasurementThread(self.dev, interval, self) # Pass self as parent_manager + self.stop_measurement() # Ensure any existing thread is stopped + self.measurement_thread = MeasurementThread(self.dev, interval, self) self.measurement_thread.start() self.is_running = True def stop_measurement(self): if self.measurement_thread: - self.measurement_thread.stop() - self.measurement_thread.wait(500) + try: + # Disconnect signals first + try: + self.measurement_thread.update_signal.disconnect() + self.measurement_thread.error_signal.disconnect() + except (RuntimeError, TypeError): + pass + + # Then stop the thread + self.measurement_thread.stop() + if not self.measurement_thread.wait(500): + self.measurement_thread.terminate() + self.measurement_thread = None + except Exception as e: + print(f"Error stopping measurement thread: {e}") self.is_running = False def reset_data(self): @@ -112,7 +126,7 @@ class MeasurementThread(QThread): update_signal = pyqtSignal(float, float, float) error_signal = pyqtSignal(str) - def __init__(self, device, interval, parent_manager): # Add parent_manager + def __init__(self, device, interval, parent_manager): super().__init__() self.device = device self.interval = interval @@ -122,13 +136,18 @@ class MeasurementThread(QThread): self.current_window = [] self.start_time = None self.measurement_queue = Queue(maxsize=1) - self.current_direction = 1 # 1 for source, -1 for sink - self.parent_manager = parent_manager + self.current_direction = 1 + self.parent_manager = parent_manager + def stop(self): + self._running = False + if not self.wait(500): # Wait up to 500ms for clean shutdown + self.terminate() + def run(self): """Continuous measurement loop with enhanced error handling""" self._running = True - self.start_time = time.time() + self.start_time = time.time() # This gets reset when mode changes consecutive_errors = 0 max_consecutive_errors = 5 @@ -213,10 +232,6 @@ class MeasurementThread(QThread): """Set current direction (1 for source, -1 for sink)""" self.current_direction = direction - def stop(self): - self._running = False - self.wait(500) - class TestSequenceWorker(QObject): finished = pyqtSignal() update_phase = pyqtSignal(str) @@ -590,14 +605,14 @@ class BatteryTester(QMainWindow): self.status_timer.start(1000) #every second def setup_ui(self): - """Configure the user interface""" + """Configure the user interface with all elements properly organized""" # Main widget and 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) - # Mode and device selection + # Mode and device selection frame mode_frame = QFrame() mode_frame.setFrameShape(QFrame.StyledPanel) mode_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}") @@ -674,7 +689,7 @@ class BatteryTester(QMainWindow): display_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}") display_layout = QGridLayout(display_frame) - # Measurement values - common for all modes + # Measurement values measurement_labels = [ ("Voltage", "V"), ("Current", "A"), ("Test Phase", ""), ("Elapsed Time", "s"), ("Capacity", "Ah"), ("Power", "W"), @@ -730,123 +745,103 @@ class BatteryTester(QMainWindow): self.params_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}") self.params_layout = QGridLayout(self.params_frame) - # Common parameters - self.capacity = 0.2 - self.capacity_label_input = QLabel("Battery Capacity (Ah):") - self.capacity_label_input.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.capacity_label_input, 0, 0) - self.capacity_input = QLineEdit("0.2") - self.capacity_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};") - self.capacity_input.setFixedWidth(60) - self.params_layout.addWidget(self.capacity_input, 0, 1) + # Add parameter inputs + row = 0 - # C-rate for test - self.c_rate = 0.1 - self.c_rate_label = QLabel("Test C-rate:") + # Battery Capacity + self.capacity_label = QLabel("Capacity (Ah):") + self.capacity_label.setStyleSheet(f"color: {self.fg_color};") + self.params_layout.addWidget(self.capacity_label, row, 0) + + self.capacity_input = QLineEdit("1.0") + self.capacity_input.setValidator(QDoubleValidator(0.001, 100, 3)) + self.params_layout.addWidget(self.capacity_input, row, 1) + row += 1 + + # C-Rate + self.c_rate_label = QLabel("C-Rate:") self.c_rate_label.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.c_rate_label, 1, 0) + self.params_layout.addWidget(self.c_rate_label, row, 0) + self.c_rate_input = QLineEdit("0.1") - self.c_rate_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};") - self.c_rate_input.setFixedWidth(40) - self.params_layout.addWidget(self.c_rate_input, 1, 1) + self.c_rate_input.setValidator(QDoubleValidator(0.01, 1, 2)) + self.params_layout.addWidget(self.c_rate_input, row, 1) + row += 1 - c_rate_note = QLabel("(e.g., 0.2 for C/5)") - c_rate_note.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(c_rate_note, 1, 2) - - # Discharge cutoff (used in Discharge and Cycle modes) - self.discharge_cutoff = 0.9 - self.discharge_cutoff_label = QLabel("Discharge Cutoff (V):") - self.discharge_cutoff_label.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.discharge_cutoff_label, 2, 0) - self.discharge_cutoff_input = QLineEdit("0.9") - self.discharge_cutoff_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};") - self.discharge_cutoff_input.setFixedWidth(60) - self.params_layout.addWidget(self.discharge_cutoff_input, 2, 1) - - # Charge cutoff (only for Cycle mode) - self.charge_cutoff = 1.43 + # Charge Cutoff Voltage self.charge_cutoff_label = QLabel("Charge Cutoff (V):") self.charge_cutoff_label.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.charge_cutoff_label, 3, 0) + self.params_layout.addWidget(self.charge_cutoff_label, row, 0) + self.charge_cutoff_input = QLineEdit("1.43") - self.charge_cutoff_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};") - self.charge_cutoff_input.setFixedWidth(60) - self.params_layout.addWidget(self.charge_cutoff_input, 3, 1) - self.charge_cutoff_label.hide() - self.charge_cutoff_input.hide() + self.charge_cutoff_input.setValidator(QDoubleValidator(0.1, 5.0, 3)) + self.params_layout.addWidget(self.charge_cutoff_input, row, 1) + row += 1 - # Rest time (only for Cycle mode) - self.rest_time = 0.25 - self.rest_time_label = QLabel("Rest Time (hours):") + # Discharge Cutoff Voltage + self.discharge_cutoff_label = QLabel("Discharge Cutoff (V):") + self.discharge_cutoff_label.setStyleSheet(f"color: {self.fg_color};") + self.params_layout.addWidget(self.discharge_cutoff_label, row, 0) + + self.discharge_cutoff_input = QLineEdit("0.01") + self.discharge_cutoff_input.setValidator(QDoubleValidator(0.1, 5.0, 3)) + self.params_layout.addWidget(self.discharge_cutoff_input, row, 1) + row += 1 + + # Rest Time + self.rest_time_label = QLabel("Rest Time (h):") self.rest_time_label.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.rest_time_label, 4, 0) - self.rest_time_input = QLineEdit("0.25") - self.rest_time_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};") - self.rest_time_input.setFixedWidth(60) - self.params_layout.addWidget(self.rest_time_input, 4, 1) - self.rest_time_label.hide() - self.rest_time_input.hide() + self.params_layout.addWidget(self.rest_time_label, row, 0) - # Test conditions input - self.test_conditions_label = QLabel("Test Conditions/Chemistry:") + self.rest_time_input = QLineEdit("0.5") + self.rest_time_input.setValidator(QDoubleValidator(0.1, 24, 1)) + self.params_layout.addWidget(self.rest_time_input, row, 1) + row += 1 + + # Test Conditions + self.test_conditions_label = QLabel("Test Conditions:") self.test_conditions_label.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.test_conditions_label, 5, 0) - self.test_conditions_input = QLineEdit("") - self.test_conditions_input.setStyleSheet(f"background-color: #3B4252; color: {self.fg_color};") - self.test_conditions_input.setFixedWidth(120) - self.params_layout.addWidget(self.test_conditions_input, 5, 1) + self.params_layout.addWidget(self.test_conditions_label, row, 0) + + self.test_conditions_input = QLineEdit("Room Temperature") + self.params_layout.addWidget(self.test_conditions_input, row, 1) controls_layout.addWidget(self.params_frame, 1) - # Button frame + # Button frame with single toggle button button_frame = QFrame() button_frame.setFrameShape(QFrame.NoFrame) button_layout = QVBoxLayout(button_frame) button_layout.setContentsMargins(0, 0, 0, 0) - # Start/Stop buttons - self.start_button = QPushButton("START") - self.start_button.setStyleSheet(f""" + # Single toggle button (Start/Stop) + self.toggle_button = QPushButton("START") + self.toggle_button.setCheckable(True) + self.toggle_button.setStyleSheet(f""" QPushButton {{ background-color: {self.accent_color}; color: {self.fg_color}; font-weight: bold; padding: 6px; border-radius: 4px; + min-width: 80px; }} - QPushButton:disabled {{ - background-color: #4C566A; - color: #D8DEE9; - }} - """) - self.start_button.clicked.connect(self.start_test) - button_layout.addWidget(self.start_button) - - self.stop_button = QPushButton("STOP") - self.stop_button.setStyleSheet(f""" - QPushButton {{ + QPushButton:checked {{ background-color: {self.warning_color}; - color: {self.fg_color}; - font-weight: bold; - padding: 6px; - border-radius: 4px; }} QPushButton:disabled {{ background-color: #4C566A; color: #D8DEE9; }} """) - self.stop_button.clicked.connect(self.stop_test) - self.stop_button.setEnabled(False) - button_layout.addWidget(self.stop_button) + self.toggle_button.clicked.connect(self.toggle_test) + button_layout.addWidget(self.toggle_button) # Continuous mode checkbox (only for Cycle mode) self.continuous_mode_check = QCheckBox("Continuous Mode") self.continuous_mode_check.setChecked(True) self.continuous_mode_check.setStyleSheet(f"color: {self.fg_color};") button_layout.addWidget(self.continuous_mode_check) - self.continuous_mode_check.stateChanged.connect(self.handle_continuous_mode_change) self.continuous_mode_check.hide() # Record button for Live mode @@ -878,7 +873,7 @@ class BatteryTester(QMainWindow): self.status_bar = self.statusBar() self.status_bar.setStyleSheet(f"color: {self.fg_color};") self.status_bar.showMessage("Ready") - + # Apply dark theme self.setStyleSheet(f""" QMainWindow {{ @@ -896,55 +891,72 @@ class BatteryTester(QMainWindow): }} """) - # Set initial mode - self.current_mode = "Live Monitoring" - self.mode_combo.setCurrentText(self.current_mode) - self.change_mode(self.current_mode) # Initialize UI for live mode + def toggle_test(self): + """Toggle between start and stop based on button state""" + if self.toggle_button.isChecked(): + # Button shows "STOP" - run start logic + self.toggle_button.setText("STOP") + self.start_test() + else: + # Button shows "START" - run stop logic + self.toggle_button.setText("START") + self.stop_test() def change_mode(self, mode_name): """Change between different test modes""" self.current_mode = mode_name self.stop_test() # Stop any current operation - # Hide all optional parameters first - self.charge_cutoff_label.hide() - self.charge_cutoff_input.hide() - self.discharge_cutoff_label.hide() - self.discharge_cutoff_input.hide() - self.rest_time_label.hide() - self.rest_time_input.hide() - self.continuous_mode_check.hide() - self.record_button.hide() + # Show/hide mode-specific UI elements + show_charge = mode_name in ["Cycle Test", "Charge Test"] + show_discharge = mode_name in ["Cycle Test", "Discharge Test"] + show_rest = mode_name == "Cycle Test" - # Show mode-specific parameters + self.charge_cutoff_label.setVisible(show_charge) + self.charge_cutoff_input.setVisible(show_charge) + self.discharge_cutoff_label.setVisible(show_discharge) + self.discharge_cutoff_input.setVisible(show_discharge) + self.rest_time_label.setVisible(show_rest) + self.rest_time_input.setVisible(show_rest) + + # Continuous mode checkbox only for cycle test + self.continuous_mode_check.setVisible(mode_name == "Cycle Test") + + # Record button only for live monitoring + self.record_button.setVisible(mode_name == "Live Monitoring") + + # Set button text based on mode if mode_name == "Cycle Test": - self.charge_cutoff_label.show() - self.charge_cutoff_input.show() - self.discharge_cutoff_label.show() - self.discharge_cutoff_input.show() - self.rest_time_label.show() - self.rest_time_input.show() - self.continuous_mode_check.show() - self.start_button.setText("START CYCLE TEST") - self.start_button.setEnabled(True) # Explicitly enable + self.toggle_button.setText("START CYCLE TEST") elif mode_name == "Discharge Test": - self.discharge_cutoff_label.show() - self.discharge_cutoff_input.show() - self.start_button.setText("START DISCHARGE") - self.start_button.setEnabled(True) # Explicitly enable + self.toggle_button.setText("START DISCHARGE") elif mode_name == "Charge Test": - self.charge_cutoff_label.show() - self.charge_cutoff_input.show() - self.start_button.setText("START CHARGE") - self.start_button.setEnabled(True) # Explicitly enable + self.toggle_button.setText("START CHARGE") elif mode_name == "Live Monitoring": - self.record_button.show() - self.start_button.setText("START MONITORING") - # Only enable start button if device is connected - self.start_button.setEnabled(self.session_active) + self.toggle_button.setText("START") # Will be hidden anyway + self.toggle_button.hide() + + # Reset button state + self.toggle_button.setChecked(False) + + # Reset measurement state and zero the time + if self.active_device: + dev = self.active_device + dev.reset_data() # This clears all data buffers - # Reset measurement state - self.reset_test() + # Reset the measurement thread's start time + if hasattr(dev, 'measurement_thread'): + dev.measurement_thread.start_time = time.time() + + # Reset UI displays + self.capacity_label.setText("0.0000") + self.energy_label.setText("0.0000") + self.cycle_label.setText("0") + self.phase_label.setText("Idle") + self.time_label.setText("00:00:00") + + # Reset plot + self.reset_plot() self.status_bar.showMessage(f"Mode changed to {mode_name}") @@ -1092,7 +1104,7 @@ class BatteryTester(QMainWindow): self.session_active = True self.connection_label.setText(f"Connected: {first_serial}") self.status_light.setStyleSheet("background-color: green; border-radius: 10px;") - self.start_button.setEnabled(True) + self.toggle_button.setEnabled(True) # Connect measurement signals self.measurement_thread = self.active_device.measurement_thread @@ -1180,7 +1192,7 @@ class BatteryTester(QMainWindow): self.status_light.setStyleSheet("background-color: orange; border-radius: 10px;") self.connection_label.setText("Simulated Devices") self.session_active = True - self.start_button.setEnabled(True) + self.toggle_button.setEnabled(True) QMessageBox.warning(self, "Simulation Mode", "Using simulated devices - real hardware not detected") @@ -1195,23 +1207,45 @@ class BatteryTester(QMainWindow): if serial not in self.devices: return - # Stoppe aktuelles Gerät + # Stop current device if self.active_device: - self.active_device.stop_measurement() - if self.measurement_thread: - self.measurement_thread.update_signal.disconnect() - - # Neues Gerät aktivieren + try: + # Disconnect signals safely + if hasattr(self.active_device, 'measurement_thread'): + thread = self.active_device.measurement_thread + try: + if thread.isRunning(): + thread.stop() + thread.wait(500) + thread.update_signal.disconnect() + thread.error_signal.disconnect() + except (RuntimeError, TypeError) as e: + print(f"Signal disconnect warning: {e}") + except Exception as e: + print(f"Error stopping thread: {e}") + except Exception as e: + print(f"Error stopping previous device: {e}") + + # Activate new device self.active_device = self.devices[serial] - self.measurement_thread = self.active_device.measurement_thread - self.measurement_thread.update_signal.connect(self.update_measurements) - self.measurement_thread.error_signal.connect(self.handle_device_error) - # Starte Messung erst NACH Verbindung der Signale - self.active_device.start_measurement(self.interval) + # Connect signals to new device + if hasattr(self.active_device, 'measurement_thread'): + try: + self.measurement_thread = self.active_device.measurement_thread + self.measurement_thread.update_signal.connect(self.update_measurements) + self.measurement_thread.error_signal.connect(self.handle_device_error) + + # Start measurement only AFTER connecting signals + if not self.measurement_thread.isRunning(): + self.active_device.start_measurement(self.interval) + except Exception as e: + print(f"Error connecting to new device: {e}") + return - # UI aktualisieren + # Update UI with current device data self.update_ui_from_active_device() + self.status_bar.showMessage(f"Switched to device: {serial}") def update_ui_from_active_device(self): dev = self.active_device @@ -1245,23 +1279,22 @@ class BatteryTester(QMainWindow): @pyqtSlot(float, float, float) def update_measurements(self, voltage, current, current_time): + if not self.active_device: + return + + dev = self.active_device + # Add measurement validation if not self.validate_measurements(voltage, current): print(f"Invalid measurement: V={voltage:.4f}, I={current:.4f}") return - - dev_manager = self.active_device - dev = self.active_device with self.plot_mutex: - dev_manager.time_data.append(current_time) - dev_manager.voltage_data.append(voltage) - dev_manager.current_data.append(current) dev.time_data.append(current_time) dev.voltage_data.append(voltage) dev.current_data.append(current) - # Aggregation für Anzeige (wie bisher) + # Aggregation for display agg_buf = dev.aggregation_buffer agg_buf['time'].append(current_time) agg_buf['voltage'].append(voltage) @@ -1269,7 +1302,7 @@ class BatteryTester(QMainWindow): agg_buf['count'] += 1 now = time.time() - if now - dev_manager.aggregation_buffer['last_plot_time'] >= 0.1: + if now - agg_buf['last_plot_time'] >= 0.1: agg_time = np.mean(agg_buf['time']) agg_voltage = np.mean(agg_buf['voltage']) agg_current = np.mean(agg_buf['current']) @@ -1282,12 +1315,12 @@ class BatteryTester(QMainWindow): 'time': [], 'voltage': [], 'current': [], 'count': 0, 'last_plot_time': now } - # UI-Labels aktualisieren + # Update UI labels self.voltage_label.setText(f"{voltage:.4f}") self.current_label.setText(f"{abs(current):.4f}") self.time_label.setText(self.format_time(current_time)) - # Kapazität berechnen + # Calculate capacity if we have enough data points if len(dev.time_data) > 1: delta_t = dev.time_data[-1] - dev.time_data[-2] power = voltage * abs(current) @@ -1296,7 +1329,7 @@ class BatteryTester(QMainWindow): self.capacity_label.setText(f"{dev.capacity_ah:.4f}") self.energy_label.setText(f"{dev.energy:.4f}") - # Plot nur alle 100ms aktualisieren + # Update plot periodically now = time.time() if not hasattr(self, '_last_plot_update'): self._last_plot_update = 0 @@ -1326,17 +1359,22 @@ class BatteryTester(QMainWindow): def update_status(self): """Update status information periodically""" - now = time.time() # Define 'now' at the start of the method + now = time.time() - if self.test_running or hasattr(self, 'record_button') and self.record_button.isChecked(): - if self.time_data: - current_time = self.time_data[-1] - if len(self.time_data) > 1: - delta_t = self.time_data[-1] - self.time_data[-2] + if not self.active_device: + return + + dev = self.active_device + + if self.test_running or (hasattr(self, 'record_button') and self.record_button.isChecked()): + if dev.time_data: + current_time = dev.time_data[-1] + if len(dev.time_data) > 1: + delta_t = dev.time_data[-1] - dev.time_data[-2] if delta_t > 0: - current_current = abs(self.current_data[-1]) - self.capacity_ah += current_current * delta_t / 3600 - self.capacity_label.setText(f"{self.capacity_ah:.4f}") + current_current = abs(dev.current_data[-1]) + dev.capacity_ah += current_current * delta_t / 3600 + self.capacity_label.setText(f"{dev.capacity_ah:.4f}") # Logging (1x per second) if (hasattr(self, 'log_writer') and @@ -1344,37 +1382,36 @@ class BatteryTester(QMainWindow): self.current_cycle_file is not None and not self.current_cycle_file.closed): - # Initialize last log time if not exists if not hasattr(self, '_last_log_time'): self._last_log_time = now - if self.time_data and (now - self._last_log_time >= 1.0): + if dev.time_data and (now - self._last_log_time >= 1.0): try: - current_time = self.time_data[-1] - voltage = self.voltage_data[-1] - current = self.current_data[-1] + current_time = dev.time_data[-1] + voltage = dev.voltage_data[-1] + current = dev.current_data[-1] if self.current_mode == "Cycle Test": self.log_writer.writerow([ f"{current_time:.4f}", 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}" + dev.test_phase, + f"{dev.capacity_ah:.4f}", + f"{dev.charge_capacity:.4f}", + f"{dev.coulomb_efficiency:.1f}", + f"{dev.cycle_count}" ]) else: self.log_writer.writerow([ f"{current_time:.4f}", f"{voltage:.6f}", f"{current:.6f}", - self.test_phase if hasattr(self, 'test_phase') else "Live", - f"{self.capacity_ah:.4f}", + dev.test_phase if hasattr(dev, 'test_phase') else "Live", + f"{dev.capacity_ah:.4f}", f"{voltage * current:.4f}", # Power - f"{self.energy:.4f}", # Energy - f"{self.cycle_count}" if hasattr(self, 'cycle_count') else "1" + f"{dev.energy:.4f}", # Energy + f"{dev.cycle_count}" if hasattr(dev, 'cycle_count') else "1" ]) self.current_cycle_file.flush() self._last_log_time = now @@ -1392,47 +1429,14 @@ class BatteryTester(QMainWindow): """Start the selected test mode using the active device""" if not self.active_device: QMessageBox.warning(self, "No Device", "No ADALM1000 device selected.") + self.toggle_button.setChecked(False) return dev_manager = self.active_device dev = dev_manager.dev # Clean up any previous test - if hasattr(self, 'test_sequence_worker') and self.test_sequence_worker is not None: - try: - self.test_sequence_worker.stop() - except: - pass - self.test_sequence_worker.deleteLater() - if hasattr(self, 'test_sequence_thread') and self.test_sequence_thread is not None: - self.test_sequence_thread.quit() - self.test_sequence_thread.wait(500) - self.test_sequence_thread.deleteLater() - del self.test_sequence_thread - - if hasattr(self, 'discharge_worker') and self.discharge_worker is not None: - try: - self.discharge_worker.stop() - except: - pass - self.discharge_worker.deleteLater() - if hasattr(self, 'discharge_thread') and self.discharge_thread is not None: - self.discharge_thread.quit() - self.discharge_thread.wait(500) - self.discharge_thread.deleteLater() - del self.discharge_thread - - if hasattr(self, 'charge_worker') and self.charge_worker is not None: - try: - self.charge_worker.stop() - except: - pass - self.charge_worker.deleteLater() - if hasattr(self, 'charge_thread') and self.charge_thread is not None: - self.charge_thread.quit() - self.charge_thread.wait(500) - self.charge_thread.deleteLater() - del self.charge_thread + self.cleanup_test_threads() # Reset test state for active device self.reset_test() @@ -1463,6 +1467,7 @@ class BatteryTester(QMainWindow): dev.channels['B'].constant(0) except Exception as e: QMessageBox.critical(self, "Device Error", f"Failed to reset device: {e}") + self.toggle_button.setChecked(False) return # Reset test variables @@ -1480,8 +1485,7 @@ class BatteryTester(QMainWindow): # Update UI self.phase_label.setText(dev_manager.test_phase) - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) + self.toggle_button.setText("STOP") # Get parameters from UI try: @@ -1495,7 +1499,7 @@ class BatteryTester(QMainWindow): self.stop_test() return - # Create log file (now includes device serial) + # Create log file if not self.create_cycle_log_file(): self.stop_test() return @@ -1667,8 +1671,8 @@ class BatteryTester(QMainWindow): self.test_phase = "Initial Discharge" self.phase_label.setText(self.test_phase) - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) + self.toggle_button.setChecked(True) + self.toggle_button.setText("STOP") self.status_bar.showMessage(f"Cycle test started | Current: {test_current:.4f}A") # Create log file @@ -1703,8 +1707,9 @@ class BatteryTester(QMainWindow): except Exception as e: QMessageBox.critical(self, "Error", str(e)) # Ensure buttons are in correct state if error occurs - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) + self.toggle_button.setChecked(False) + self.toggle_button.setText("START") + self.toggle_button.setEnabled(True) def start_discharge_test(self): """Start the battery discharge test""" @@ -1808,8 +1813,9 @@ class BatteryTester(QMainWindow): except Exception as e: QMessageBox.critical(self, "Error", str(e)) # Ensure buttons are in correct state if error occurs - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) + self.toggle_button.setChecked(False) + self.toggle_button.setText("START") + self.toggle_button.setEnabled(True) def start_charge_test(self): """Start the battery charge test""" @@ -1912,8 +1918,9 @@ class BatteryTester(QMainWindow): except Exception as e: QMessageBox.critical(self, "Error", str(e)) - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) + self.toggle_button.setChecked(False) + self.toggle_button.setText("START") + self.toggle_button.setEnabled(True) def start_live_monitoring(self): """Start live monitoring mode for the active device""" @@ -1949,6 +1956,9 @@ class BatteryTester(QMainWindow): self.stop_button.setEnabled(True) self.start_button.setEnabled(False) + # Hide the toggle button in Live mode + self.toggle_button.hide() + # Status self.status_bar.showMessage(f"Live monitoring started | Device: {dev.serial}") @@ -2101,8 +2111,14 @@ class BatteryTester(QMainWindow): self.active_device.test_phase = "Idle" self.phase_label.setText("Idle") - self.stop_button.setEnabled(False) - self.start_button.setEnabled(True) + # Reset button state + self.toggle_button.setChecked(False) + if self.current_mode == "Cycle Test": + self.toggle_button.setText("START CYCLE TEST") + elif self.current_mode == "Discharge Test": + self.toggle_button.setText("START DISCHARGE") + elif self.current_mode == "Charge Test": + self.toggle_button.setText("START CHARGE") if self.current_mode == "Live Monitoring": self.status_bar.showMessage("Live monitoring stopped") @@ -2226,8 +2242,9 @@ class BatteryTester(QMainWindow): # 7. Reset UI and state self.request_stop = False - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) + self.toggle_button.setChecked(False) + self.toggle_button.setText("START") + self.toggle_button.setEnabled(True) # 8. Show completion message if test wasn't stopped by user if not self.request_stop: @@ -2285,8 +2302,9 @@ class BatteryTester(QMainWindow): import traceback traceback.print_exc() # Ensure we don't leave the UI in a locked state - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) + self.toggle_button.setChecked(False) + self.toggle_button.setText("START") + self.toggle_button.setEnabled(True) self.status_bar.showMessage("Error during test finalization") def reset_plot(self): @@ -2580,7 +2598,7 @@ class BatteryTester(QMainWindow): self.connection_label.setText(f"Connected: {self.active_device.serial}") self.status_bar.showMessage("Successfully reconnected devices") self.status_light.setStyleSheet("background-color: green; border-radius: 10px;") - self.start_button.setEnabled(True) + self.toggle_button.setEnabled(True) return except DeviceDisconnectedError as e: @@ -2640,4 +2658,6 @@ if __name__ == "__main__": window.show() app.exec_() except Exception as e: + import traceback + traceback.print_exc() QMessageBox.critical(None, "Fatal Error", f"Application failed: {str(e)}") \ No newline at end of file