From a3c8ed7f7e9dd042cbe83c2a97532e7bb558493f Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 6 Aug 2025 18:32:22 +0200 Subject: [PATCH] MainCode/adalm1000_logger.py aktualisiert multible devices visible Qwen --- MainCode/adalm1000_logger.py | 983 ++++++++++++++++++++--------------- 1 file changed, 552 insertions(+), 431 deletions(-) diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py index a4a991f..bc9c5c8 100644 --- a/MainCode/adalm1000_logger.py +++ b/MainCode/adalm1000_logger.py @@ -32,6 +32,72 @@ class PatchedSession(pysmu.Session): else: raise +class DeviceManager: + def __init__(self, dev): + self.dev = dev + self.serial = dev.serial + self.measurement_thread = None + self.is_running = False + + # Datenpuffer + max_data_points = 36000 + self.time_data = deque(maxlen=max_data_points) + self.voltage_data = deque(maxlen=max_data_points) + self.current_data = deque(maxlen=max_data_points) + self.display_time_data = deque(maxlen=10000) + self.display_voltage_data = deque(maxlen=10000) + self.display_current_data = deque(maxlen=10000) + + # Testzustand + self.capacity_ah = 0.0 + self.energy = 0.0 + self.charge_capacity = 0.0 + self.coulomb_efficiency = 0.0 + self.cycle_count = 0 + self.test_phase = "Idle" + self.start_time = time.time() + + # Logging + self.current_cycle_file = None + self.log_writer = None + + # Downsampling + self.downsample_factor = 1 + self.aggregation_buffer = { + 'time': [], 'voltage': [], 'current': [], 'count': 0, 'last_plot_time': 0 + } + + def start_measurement(self, interval=0.1): + if self.measurement_thread: + self.measurement_thread.stop() + self.measurement_thread.wait(500) + self.measurement_thread = MeasurementThread(self.dev, interval) + 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) + self.is_running = False + + def reset_data(self): + self.time_data.clear() + self.voltage_data.clear() + self.current_data.clear() + self.display_time_data.clear() + self.display_voltage_data.clear() + self.display_current_data.clear() + self.aggregation_buffer = { + 'time': [], 'voltage': [], 'current': [], 'count': 0, 'last_plot_time': 0 + } + self.capacity_ah = 0.0 + self.energy = 0.0 + self.charge_capacity = 0.0 + self.coulomb_efficiency = 0.0 + self.cycle_count = 0 + self.start_time = time.time() + self.test_phase = "Idle" class DeviceDisconnectedError(Exception): pass @@ -42,7 +108,8 @@ class MeasurementThread(QThread): def __init__(self, device, interval=0.1): super().__init__() - self.device = device + self.devices = {} # serial -> DeviceManager + self.active_device = None self.interval = interval self._running = False self.filter_window_size = 10 @@ -429,7 +496,9 @@ class BatteryTester(QMainWindow): def __init__(self): self.plot_mutex = threading.Lock() super().__init__() - + + self.devices = {} # Dictionary DeviceManager-Instanzen + self.active_device = None self.last_logged_phase = None # Color scheme @@ -523,7 +592,7 @@ class BatteryTester(QMainWindow): mode_layout.addWidget(self.mode_combo, 1) # Device selection - self.device_label = QLabel("ADALM1000 Device:") + self.device_label = QLabel("Device:") self.device_label.setStyleSheet(f"color: {self.fg_color};") mode_layout.addWidget(self.device_label) @@ -849,56 +918,35 @@ class BatteryTester(QMainWindow): self.status_bar.showMessage(f"Mode changed to {mode_name}") def reset_test(self): - """Reset test state without stopping measurement""" - # Reset Downsampling - self.downsample_factor = 1 - self.downsample_counter = 0 - - # Clear all data buffers + """Reset test state for the active device""" + if not self.active_device: + return + + dev_manager = self.active_device + + # Reset data buffers with self.plot_mutex: - self.time_data.clear() - self.voltage_data.clear() - self.current_data.clear() - if hasattr(self, 'phase_data'): - self.phase_data.clear() - - # Also clear display buffers - if hasattr(self, 'display_time_data'): - self.display_time_data.clear() - self.display_voltage_data.clear() - self.display_current_data.clear() - - # Reset aggregation buffer - self.aggregation_buffer = { - 'time': [], 'voltage': [], 'current': [], - 'count': 0, 'last_plot_time': 0 - } - - # Clear measurement thread buffers if it exists - if hasattr(self, 'measurement_thread'): - self.measurement_thread.voltage_window.clear() - self.measurement_thread.current_window.clear() - with self.measurement_thread.measurement_queue.mutex: - self.measurement_thread.measurement_queue.queue.clear() - self.measurement_thread.start_time = time.time() - - # Reset capacities and timing - self.start_time = time.time() - self.last_update_time = self.start_time - self.capacity_ah = 0.0 - self.energy = 0.0 - if hasattr(self, 'charge_capacity'): - self.charge_capacity = 0.0 - if hasattr(self, 'coulomb_efficiency'): - self.coulomb_efficiency = 0.0 - - # Reset plot - self.reset_plot() - - # Update UI - self.phase_label.setText("Idle") - if hasattr(self, 'test_phase'): - self.test_phase = "Idle" + dev_manager.time_data.clear() + dev_manager.voltage_data.clear() + dev_manager.current_data.clear() + dev_manager.display_time_data.clear() + dev_manager.display_voltage_data.clear() + dev_manager.display_current_data.clear() + + # Reset state variables + dev_manager.capacity_ah = 0.0 + dev_manager.energy = 0.0 + dev_manager.charge_capacity = 0.0 + dev_manager.coulomb_efficiency = 0.0 + dev_manager.cycle_count = 0 + dev_manager.start_time = time.time() + dev_manager.test_phase = "Idle" + + # Reset UI + self.capacity_label.setText("0.0000") + self.energy_label.setText("0.0000") + self.cycle_label.setText("0") + self.phase_label.setText(dev_manager.test_phase) def toggle_recording(self): """Toggle data recording in Live Monitoring mode""" @@ -987,76 +1035,51 @@ class BatteryTester(QMainWindow): self.main_layout.addWidget(self.canvas, 1) def init_device(self): - """Initialize ADALM1000 with proper permission handling""" + """Initialize all ADALM1000 with proper permission handling""" try: - # Clean up previous session if exists + # Cleanup previous session if hasattr(self, 'session'): try: self.session.end() - except Exception as e: - print(f"Session cleanup error: {e}") + except: + pass del self.session - # Debug USB devices - print("Checking USB devices...") - usb_info = os.popen("lsusb -d 064b:784c -v").read() - print(usb_info) - - # Check if devices are present - if "064b:784c" not in usb_info: - raise DeviceDisconnectedError("No ADALM1000 devices detected") + self.session = PatchedSession(ignore_dataflow=True, queue_size=10000) - # Initialize new session with permission handling - print("Initializing new session...") - try: - # First try with normal permissions - self.session = pysmu.Session(ignore_dataflow=True, queue_size=10000) - except (OSError, pysmu.exceptions.SessionError) as e: - if "permission" in str(e).lower() or "not permitted" in str(e): - print("Permission error detected - requesting elevated privileges") - self.request_usb_permissions() - # Try again after permission request - self.session = pysmu.Session(ignore_dataflow=True, queue_size=10000) - else: - raise + if not self.session.devices: + self.status_bar.showMessage("No ADALM1000 devices found") + self.session_active = False + self.active_device = None # Sicherstellen, dass es None ist + return - # Get and store available devices _einmal_ - self.device_list = list(self.session.devices) - print(f"Found {len(self.device_list)} device(s) in session") - - if not self.device_list: - raise DeviceDisconnectedError("No ADALM1000 devices available") + self.devices.clear() + for dev in self.session.devices: + manager = DeviceManager(dev) + manager.start_measurement(interval=self.interval) + self.devices[dev.serial] = manager - # Use the first available device - self.dev = self.device_list[0] + # Erstes Gerät aktivieren + first_serial = self.session.devices[0].serial + self.active_device = self.devices[first_serial] - # Configure device - 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) - - # Start session - self.session.start(0) - self.session_active = True - - # Update UI - self.status_light.setStyleSheet("background-color: green; border-radius: 10px;") - self.connection_label.setText(f"Connected: {self.dev.serial}") + # UI aktualisieren self.device_combo.clear() - self.device_combo.addItem(self.dev.serial) - - # Start measurement thread - if hasattr(self, 'measurement_thread'): - self.measurement_thread.stop() - - self.measurement_thread = MeasurementThread(self.dev, self.interval) + for serial in self.devices: + self.device_combo.addItem(serial) + self.device_combo.setCurrentText(first_serial) + + 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) + + # Starte Mess-Update für aktives Gerät + 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) - self.measurement_thread.start() except Exception as e: - print(f"INIT ERROR DETAILS: {str(e)}") self.handle_device_error(str(e)) def request_usb_permissions(self): @@ -1132,150 +1155,119 @@ class BatteryTester(QMainWindow): print(f"Manual init failed: {e}") def change_device(self, index): - """Safely switch to another ADALM1000 device""" - if not self.session_active or index < 0 or index >= len(self.device_list): + if not self.session_active or index < 0: + return + serial = self.device_combo.itemText(index) + if serial not in self.devices: return - try: - # Stop current operations + # Stoppe aktuelle Messung (UI) + was_running = self.test_running + if was_running: self.stop_test() - - # Stop measurement thread - if hasattr(self, 'measurement_thread'): - self.measurement_thread.stop() - self.measurement_thread.wait(500) - # Switch to new device - self.dev = self.device_list[index] - - # Reconfigure channels - 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) + # Trenne alten Thread-Slot + if hasattr(self, 'measurement_thread'): + self.measurement_thread.update_signal.disconnect() - # Restart session for new device - try: - self.session.stop() - except Exception: - pass - self.session.start(index) + # Setze neues aktives Gerät + self.active_device = self.devices[serial] + self.measurement_thread = self.active_device.measurement_thread + self.measurement_thread.update_signal.connect(self.update_measurements) - # Update UI - self.device_combo.setCurrentIndex(index) - self.connection_label.setText(f"Connected: {self.dev.serial}") - self.status_bar.showMessage(f"Switched to device {self.dev.serial}") - - # Restart measurement - 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() + # Aktualisiere UI + self.connection_label.setText(f"Connected: {serial}") + self.status_bar.showMessage(f"Switched to device {serial}") - except Exception as e: - self.handle_device_error(f"Device switch failed: {str(e)}") + # Reset Plot + UI mit Daten des neuen Geräts + self.update_ui_from_active_device() + if was_running: + self.start_test() # Nur wenn vorher aktiv + + def update_ui_from_active_device(self): + dev = self.active_device + if not dev: + return + + with self.plot_mutex: + # Kopiere aktuelle Daten + x = list(dev.display_time_data) + y_v = list(dev.display_voltage_data) + y_c = list(dev.display_current_data) + + # Aktualisiere Plot + self.line_voltage.set_data(x, y_v) + self.line_current.set_data(x, y_c) + self.auto_scale_axes() + self.canvas.draw_idle() + + # Aktualisiere Labels + if dev.voltage_data: + v = dev.voltage_data[-1] + i = dev.current_data[-1] + t = dev.time_data[-1] if dev.time_data else 0 + self.voltage_label.setText(f"{v:.4f}") + self.current_label.setText(f"{abs(i):.4f}") + self.time_label.setText(self.format_time(t)) + self.capacity_label.setText(f"{dev.capacity_ah:.4f}") + self.energy_label.setText(f"{dev.energy:.4f}") + self.cycle_label.setText(str(dev.cycle_count)) + self.phase_label.setText(dev.test_phase) @pyqtSlot(float, float, float) def update_measurements(self, voltage, current, current_time): - try: - # Only store data if in a test or recording - if not (self.test_running or self.record_button.isChecked()): - return - - # 1. Originale Daten immer vollständig speichern (für Berechnungen und Logging) - with self.plot_mutex: - self.time_data.append(current_time) - self.voltage_data.append(voltage) - self.current_data.append(current) - - # 2. Downsampling für die Anzeige - if not hasattr(self, 'aggregation_buffer'): - self.aggregation_buffer = { - 'time': [], 'voltage': [], 'current': [], - 'count': 0, 'last_plot_time': 0 - } - - self.aggregation_buffer['time'].append(current_time) - self.aggregation_buffer['voltage'].append(voltage) - self.aggregation_buffer['current'].append(current) - self.aggregation_buffer['count'] += 1 - - # Nur aggregieren wenn genug Daten oder Zeit vergangen - now = time.time() - if (self.aggregation_buffer['count'] >= self.downsample_factor or - now - self.aggregation_buffer['last_plot_time'] >= 1.0): - - # Berechne aggregierte Werte (Mittelwert) - agg_time = np.mean(self.aggregation_buffer['time']) - agg_voltage = np.mean(self.aggregation_buffer['voltage']) - agg_current = np.mean(self.aggregation_buffer['current']) - - # Für die Anzeige verwenden - if not hasattr(self, 'display_time_data'): - self.display_time_data = deque(maxlen=self.max_points_to_keep) - self.display_voltage_data = deque(maxlen=self.max_points_to_keep) - self.display_current_data = deque(maxlen=self.max_points_to_keep) - - self.display_time_data.append(agg_time) - self.display_voltage_data.append(agg_voltage) - self.display_current_data.append(agg_current) - - # Reset Buffer - self.aggregation_buffer = { - 'time': [], 'voltage': [], 'current': [], - 'count': 0, 'last_plot_time': now - } - - # 3. Originale Funktionalität für Berechnungen beibehalten - self.voltage_label.setText(f"{voltage:.4f}") - self.current_label.setText(f"{abs(current):.4f}") - self.time_label.setText(self.format_time(current_time)) - - # Calculate and display power and energy - power = voltage * abs(current) - self.power_label.setText(f"{power:.4f}") - - if len(self.time_data) > 1: - delta_t = self.time_data[-1] - self.time_data[-2] - self.energy += power * delta_t / 3600 # Convert to Wh - self.energy_label.setText(f"{self.energy:.4f}") - - # 4. Auto-Skalierung anpassen - if len(self.time_data) > self.max_points_to_keep * 1.5: - self.adjust_downsampling() - - # 5. Plot updates throttled to 10Hz - if not hasattr(self, '_last_plot_update'): - self._last_plot_update = 0 - - if now - self._last_plot_update >= 0.1: - self._last_plot_update = now - QTimer.singleShot(0, self.update_plot) - - except Exception as e: - print(f"Error in update_measurements: {e}") - import traceback - traceback.print_exc() - # Versuche den Aggregationsbuffer zu retten - if hasattr(self, 'aggregation_buffer'): - agg_buffer = self.aggregation_buffer - if agg_buffer['count'] > 0: - try: - with self.plot_mutex: - if not hasattr(self, 'display_time_data'): - self.display_time_data = deque(maxlen=self.max_points_to_keep) - self.display_voltage_data = deque(maxlen=self.max_points_to_keep) - self.display_current_data = deque(maxlen=self.max_points_to_keep) - - self.display_time_data.append(np.mean(agg_buffer['time'])) - self.display_voltage_data.append(np.mean(agg_buffer['voltage'])) - self.display_current_data.append(np.mean(agg_buffer['current'])) - except: - pass - self.aggregation_buffer = { - 'time': [], 'voltage': [], 'current': [], - 'count': 0, 'last_plot_time': time.time() - } + if not self.active_device: + return + + dev = self.active_device + + with self.plot_mutex: + dev.time_data.append(current_time) + dev.voltage_data.append(voltage) + dev.current_data.append(current) + + # Aggregation für Anzeige (wie bisher) + agg_buf = dev.aggregation_buffer + agg_buf['time'].append(current_time) + agg_buf['voltage'].append(voltage) + agg_buf['current'].append(current) + agg_buf['count'] += 1 + + now = time.time() + if agg_buf['count'] >= dev.downsample_factor or now - agg_buf['last_plot_time'] >= 1.0: + agg_time = np.mean(agg_buf['time']) + agg_voltage = np.mean(agg_buf['voltage']) + agg_current = np.mean(agg_buf['current']) + + dev.display_time_data.append(agg_time) + dev.display_voltage_data.append(agg_voltage) + dev.display_current_data.append(agg_current) + + dev.aggregation_buffer = { + 'time': [], 'voltage': [], 'current': [], 'count': 0, 'last_plot_time': now + } + + # UI-Labels aktualisieren + 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 + if len(dev.time_data) > 1: + delta_t = dev.time_data[-1] - dev.time_data[-2] + power = voltage * abs(current) + dev.capacity_ah += abs(current) * delta_t / 3600 + dev.energy += power * delta_t / 3600 + self.capacity_label.setText(f"{dev.capacity_ah:.4f}") + self.energy_label.setText(f"{dev.energy:.4f}") + + # Plot nur alle 100ms aktualisieren + now = time.time() + if not hasattr(self, '_last_plot_update'): + self._last_plot_update = 0 + if now - self._last_plot_update >= 0.1: + self._last_plot_update = now + QTimer.singleShot(0, self.update_plot) def adjust_downsampling(self): current_length = len(self.time_data) @@ -1362,13 +1354,203 @@ class BatteryTester(QMainWindow): self.current_cycle_file = None def start_test(self): - """Start the selected test mode""" + """Start the selected test mode using the active device""" + if not self.active_device: + QMessageBox.warning(self, "No Device", "No ADALM1000 device selected.") + 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 + + # Reset test state for active device + self.reset_test() + + # Reset measurement thread timing + dev_manager.measurement_thread.start_time = time.time() + dev_manager.measurement_thread.voltage_window.clear() + dev_manager.measurement_thread.current_window.clear() + with dev_manager.measurement_thread.measurement_queue.mutex: + dev_manager.measurement_thread.measurement_queue.queue.clear() + + # Reset data buffers + dev_manager.time_data.clear() + dev_manager.voltage_data.clear() + dev_manager.current_data.clear() + dev_manager.display_time_data.clear() + dev_manager.display_voltage_data.clear() + dev_manager.display_current_data.clear() + dev_manager.aggregation_buffer = { + 'time': [], 'voltage': [], 'current': [], 'count': 0, 'last_plot_time': 0 + } + + # Reset device state + try: + dev.channels['A'].mode = pysmu.Mode.HI_Z + dev.channels['A'].constant(0) + dev.channels['B'].mode = pysmu.Mode.HI_Z + except Exception as e: + QMessageBox.critical(self, "Device Error", f"Failed to reset device: {e}") + return + + # Reset test variables + dev_manager.capacity_ah = 0.0 + dev_manager.energy = 0.0 + dev_manager.charge_capacity = 0.0 + dev_manager.coulomb_efficiency = 0.0 + dev_manager.cycle_count = 0 + dev_manager.start_time = time.time() + dev_manager.test_phase = "Running" + + # Set global state + self.test_running = True + self.request_stop = False + + # Update UI + self.phase_label.setText(dev_manager.test_phase) + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) + + # Get parameters from UI + try: + self.capacity = float(self.capacity_input.text()) + self.c_rate = float(self.c_rate_input.text()) + test_current = self.c_rate * self.capacity + if test_current > 0.2: + raise ValueError("Current must be ≤ 200mA (0.2A) for ADALM1000") + except ValueError as e: + QMessageBox.critical(self, "Input Error", str(e)) + self.stop_test() + return + + # Create log file (now includes device serial) + if not self.create_cycle_log_file(): + self.stop_test() + return + + # Start the appropriate test if self.current_mode == "Cycle Test": - self.start_cycle_test() + try: + self.charge_cutoff = float(self.charge_cutoff_input.text()) + self.discharge_cutoff = float(self.discharge_cutoff_input.text()) + self.rest_time = float(self.rest_time_input.text()) + if self.charge_cutoff <= self.discharge_cutoff: + raise ValueError("Charge cutoff must be higher than discharge cutoff") + + # Start test sequence + self.test_sequence_thread = QThread() + self.test_sequence_worker = TestSequenceWorker( + dev, + test_current, + self.charge_cutoff, + self.discharge_cutoff, + self.rest_time, + self.continuous_mode_check.isChecked(), + self + ) + self.test_sequence_worker.moveToThread(self.test_sequence_thread) + + self.test_sequence_worker.update_phase.connect(self.update_test_phase) + self.test_sequence_worker.update_status.connect(self.status_bar.showMessage) + self.test_sequence_worker.test_completed.connect(self.finalize_test) + self.test_sequence_worker.error_occurred.connect(self.handle_test_error) + self.test_sequence_worker.finished.connect(self.test_sequence_thread.quit) + self.test_sequence_worker.finished.connect(self.test_sequence_worker.deleteLater) + self.test_sequence_thread.finished.connect(self.test_sequence_thread.deleteLater) + + self.test_sequence_thread.start() + QTimer.singleShot(0, self.test_sequence_worker.run) + + self.status_bar.showMessage(f"Cycle test started | Device: {dev.serial} | Current: {test_current:.4f}A") + + except Exception as e: + QMessageBox.critical(self, "Error", str(e)) + self.stop_test() + elif self.current_mode == "Discharge Test": - self.start_discharge_test() + try: + self.discharge_cutoff = float(self.discharge_cutoff_input.text()) + + self.discharge_thread = QThread() + self.discharge_worker = DischargeWorker(dev, test_current, self.discharge_cutoff, self) + self.discharge_worker.moveToThread(self.discharge_thread) + + self.discharge_worker.update_status.connect(self.status_bar.showMessage) + self.discharge_worker.test_completed.connect(self.finalize_test) + self.discharge_worker.error_occurred.connect(self.handle_test_error) + self.discharge_worker.finished.connect(self.discharge_thread.quit) + self.discharge_worker.finished.connect(self.discharge_worker.deleteLater) + self.discharge_thread.finished.connect(self.discharge_thread.deleteLater) + + self.discharge_thread.start() + QTimer.singleShot(0, self.discharge_worker.run) + + self.status_bar.showMessage(f"Discharge test started | Device: {dev.serial} | Current: {test_current:.4f}A") + + except Exception as e: + QMessageBox.critical(self, "Error", str(e)) + self.stop_test() + elif self.current_mode == "Charge Test": - self.start_charge_test() + try: + self.charge_cutoff = float(self.charge_cutoff_input.text()) + + self.charge_thread = QThread() + self.charge_worker = ChargeWorker(dev, test_current, self.charge_cutoff, self) + self.charge_worker.moveToThread(self.charge_thread) + + self.charge_worker.update_status.connect(self.status_bar.showMessage) + self.charge_worker.test_completed.connect(self.finalize_test) + self.charge_worker.error_occurred.connect(self.handle_test_error) + self.charge_worker.finished.connect(self.charge_thread.quit) + self.charge_worker.finished.connect(self.charge_worker.deleteLater) + self.charge_thread.finished.connect(self.charge_thread.deleteLater) + + self.charge_thread.start() + QTimer.singleShot(0, self.charge_worker.run) + + self.status_bar.showMessage(f"Charge test started | Device: {dev.serial} | Current: {test_current:.4f}A") + + except Exception as e: + QMessageBox.critical(self, "Error", str(e)) + self.stop_test() + elif self.current_mode == "Live Monitoring": self.start_live_monitoring() @@ -1459,7 +1641,7 @@ class BatteryTester(QMainWindow): # Start test sequence in a QThread self.test_sequence_thread = QThread() self.test_sequence_worker = TestSequenceWorker( - self.dev, + self.active_device.dev, test_current, self.charge_cutoff, self.discharge_cutoff, @@ -1568,7 +1750,7 @@ class BatteryTester(QMainWindow): # Start discharge worker in a QThread self.discharge_thread = QThread() self.discharge_worker = DischargeWorker( - self.dev, + self.active_device.dev, test_current, self.discharge_cutoff, self # Pass reference to main window for callbacks @@ -1673,7 +1855,7 @@ class BatteryTester(QMainWindow): # Start charge worker in a QThread self.charge_thread = QThread() self.charge_worker = ChargeWorker( - self.dev, + self.active_device.dev, test_current, self.charge_cutoff, self @@ -1698,94 +1880,99 @@ class BatteryTester(QMainWindow): self.stop_button.setEnabled(False) def start_live_monitoring(self): - """Start live monitoring mode""" - try: - # Reset everything completely - self.reset_test() - - # Reset measurement timing - if hasattr(self, 'measurement_thread'): - self.measurement_thread.start_time = time.time() + """Start live monitoring mode for the active device""" + if not self.active_device: + QMessageBox.warning(self, "No Device", "No ADALM1000 selected.") + return - # Set monitoring flags + dev_manager = self.active_device + dev = dev_manager.dev + + try: + # Reset test state for active device + self.reset_test() + + # Reset measurement thread timing + if hasattr(dev_manager, 'measurement_thread'): + dev_manager.measurement_thread.start_time = time.time() + dev_manager.measurement_thread.voltage_window.clear() + dev_manager.measurement_thread.current_window.clear() + with dev_manager.measurement_thread.measurement_queue.mutex: + dev_manager.measurement_thread.measurement_queue.queue.clear() + + # Reset device state + dev.channels['A'].mode = pysmu.Mode.HI_Z + dev.channels['A'].constant(0) + dev.channels['B'].mode = pysmu.Mode.HI_Z + + # Reset UI self.test_running = True - self.test_phase = "Live Monitoring" - self.phase_label.setText(self.test_phase) - - # Update UI + dev_manager.test_phase = "Live Monitoring" + self.phase_label.setText(dev_manager.test_phase) self.stop_button.setEnabled(True) self.start_button.setEnabled(False) - - # Configure device for monitoring - if hasattr(self, 'dev'): - try: - self.dev.channels['A'].mode = pysmu.Mode.HI_Z - self.dev.channels['A'].constant(0) - self.dev.channels['B'].mode = pysmu.Mode.HI_Z - except Exception as e: - print(f"Error configuring device for monitoring: {e}") - - self.status_bar.showMessage("Live monitoring started") - except Exception as e: - print(f"Error starting live monitoring: {e}") - self.test_running = False - QMessageBox.critical(self, "Error", f"Failed to start monitoring:\n{str(e)}") - def create_cycle_log_file(self): - """Create a new log file for the current test""" + # Status + self.status_bar.showMessage(f"Live monitoring started | Device: {dev.serial}") + + except Exception as e: + self.handle_device_error(f"Failed to start live monitoring: {str(e)}") + + def create_cycle_log_file(self): + """Create a new log file for the current test with device serial in filename""" try: self._last_log_time = time.time() - # Close previous file if exists + + # Schließe vorherige Datei if hasattr(self, 'current_cycle_file') and self.current_cycle_file: try: self.current_cycle_file.close() except Exception as e: print(f"Error closing previous log file: {e}") - - # Ensure log directory exists + self.current_cycle_file = None + + # Stelle sicher, dass Log-Ordner existiert os.makedirs(self.log_dir, exist_ok=True) - if not os.access(self.log_dir, os.W_OK): QMessageBox.critical(self, "Error", f"No write permissions in {self.log_dir}") return False - - # Generate filename based on mode + + # Generiere Timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + device_serial = self.active_device.serial if self.active_device else "unknown" + + # Generiere Dateinamen mit Seriennummer if self.current_mode == "Cycle Test": - self.filename = os.path.join(self.log_dir, f"battery_cycle_{timestamp}.csv") + self.filename = os.path.join(self.log_dir, f"battery_cycle_{device_serial}_{timestamp}.csv") elif self.current_mode == "Discharge Test": - self.filename = os.path.join(self.log_dir, f"battery_discharge_{timestamp}.csv") + self.filename = os.path.join(self.log_dir, f"battery_discharge_{device_serial}_{timestamp}.csv") else: # Live Monitoring - self.filename = os.path.join(self.log_dir, f"battery_live_{timestamp}.csv") - - # Open new file + self.filename = os.path.join(self.log_dir, f"battery_live_{device_serial}_{timestamp}.csv") + + # Öffne neue Datei try: self.current_cycle_file = open(self.filename, 'w', newline='') - - # Write header with test parameters test_current = self.c_rate * self.capacity test_conditions = self.test_conditions_input.text() if hasattr(self, 'test_conditions_input') else "N/A" - + + # Schreibe Header self.current_cycle_file.write(f"# ADALM1000 Battery Test Log - {self.current_mode}\n") self.current_cycle_file.write(f"# Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + self.current_cycle_file.write(f"# Device Serial: {device_serial}\n") self.current_cycle_file.write(f"# Battery Capacity: {self.capacity} Ah\n") - if self.current_mode != "Live Monitoring": self.current_cycle_file.write(f"# Test Current: {test_current:.4f} A (C/{1/self.c_rate:.1f})\n") - if self.current_mode == "Cycle Test": self.current_cycle_file.write(f"# Charge Cutoff: {self.charge_cutoff} V\n") self.current_cycle_file.write(f"# Discharge Cutoff: {self.discharge_cutoff} V\n") self.current_cycle_file.write(f"# Rest Time: {self.rest_time} hours\n") elif self.current_mode == "Discharge Test": self.current_cycle_file.write(f"# Discharge Cutoff: {self.discharge_cutoff} V\n") - self.current_cycle_file.write(f"# Test Conditions/Chemistry: {test_conditions}\n") self.current_cycle_file.write("#\n") - - # Write data header + + # CSV Writer self.log_writer = csv.writer(self.current_cycle_file) - if self.current_mode == "Cycle Test": self.log_writer.writerow([ "Time(s)", "Voltage(V)", "Current(A)", "Phase", @@ -1798,9 +1985,11 @@ class BatteryTester(QMainWindow): "Capacity(Ah)", "Power(W)", "Energy(Wh)", "Cycle" ]) return True + except Exception as e: QMessageBox.critical(self, "Error", f"Failed to create log file: {e}") return False + except Exception as e: print(f"Error in create_cycle_log_file: {e}") return False @@ -1853,73 +2042,35 @@ class BatteryTester(QMainWindow): def stop_test(self): """Request immediate stop of the current test or monitoring""" - if not self.test_running and not (hasattr(self, 'record_button') and self.record_button.isChecked()): + if not self.test_running: return self.request_stop = True self.test_running = False self.measuring = False - - # Stop any active test threads - if hasattr(self, 'test_sequence_worker'): - try: - if not sip.isdeleted(self.test_sequence_worker): - self.test_sequence_worker.stop() - except: - pass - - if hasattr(self, 'discharge_worker'): - try: - if not sip.isdeleted(self.discharge_worker): - self.discharge_worker.stop() - except: - pass - # Stop recording if active - if hasattr(self, 'record_button') and self.record_button.isChecked(): - self.record_button.setChecked(False) - if hasattr(self, 'current_cycle_file') and self.current_cycle_file is not None: - self.finalize_log_file() - self.record_button.setText("Start Recording") - - # Reset device to safe state - if hasattr(self, 'dev'): - try: - self.dev.channels['A'].mode = pysmu.Mode.HI_Z - self.dev.channels['A'].constant(0) - self.dev.channels['B'].mode = pysmu.Mode.HI_Z - except Exception as e: - print(f"Error resetting device: {e}") + # Stop test threads + for attr in ['test_sequence_worker', 'discharge_worker', 'charge_worker']: + if hasattr(self, attr): + worker = getattr(self, attr) + try: + if worker and not sip.isdeleted(worker): + worker.stop() + except: + pass - # Clear all data buffers - with self.plot_mutex: - self.time_data.clear() - self.voltage_data.clear() - self.current_data.clear() - if hasattr(self, 'phase_data'): - self.phase_data.clear() + # Reset UI + if self.active_device: + self.active_device.test_phase = "Idle" + self.phase_label.setText("Idle") - # Reset measurements - self.capacity_ah = 0.0 - self.energy = 0.0 - if hasattr(self, 'charge_capacity'): - self.charge_capacity = 0.0 - if hasattr(self, 'coulomb_efficiency'): - self.coulomb_efficiency = 0.0 - - # Reset plot - self.reset_plot() - - # Update UI - self.test_phase = "Idle" - self.phase_label.setText(self.test_phase) self.stop_button.setEnabled(False) self.start_button.setEnabled(True) - + if self.current_mode == "Live Monitoring": self.status_bar.showMessage("Live monitoring stopped") else: - self.status_bar.showMessage("Test stopped - Ready for new test") + self.status_bar.showMessage("Test stopped by user") def finalize_test(self): """Final cleanup after test completes or is stopped""" @@ -1929,13 +2080,12 @@ class BatteryTester(QMainWindow): self.test_running = False # 2. Reset device to safe state - if hasattr(self, 'dev'): - try: - self.dev.channels['A'].mode = pysmu.Mode.HI_Z - self.dev.channels['A'].constant(0) - self.dev.channels['B'].mode = pysmu.Mode.HI_Z - except Exception as e: - print(f"Error resetting device in finalize: {e}") + try: + self.active_device.dev.channels['A'].mode = pysmu.Mode.HI_Z + self.active_device.dev.channels['A'].constant(0) + self.active_device.dev.channels['B'].mode = pysmu.Mode.HI_Z + except Exception as e: + print(f"Error resetting device in finalize: {e}") # 3. Clean up test sequence thread safely if hasattr(self, 'test_sequence_thread'): @@ -2199,35 +2349,25 @@ class BatteryTester(QMainWindow): self.ax2.set_ylim(min_current, max_current) @pyqtSlot(str) - def handle_device_error(self, error): - """Handle device connection errors""" - error_msg = str(error) - print(f"Device error: {error_msg}") - - if hasattr(self, 'session'): - try: - if self.session_active: - self.session.end() - del self.session - except Exception as e: - print(f"Error cleaning up session: {e}") - - self.status_light.setStyleSheet(f"background-color: red; border-radius: 10px;") - self.connection_label.setText("Disconnected") + def handle_device_error(self, error_msg): + """Handle device errors gracefully""" self.status_bar.showMessage(f"Device error: {error_msg}") - + self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") self.session_active = False self.test_running = False - self.continuous_mode = False self.measuring = False - self.start_button.setEnabled(False) self.stop_button.setEnabled(False) - - self.time_data.clear() - self.voltage_data.clear() - self.current_data.clear() - + + # Clear device-specific data ONLY if we have an active device + if self.active_device: + data = self.active_device + with self.plot_mutex: + data.time_data.clear() + data.voltage_data.clear() + data.current_data.clear() + self.update_plot() + @pyqtSlot(str) def update_test_phase(self, phase_text): """Update the test phase display""" @@ -2283,70 +2423,51 @@ class BatteryTester(QMainWindow): QTimer.singleShot(1000, self.reconnect_device) def reconnect_device(self): - """Robust reconnection handler with device persistence""" - self.status_bar.showMessage("Reconnecting...") - - # Remember current selection - current_serial = self.dev.serial if hasattr(self, 'dev') else None - - # Cleanup existing connection - if hasattr(self, 'session'): - try: - self.session.end() - except Exception as e: - print(f"Error ending session: {e}") - if hasattr(self, 'measurement_thread'): self.measurement_thread.stop() self.measurement_thread.wait(500) - time.sleep(1) # Allow for USB reinitialization - try: - # Reinitialize session self.session = PatchedSession(ignore_dataflow=True, queue_size=10000) if not self.session.devices: - raise DeviceDisconnectedError("No devices available") + raise DeviceDisconnectedError("No devices found") - # Repopulate device list - self.device_combo.clear() + new_serials = {dev.serial for dev in self.session.devices} + old_serials = set(self.devices.keys()) + + # Neue hinzufügen for dev in self.session.devices: - self.device_combo.addItem(dev.serial) + if dev.serial not in self.devices: + manager = DeviceManager(dev) + manager.start_measurement(self.interval) + self.devices[dev.serial] = manager - # Try to reselect previous device - target_index = 0 - if current_serial: - for i, dev in enumerate(self.session.devices): - if dev.serial == current_serial: - target_index = i - break + # Entfernte entfernen + for serial in list(self.devices.keys()): + if serial not in new_serials: + self.devices[serial].stop_measurement() + del self.devices[serial] - self.dev = self.session.devices[target_index] - self.device_combo.setCurrentIndex(target_index) + # Aktualisiere UI + current_serial = self.active_device.serial if self.active_device else None + self.device_combo.clear() + for serial in self.devices: + self.device_combo.addItem(serial) - # Configure and start - 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) - - device_index = self.session.devices.index(self.dev) - self.session.start(device_index) + if current_serial in self.devices: + self.device_combo.setCurrentText(current_serial) + self.active_device = self.devices[current_serial] + else: + first = next(iter(self.devices)) + self.device_combo.setCurrentText(first) + self.active_device = self.devices[first] - # Update UI - self.status_light.setStyleSheet("background-color: green; border-radius: 10px;") - self.connection_label.setText(f"Reconnected: {self.dev.serial}") - self.status_bar.showMessage(f"Device {self.dev.serial} ready") - self.session_active = True - - # Restart measurement - self.measurement_thread = MeasurementThread(self.dev, self.interval) - self.measurement_thread.update_signal.connect(lambda v, c, t, s: self.update_measurements(v, c, t) if s == self.current_device_serial else None) - self.measurement_thread.error_signal.connect(lambda e, s: self.handle_device_error(e) if s == self.current_device_serial else None) - self.measurement_thread.start() + self.measurement_thread = self.active_device.measurement_thread + self.measurement_thread.update_signal.connect(self.update_measurements) + self.connection_label.setText(f"Reconnected: {self.active_device.serial}") + self.status_bar.showMessage("Reconnected all devices") except Exception as e: - self.status_bar.showMessage("Reconnect failed - retrying...") QTimer.singleShot(2000, self.reconnect_device) def closeEvent(self, event):