diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py index 0f72afa..714ff36 100644 --- a/MainCode/adalm1000_logger.py +++ b/MainCode/adalm1000_logger.py @@ -76,7 +76,7 @@ class DeviceManager: } def handle_read_error(self, increment=1): - """Enhanced device recovery with proper session handling""" + """Enhanced device recovery with proper USB resource handling""" self.consecutive_read_errors += increment if self.consecutive_read_errors >= self.max_consecutive_errors: @@ -117,7 +117,23 @@ class DeviceManager: # Add delay and attempt USB reset time.sleep(2.0) # Longer delay for USB to settle + # Try to release USB resources using libusb + try: + import usb.core + # Find all ADALM1000 devices + devices = usb.core.find(find_all=True, idVendor=0x064b, idProduct=0x784c) + for dev in devices: + try: + print(f"Releasing USB device: {dev}") + usb.util.dispose_resources(dev) + except Exception as e: + print(f"Error releasing USB device: {e}") + time.sleep(1.0) + except ImportError: + print("pyusb not available, skipping resource release") + # Try multiple times to scan for devices + new_session = None for attempt in range(3): try: print(f"Scan attempt {attempt + 1}/3...") @@ -130,60 +146,120 @@ class DeviceManager: 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) + new_dev = None + for d in devices: + if d.serial == self.serial: + new_dev = d + break + if new_dev: + 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 + else: + print(f"Original device not found on attempt {attempt+1}") + 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 - + except Exception as e: print(f"Full reinitialization failed: {e}") - return False - - # 3. Final fallback - try USB reset command + # Continue to final fallback even if this fails + pass + except Exception as e: + print(f"Device recovery failed: {e}") + # Continue to final fallback + pass + + # 3. Final fallback - try USB reset command + try: + print("Attempting USB port reset...") + # ADALM1000 USB IDs + vendor_id = "064b" + product_id = "784c" + try: - print("Attempting USB port reset...") - import subprocess - # ADALM1000 USB IDs - result = subprocess.run(['usbreset', '064b:784c'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + # Linux-specific reset + result = subprocess.run(['usbreset', f'{vendor_id}:{product_id}'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True) + if result.returncode == 0: - print("USB reset command executed, waiting 3 seconds...") + print("USB reset command executed successfully, 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}") - + print(f"USB reset command failed with code {result.returncode}") + print(f"Error output: {result.stderr}") + + except FileNotFoundError: + # Windows alternative + try: + print("usbreset not found, trying Windows USB reset") + # Use devcon utility for Windows + result = subprocess.run(['devcon', 'restart', f'USB\VID_{vendor_id}&PID_{product_id}'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True) + + if result.returncode == 0: + print("USB device reset via devcon, waiting 3 seconds...") + time.sleep(3.0) + return self.handle_read_error(0) + else: + print(f"devcon reset failed with code {result.returncode}") + print(f"Error output: {result.stderr}") + + except FileNotFoundError: + # Python-based reset as final fallback + print("devcon not found, attempting Python USB reset") + try: + # Try to import USB module + try: + import usb.core + except ImportError: + print("pyusb not installed, please install with: pip install pyusb") + return False + + # Find device by vendor and product ID + dev = usb.core.find(idVendor=0x064b, idProduct=0x784c) + if dev is not None: + try: + # Reset the USB device + dev.reset() + print("USB device reset via pyusb, waiting 3 seconds...") + time.sleep(3.0) + return self.handle_read_error(0) + except usb.core.USBError as e: + print(f"USB reset error: {e}") + else: + print("USB device not found for reset") + except Exception as e: + print(f"Python USB reset failed: {e}") + except Exception as e: - print(f"Device recovery failed: {e}") - return False - + print(f"USB reset command failed: {e}") + import traceback + traceback.print_exc() + return True # Not enough errors yet to trigger recovery def start_measurement(self, interval=0.1): @@ -1127,36 +1203,28 @@ class BatteryTester(QMainWindow): def toggle_global_recording(self): """Toggle recording for all connected devices simultaneously""" - if not hasattr(self, 'global_recording'): - self.global_recording = False - self.global_recording = not self.global_recording if self.global_recording: # Start recording for all devices - all_success = True - for serial, device in self.devices.items(): - if not device.is_recording: - if not self.start_live_monitoring(device): - all_success = False - print(f"Failed to start recording for device {serial}") + for device in self.devices.values(): + device.is_recording = True + if not device.measurement_thread.isRunning(): + device.start_measurement(self.interval) + self.start_live_monitoring(device) - if all_success: - self.record_button.setText("■ Stop Recording") - self.apply_button_style() # Update style - self.status_bar.showMessage("Recording started for all connected devices") - else: - self.global_recording = False - self.record_button.setChecked(False) - QMessageBox.warning(self, "Warning", "Failed to start recording for some devices") + self.record_button.setText("■ Stop Recording") + self.status_bar.showMessage("Recording started for all connected devices") else: # Stop recording for all devices for device in self.devices.values(): - self.finalize_device_log_file(device) # Changed from _stop_device_recording + self.finalize_device_log_file(device) + device.is_recording = False self.record_button.setText("● Start Recording") - self.apply_button_style() # Update style self.status_bar.showMessage("Recording stopped for all devices") + + self.apply_button_style() def safe_execute(func): """Decorator to catch and log exceptions in Qt event handlers""" @@ -1380,12 +1448,17 @@ class BatteryTester(QMainWindow): self.handle_device_connection(False, "No devices found") return - self.session.start(0) self.devices = {} + self.active_device = None + for dev in self.session.devices: - manager = DeviceManager(dev) # Should work now + manager = DeviceManager(dev) manager.start_measurement(interval=self.interval) self.devices[dev.serial] = manager + + # Connect signals for all devices + manager.measurement_thread.update_signal.connect(self.update_measurements) + manager.measurement_thread.error_signal.connect(self.handle_device_error) # Select first device first_serial = next(iter(self.devices.keys())) @@ -1536,61 +1609,37 @@ class BatteryTester(QMainWindow): if serial not in self.devices: return - # Stop current device - if self.active_device: - try: - # Disconnect signals safely - if hasattr(self.active_device, 'measurement_thread'): - thread = self.active_device.measurement_thread - try: - thread.stop() # Properly stop the thread - thread.wait(500) # Wait for clean shutdown - 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}") - + # Store current device state before switching + old_device = self.active_device + # Activate new device self.active_device = self.devices[serial] - # Ensure measurement thread exists and is properly configured - if not hasattr(self.active_device, 'measurement_thread') or not self.active_device.measurement_thread: - self.active_device.start_measurement(self.interval) - - # Connect signals to new device + # Reconnect signals for new device self.measurement_thread = self.active_device.measurement_thread - - # Disconnect any existing connections first - try: - self.measurement_thread.update_signal.disconnect() - except (TypeError, RuntimeError): - pass - try: - self.measurement_thread.error_signal.disconnect() - except (TypeError, RuntimeError): - pass - - # Reconnect signals self.measurement_thread.update_signal.connect(self.update_measurements) self.measurement_thread.error_signal.connect(self.handle_device_error) - # Ensure thread is running + # Restart measurement if not running if not self.measurement_thread.isRunning(): self.active_device.start_measurement(self.interval) # Update UI with current device data self.update_ui_from_active_device() + + # Preserve recording state for old device + if old_device and old_device.is_recording: + # Ensure old device continues recording + if not old_device.measurement_thread.isRunning(): + old_device.start_measurement(self.interval) + + # Update recording button for new device + self.record_button.setChecked(self.active_device.is_recording) + self.record_button.setText("Stop Recording" if self.active_device.is_recording else "Start Recording") + self.apply_button_style() + self.status_bar.showMessage(f"Switched to device: {serial}") - # Update recording button state - self.record_button.setChecked(self.global_recording) - self.record_button.setText("Stop Recording" if self.global_recording else "Start Recording") - self.apply_button_style() # Ensure proper colors - def update_ui_from_active_device(self): dev = self.active_device if not dev: @@ -1622,52 +1671,77 @@ class BatteryTester(QMainWindow): 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: + @pyqtSlot(str, float, float, float) + def update_measurements(self, serial, voltage, current, current_time): + """Update measurements for a specific device identified by serial""" + # Get device from serial + device = self.devices.get(serial) + if not device: return - dev = self.active_device - # Update device data - dev.time_data.append(current_time) - dev.voltage_data.append(voltage) - dev.current_data.append(current) + device.time_data.append(current_time) + device.voltage_data.append(voltage) + device.current_data.append(current) # Calculate metrics - power = voltage * abs(current) # Always define power - if len(dev.time_data) > 1: - delta_t = dev.time_data[-1] - dev.time_data[-2] - dev.capacity_ah += abs(current) * delta_t / 3600 # Ah - dev.energy += power * delta_t / 3600 # Wh + power = voltage * abs(current) + if len(device.time_data) > 1: + delta_t = device.time_data[-1] - device.time_data[-2] + device.capacity_ah += abs(current) * delta_t / 3600 # Ah + device.energy += power * delta_t / 3600 # Wh # Update display buffers - dev.display_time_data.append(current_time) - dev.display_voltage_data.append(voltage) - dev.display_current_data.append(current) + device.display_time_data.append(current_time) + device.display_voltage_data.append(voltage) + device.display_current_data.append(current) - # Update UI for active device - self.voltage_label.setText(f"{voltage:.4f}") - self.current_label.setText(f"{abs(current):.4f}") - self.capacity_label.setText(f"{dev.capacity_ah:.4f}") - self.energy_label.setText(f"{dev.energy:.4f}") - - # Handle recording for ALL devices + # Update UI only if this is the active device + if device == self.active_device: + self.voltage_label.setText(f"{voltage:.4f}") + self.current_label.setText(f"{abs(current):.4f}") + self.capacity_label.setText(f"{device.capacity_ah:.4f}") + self.energy_label.setText(f"{device.energy:.4f}") + self.time_label.setText(self.format_time(current_time)) + + # Update plot if needed (only for active device) + if time.time() - getattr(device, '_last_plot_update', 0) > 0.5: # Throttle updates + device._last_plot_update = time.time() + self.update_plot() + + # Handle recording for this device now = time.time() - for device in self.devices.values(): - if device.is_recording and device.log_writer and device.time_data: - if now - device._last_log_time >= 1.0: # Log at 1Hz + if device.is_recording and device.log_writer and device.time_data: + if now - device._last_log_time >= 1.0: # Log at 1Hz + try: device.log_writer.writerow([ f"{current_time:.2f}", f"{voltage:.6f}", f"{current:.6f}", - f"{dev.capacity_ah:.6f}", + f"{device.capacity_ah:.6f}", f"{power:.6f}", - f"{dev.energy:.6f}", - dev.test_phase + f"{device.energy:.6f}", + device.test_phase ]) device.log_file.flush() device._last_log_time = now + except Exception as e: + print(f"Log write error for device {serial}: {e}") + # Attempt to close and reopen log file + try: + if device.log_file: + device.log_file.close() + except: + pass + device.log_file = None + device.log_writer = None + device.is_recording = False + + # Update UI if this is the active device + if device == self.active_device: + self.record_button.setChecked(False) + self.record_button.setText("Start Recording") + self.apply_button_style() def adjust_downsampling(self): current_length = len(self.time_data)