diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py index 7094fd9..2a23d1d 100644 --- a/MainCode/adalm1000_logger.py +++ b/MainCode/adalm1000_logger.py @@ -7,6 +7,7 @@ import traceback from datetime import datetime import numpy as np import matplotlib +import subprocess matplotlib.use('Qt5Agg') from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure @@ -34,6 +35,7 @@ class DeviceManager: self._last_log_time = 0 self.log_dir = os.path.expanduser("~/adalm1000/logs") os.makedirs(self.log_dir, exist_ok=True) + self.session = None # Datenpuffer max_data_points = 36000 @@ -52,6 +54,7 @@ class DeviceManager: self.cycle_count = 0 self.test_phase = "Idle" self.start_time = time.time() + self.plot_mutex = threading.Lock() # Logging self.current_cycle_file = None @@ -78,40 +81,110 @@ class DeviceManager: if self.consecutive_read_errors >= self.max_consecutive_errors: try: - print("Attempting device recovery...") + print(f"Attempting device recovery (errors: {self.consecutive_read_errors})...") # 1. First try soft reset try: if hasattr(self.dev, 'reset'): + print("Attempting soft reset...") self.dev.reset() - time.sleep(0.5) - return True + time.sleep(1.5) # Increased delay for reset to complete + + # Verify reset worked + try: + samples = self.dev.read(1, 500, True) + if samples: + self.consecutive_read_errors = 0 + print("Soft reset successful") + return True + except Exception as e: + print(f"Verification after soft reset failed: {e}") except Exception as e: print(f"Soft reset failed: {e}") - # 2. Full reinitialization - global_session = pysmu.Session() - devices = global_session.devices - - if not devices: - print("No devices found after rescan") + # 2. Full reinitialization with USB reset + print("Attempting full USB reset...") + try: + # Close existing session cleanly + if hasattr(self, 'session') and self.session: + try: + print("Ending existing session...") + self.session.end() + time.sleep(0.5) + except Exception as e: + print(f"Error ending session: {e}") + + # Add delay and attempt USB reset + time.sleep(2.0) # Longer delay for USB to settle + + # Try multiple times to scan for devices + for attempt in range(3): + try: + print(f"Scan attempt {attempt + 1}/3...") + new_session = pysmu.Session(ignore_dataflow=True, queue_size=10000) + devices = new_session.scan() + + if not devices: + print(f"No devices found on attempt {attempt+1}/3") + time.sleep(1.0) + continue + + # Find our device by serial + for new_dev in devices: + if new_dev.serial == self.serial: + print(f"Found device {self.serial} on attempt {attempt+1}") + self.dev = new_dev + self.consecutive_read_errors = 0 + self.session = new_session # Update session reference + + # Restart measurement thread + if hasattr(self, 'measurement_thread'): + try: + self.measurement_thread.stop() + time.sleep(0.5) + except: + pass + self.start_measurement(self.interval) + + print("Device reinitialized successfully") + return True + + print(f"Original device not found on attempt {attempt+1}") + time.sleep(1.0) + + except Exception as e: + print(f"Scan attempt {attempt+1} failed: {e}") + time.sleep(1.0) + + print("Failed to find original device after 3 attempts") return False - # Find our device by serial - for new_dev in devices: - if new_dev.serial == self.serial: - self.dev = new_dev - self.consecutive_read_errors = 0 - print("Device reinitialized successfully") - return True - - print("Original device not found after rescan") - return False - + except Exception as e: + print(f"Full reinitialization failed: {e}") + return False + + # 3. Final fallback - try USB reset command + try: + print("Attempting USB port reset...") + import subprocess + # ADALM1000 USB IDs + result = subprocess.run(['usbreset', '064b:784c'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + if result.returncode == 0: + print("USB reset command executed, waiting 3 seconds...") + time.sleep(3.0) + return self.handle_read_error(0) # Retry with counter reset + else: + print("USB reset failed - command not found or permission denied") + except Exception as e: + print(f"USB reset command failed: {e}") + except Exception as e: print(f"Device recovery failed: {e}") return False - return True + + return True # Not enough errors yet to trigger recovery def start_measurement(self, interval=0.1): self.stop_measurement() # Ensure any existing thread is stopped @@ -226,7 +299,7 @@ class MeasurementThread(QThread): current = np.mean(self.current_window) # Validate measurements - if not (0.0 <= voltage <= 5.0): + if not (-0.2 <= voltage <= 5.0): raise ValueError(f"Voltage out of range: {voltage:.4f}V") if not (-0.25 <= current <= 0.25): raise ValueError(f"Current out of range: {current:.4f}A") @@ -470,10 +543,11 @@ class DischargeWorker(QObject): continue # Update parent's data for logging/display - with self.parent.plot_mutex: - if len(self.parent.voltage_data) > 0: - self.parent.voltage_data[-1] = voltage - self.parent.current_data[-1] = current + if self.parent.active_device: + with self.parent.active_device.plot_mutex: + if self.parent.active_device.voltage_data: + self.parent.active_device.voltage_data[-1] = voltage + self.parent.active_device.current_data[-1] = current if voltage <= self.discharge_cutoff: break @@ -543,10 +617,11 @@ class ChargeWorker(QObject): continue # Update parent's data for logging/display - with self.parent.plot_mutex: - if len(self.parent.voltage_data) > 0: - self.parent.voltage_data[-1] = voltage - self.parent.current_data[-1] = current + if self.parent.active_device: + with self.parent.active_device.plot_mutex: + if self.parent.active_device.voltage_data: + self.parent.active_device.voltage_data[-1] = voltage + self.parent.active_device.current_data[-1] = current if voltage >= self.charge_cutoff: break @@ -919,9 +994,14 @@ class BatteryTester(QMainWindow): border-radius: 4px; min-height: 28px; background-color: {self.accent_color}; + color: {self.fg_color}; }} QPushButton:checked {{ - background-color: {self.warning_color}; + background-color: {self.warning_color} !important; + color: {self.fg_color} !important; + }} + QPushButton:disabled {{ + background-color: #4C566A; }} """ @@ -973,6 +1053,49 @@ class BatteryTester(QMainWindow): }} """) + def apply_button_style(self): + """Apply consistent button styling based on current state""" + if self.toggle_button.isChecked(): + # Stop state + self.toggle_button.setStyleSheet(f""" + QPushButton {{ + background-color: {self.warning_color}; + color: white; + font-weight: bold; + border: none; + border-radius: 4px; + padding: 4px 8px; + min-height: 28px; + }} + QPushButton:hover {{ + background-color: #{self.darker_color(self.warning_color)}; + }} + """) + else: + # Start state + self.toggle_button.setStyleSheet(f""" + QPushButton {{ + background-color: {self.accent_color}; + color: {self.fg_color}; + font-weight: bold; + border: none; + border-radius: 4px; + padding: 4px 8px; + min-height: 28px; + }} + QPushButton:hover {{ + background-color: #{self.darker_color(self.accent_color)}; + }} + """) + + def darker_color(self, hex_color): + """Helper to generate a darker shade for hover effects""" + if not hex_color.startswith('#'): + hex_color = '#' + hex_color + rgb = [int(hex_color[i:i+2], 16) for i in (1, 3, 5)] + darker = [max(0, c - 40) for c in rgb] + return ''.join([f"{c:02x}" for c in darker]) + def toggle_global_recording(self): """Toggle recording for all connected devices simultaneously""" if not hasattr(self, 'global_recording'): @@ -1006,15 +1129,23 @@ class BatteryTester(QMainWindow): self.record_button.setStyleSheet(f"background-color: {self.success_color};") self.status_bar.showMessage("Recording stopped for all devices") + def safe_execute(func): + """Decorator to catch and log exceptions in Qt event handlers""" + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception as e: + print(f"Error in {func.__name__}: {str(e)}") + traceback.print_exc() + return wrapper + + @safe_execute 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.apply_button_style() # Add this line self.start_test() else: - # Button shows "START" - run stop logic - self.toggle_button.setText("START") + self.apply_button_style() # Add this line self.stop_test() def change_mode(self, mode_name): @@ -1056,6 +1187,7 @@ class BatteryTester(QMainWindow): # Reset button state self.toggle_button.setChecked(False) self.toggle_button.setEnabled(True) + self.apply_button_style() # Reset measurement state and zero the time if self.active_device: @@ -1071,7 +1203,7 @@ class BatteryTester(QMainWindow): self.energy_label.setText("0.0000") self.cycle_label.setText("0") self.phase_label.setText("Idle") - self.time_label.setText("00:00:00") + #self.time_label.setText("00:00:00") # Reset plot self.reset_plot() @@ -1090,6 +1222,9 @@ class BatteryTester(QMainWindow): else: self.record_button.setVisible(False) + self.apply_button_style() + self.status_bar.showMessage(f"Mode changed to {mode_name}") + def reset_test(self): if not self.active_device: return @@ -1399,6 +1534,7 @@ class BatteryTester(QMainWindow): # Start measurement only AFTER connecting signals if not self.measurement_thread.isRunning(): self.active_device.start_measurement(self.interval) + print("Measurement thread running?", self.measurement_thread.isRunning()) except Exception as e: print(f"Error connecting to new device: {e}") return @@ -1441,6 +1577,7 @@ class BatteryTester(QMainWindow): self.cycle_label.setText(str(dev.cycle_count)) self.phase_label.setText(dev.test_phase) + @safe_execute @pyqtSlot(float, float, float) def update_measurements(self, voltage, current, current_time): if not self.active_device: @@ -1551,7 +1688,13 @@ class BatteryTester(QMainWindow): dev = self.active_device self.capacity_label.setText(f"{dev.capacity_ah:.4f}") self.energy_label.setText(f"{dev.energy:.4f}") + + # Update elapsed-time display + if self.active_device and self.active_device.time_data: + elapsed = self.active_device.time_data[-1] + self.time_label.setText(self.format_time(elapsed)) + @safe_execute def start_test(self): """Start the selected test mode using the active device""" if not self.active_device: @@ -1574,6 +1717,7 @@ class BatteryTester(QMainWindow): dev_manager.measurement_thread.current_window.clear() with dev_manager.measurement_thread.measurement_queue.mutex: dev_manager.measurement_thread.measurement_queue.queue.clear() + self.time_label.setText("00:00:00") # Reset data buffers dev_manager.time_data.clear() @@ -1613,6 +1757,7 @@ class BatteryTester(QMainWindow): # Update UI self.phase_label.setText(dev_manager.test_phase) self.toggle_button.setText("STOP") + self.apply_button_style() # Get parameters from UI try: @@ -1801,6 +1946,7 @@ class BatteryTester(QMainWindow): self.toggle_button.setChecked(True) self.toggle_button.setText("STOP") + self.apply_button_style() self.status_bar.showMessage(f"Cycle test started | Current: {test_current:.4f}A") # Create log file @@ -1837,6 +1983,7 @@ class BatteryTester(QMainWindow): # Ensure buttons are in correct state if error occurs self.toggle_button.setChecked(False) self.toggle_button.setText("START") + self.apply_button_style() self.toggle_button.setEnabled(True) def start_discharge_test(self): @@ -1911,6 +2058,7 @@ class BatteryTester(QMainWindow): self.toggle_button.setChecked(True) self.toggle_button.setText("STOP") + self.apply_button_style() self.status_bar.showMessage(f"Discharge started | Current: {test_current:.4f}A") # Create log file @@ -1943,6 +2091,7 @@ class BatteryTester(QMainWindow): # Ensure buttons are in correct state if error occurs self.toggle_button.setChecked(False) self.toggle_button.setText("START") + self.apply_button_style() self.toggle_button.setEnabled(True) def start_charge_test(self): @@ -2017,6 +2166,7 @@ class BatteryTester(QMainWindow): self.toggle_button.setChecked(True) self.toggle_button.setText("STOP") + self.apply_button_style() self.status_bar.showMessage(f"Charge started @ {test_current:.3f}A to {self.charge_cutoff}V") # Create log file @@ -2048,6 +2198,7 @@ class BatteryTester(QMainWindow): QMessageBox.critical(self, "Error", str(e)) self.toggle_button.setChecked(False) self.toggle_button.setText("START") + self.apply_button_style() self.toggle_button.setEnabled(True) def start_live_monitoring(self, device): @@ -2164,6 +2315,7 @@ class BatteryTester(QMainWindow): seconds = int(seconds % 60) return f"{hours:02d}:{minutes:02d}:{seconds:02d}" + @safe_execute def stop_test(self): """Request immediate stop with proper visual feedback""" # Immediate red button feedback @@ -2243,6 +2395,7 @@ class BatteryTester(QMainWindow): self.toggle_button.setChecked(False) self.toggle_button.setText(text) + self.apply_button_style() self.toggle_button.setStyleSheet(f""" QPushButton {{ background-color: {self.status_colors["active"]}; @@ -2279,12 +2432,23 @@ class BatteryTester(QMainWindow): """) def closeEvent(self, event): - """Ensure all log files are properly closed""" - if hasattr(self, 'global_recording') and self.global_recording: - self.toggle_global_recording() # This will stop all recordings + """Ensure clean shutdown""" + self.stop_test() - # Additional cleanup if needed - super().closeEvent(event) + # Stop all timers + self.status_timer.stop() + + # Close all log files + for device in self.devices.values(): + if device.is_recording: + self.finalize_device_log_file(device) + + # Clean up threads + if hasattr(self, 'measurement_thread') and self.measurement_thread: + self.measurement_thread.stop() + self.measurement_thread.wait(1000) + + event.accept() def reset_test_data(self): if not self.active_device: @@ -2311,7 +2475,7 @@ class BatteryTester(QMainWindow): # Reset UI displays self.voltage_label.setText("0.000") self.current_label.setText("0.000") - self.time_label.setText("00:00:00") + #self.time_label.setText("00:00:00") self.capacity_label.setText("0.0000") self.energy_label.setText("0.0000") self.phase_label.setText("Idle") @@ -2435,7 +2599,10 @@ class BatteryTester(QMainWindow): self.request_stop = False self.toggle_button.setChecked(False) self.toggle_button.setText("START") + self.apply_button_style() self.toggle_button.setEnabled(True) + self.apply_button_style() # Add this line + self.test_running = False # 8. Show completion message if test wasn't stopped by user if not self.request_stop: @@ -2495,6 +2662,7 @@ class BatteryTester(QMainWindow): # Ensure we don't leave the UI in a locked state self.toggle_button.setChecked(False) self.toggle_button.setText("START") + self.apply_button_style() self.toggle_button.setEnabled(True) self.status_bar.showMessage("Error during test finalization")