MainCode/adalm1000_logger.py aktualisiert

not working
This commit is contained in:
Jan 2025-06-30 16:01:19 +02:00
parent 7b5b03357a
commit bb25bcbcad

View File

@ -38,64 +38,60 @@ class MeasurementThread(QThread):
def __init__(self, device: object, interval: float = 0.1, start_time: float = None): def __init__(self, device: object, interval: float = 0.1, start_time: float = None):
"""Initialize measurement thread.""" """Initialize measurement thread."""
super().__init__() super().__init__()
self._mutex = QMutex()
self.data_mutex = QMutex()
self.device = device self.device = device
self.interval = max(0.05, interval) # Minimum interval self.interval = max(0.05, interval)
self._running = False self._running = False
self._mutex = QMutex() # Thread safety
self.filter_window_size = 10 self.filter_window_size = 10
self.start_time = time.time() self.start_time = start_time if start_time else time.time()
self.last_update_time = self.start_time self.last_update_time = self.start_time
# Configure channels
self.device.channels['A'].mode = pysmu.Mode.SIMV # Channel A for current
self.device.channels['B'].mode = pysmu.Mode.HI_Z # Channel B for voltage
def run(self): def run(self):
"""Main measurement loop with enhanced error handling.""" """Measurement loop for both voltage and current."""
self._running = True self._running = True
voltage_window = deque() voltage_window = deque()
current_window = deque() current_window = deque()
self.status_signal.emit("Measurement started") self.status_signal.emit("Measurement started")
self.last_update_time = time.time()
while self._running: while self._running:
try: try:
# Read samples with timeout samples = self.device.read(self.filter_window_size, timeout=500)
samples = self.device.read( if not samples or len(samples) < self.filter_window_size:
self.filter_window_size, continue
timeout=500,
)
if not samples:
raise DeviceDisconnectedError("No samples received - device may be disconnected")
# Process samples (thread-safe)
with QMutexLocker(self._mutex): with QMutexLocker(self._mutex):
raw_voltage = np.mean([s[1][0] for s in samples]) # Get voltage from Channel B and current from Channel A
raw_current = np.mean([s[0][1] for s in samples]) raw_voltage = np.mean([s[1][0] for s in samples]) # Channel B voltage
raw_current = np.mean([s[0][1] for s in samples]) # Channel A current
# Apply moving average filter
if len(voltage_window) >= self.filter_window_size: if len(voltage_window) >= self.filter_window_size:
voltage_window.popleft() voltage_window.popleft()
current_window.popleft()
voltage_window.append(raw_voltage) voltage_window.append(raw_voltage)
if len(current_window) >= self.filter_window_size:
current_window.popleft()
current_window.append(raw_current) current_window.append(raw_current)
voltage = np.mean(list(voltage_window)) voltage = np.mean(voltage_window)
current = np.mean(list(current_window)) current = np.mean(current_window)
current_time = time.time() - self.start_time current_time = time.time() - self.start_time
# Emit updates
self.update_signal.emit(voltage, current, current_time) self.update_signal.emit(voltage, current, current_time)
self.last_update_time = time.time()
# Dynamic sleep adjustment
elapsed = time.time() - self.last_update_time elapsed = time.time() - self.last_update_time
sleep_time = max(0.01, self.interval - elapsed) sleep_time = max(0.01, self.interval - elapsed)
time.sleep(sleep_time) time.sleep(sleep_time)
self.last_update_time = time.time()
except DeviceDisconnectedError as e:
self.error_signal.emit(f"Device error: {str(e)}")
break
except Exception as e: except Exception as e:
self.error_signal.emit(f"Measurement error: {str(e)}") self.error_signal.emit(f"Measurement error: {str(e)}")
self.status_signal.emit(f"Error: {str(e)}")
break break
self.status_signal.emit("Measurement stopped") self.status_signal.emit("Measurement stopped")
@ -123,24 +119,22 @@ class TestSequenceThread(QThread):
error_occurred = pyqtSignal(str) error_occurred = pyqtSignal(str)
def __init__(self, parent: QObject): def __init__(self, parent: QObject):
"""Initialize test sequence thread. """Initialize test sequence thread."""
super().__init__(parent) # Pass parent to QThread
Args:
parent: Reference to main BatteryTester instance
"""
super().__init__()
self.parent = parent self.parent = parent
self._mutex = QMutex() self._mutex = QMutex() # For thread operation control
self.data_mutex = QMutex() # For data protection
self._running = False self._running = False
def run(self): def run(self):
"""Execute the complete test sequence.""" """Execute the complete test sequence with configurable modes."""
self._running = True self._running = True
try: try:
test_current = float(self.parent.c_rate_input.text()) * float(self.parent.capacity_input.text()) test_current = float(self.parent.c_rate_input.text()) * float(self.parent.capacity_input.text())
charge_cutoff = float(self.parent.charge_cutoff_input.text()) charge_cutoff = float(self.parent.charge_cutoff_input.text())
discharge_cutoff = float(self.parent.discharge_cutoff_input.text()) discharge_cutoff = float(self.parent.discharge_cutoff_input.text())
cv_cutoff_current = float(self.parent.cv_cutoff_input.text()) # Add this input field
while self._running and (self.parent.continuous_mode or self.parent.cycle_count == 0): while self._running and (self.parent.continuous_mode or self.parent.cycle_count == 0):
with QMutexLocker(self._mutex): with QMutexLocker(self._mutex):
@ -150,8 +144,15 @@ class TestSequenceThread(QThread):
self.parent.cycle_count += 1 self.parent.cycle_count += 1
cycle = self.parent.cycle_count cycle = self.parent.cycle_count
# Charge phase # CC Charge phase
self._execute_phase("charge", test_current, charge_cutoff, discharge_cutoff, charge_cutoff) self._execute_phase("charge", test_current, charge_cutoff,
discharge_cutoff, charge_cutoff)
if not self._running:
break
# Optional CV Charge phase
if self.parent.cv_mode_enabled: # Add checkbox in UI
self._execute_cv_charge(charge_cutoff, test_current, cv_cutoff_current)
if not self._running: if not self._running:
break break
@ -160,8 +161,9 @@ class TestSequenceThread(QThread):
if not self._running: if not self._running:
break break
# Discharge phase # Discharge phase (CC)
self._execute_phase("discharge", test_current, discharge_cutoff, discharge_cutoff, charge_cutoff) self._execute_phase("discharge", test_current, discharge_cutoff,
discharge_cutoff, charge_cutoff)
if not self.parent.continuous_mode: if not self.parent.continuous_mode:
break break
@ -180,39 +182,15 @@ class TestSequenceThread(QThread):
finally: finally:
self._running = False self._running = False
def _execute_phase(self, phase: str, current: float, target_voltage: float, discharge_cutoff: float, charge_cutoff: float): def _execute_cv_charge(self, target_voltage: float, current_limit: float, cutoff_current: float):
"""Execute constant voltage charge phase."""
try: try:
print(f"\n=== Starting {phase} phase ===") self.progress_updated.emit(0.0, "CV Charge")
print(f"Device session active: {self.parent.session_active}")
print(f"Channel A mode before: {self.parent.dev.channels['A'].mode}")
# Reset channel first # Configure for CV mode
self.parent.dev.channels['A'].mode = pysmu.Mode.HI_Z
self.parent.dev.channels['A'].constant(0)
time.sleep(0.5) # Increased settling time
# Configure for current mode
if phase == "charge":
print(f"Starting CHARGE at {current}A to {target_voltage}V")
self.parent.dev.channels['A'].mode = pysmu.Mode.SIMV self.parent.dev.channels['A'].mode = pysmu.Mode.SIMV
time.sleep(0.1) # Additional delay self.parent.dev.channels['A'].constant(target_voltage)
self.parent.dev.channels['A'].constant(current)
else:
print(f"Starting DISCHARGE at {-current}A to {target_voltage}V")
self.parent.dev.channels['A'].mode = pysmu.Mode.SIMV
time.sleep(0.1) # Additional delay
self.parent.dev.channels['A'].constant(-current)
time.sleep(0.5) # Allow more settling time
# Verify current setting
samples = self.parent.dev.read(10, timeout=1000) # More samples, longer timeout
measured_current = np.mean([s[0][1] for s in samples])
print(f"Requested {current}A, Measured {measured_current:.6f}A") # More precision
print(f"Channel A mode after: {self.parent.dev.channels['A'].mode}")
# Rest of your existing loop...
time.sleep(0.1)
start_time = time.time() start_time = time.time()
last_update = start_time last_update = start_time
@ -221,21 +199,113 @@ class TestSequenceThread(QThread):
if self.parent.request_stop: if self.parent.request_stop:
break break
if not self.parent.voltage_data: if not self.parent.test_data['voltage']:
time.sleep(0.1) time.sleep(0.1)
continue continue
current_voltage = self.parent.voltage_data[-1] current_voltage = self.parent.test_data['voltage'][-1]
current_current = abs(self.parent.test_data['current'][-1])
current_time = time.time()
delta_t = current_time - last_update
last_update = current_time
# Update charge capacity
self.parent.charge_capacity += current_current * delta_t / 3600
# Calculate progress based on current
progress = 1 - (current_current / current_limit)
progress = max(0.0, min(1.0, progress))
self.progress_updated.emit(progress, "CV Charge")
# Check termination condition
if current_current <= cutoff_current:
break
time.sleep(0.1)
except Exception as e:
self.error_occurred.emit(f"CV Charge error: {str(e)}")
raise
finally:
self.parent.dev.channels['A'].mode = pysmu.Mode.HI_Z
def _execute_phase(self, phase: str, current: float, target_voltage: float, discharge_cutoff: float, charge_cutoff: float):
try:
print(f"\n=== Starting {phase} phase ===")
print(f"Device session active: {self.parent.session_active}")
print(f"Channel A mode before: {self.parent.dev.channels['A'].mode}")
# Reset channel first with longer delay
self.parent.dev.channels['A'].mode = pysmu.Mode.HI_Z
self.parent.dev.channels['A'].constant(0)
time.sleep(1.0) # Increased settling time
# Configure for current mode with explicit setup
self.parent.dev.channels['A'].mode = pysmu.Mode.SIMV
time.sleep(0.5) # Additional delay for mode change
# Set current with proper polarity
if phase == "charge":
print(f"Starting CHARGE at {current}A to {target_voltage}V")
self.parent.dev.channels['A'].constant(current)
else:
print(f"Starting DISCHARGE at {-current}A to {target_voltage}V")
self.parent.dev.channels['A'].constant(-current)
time.sleep(1.0) # Allow more settling time
print(f"Set constant current to: {current} A on channel A")
# Verify current setting with more samples
samples = self.parent.dev.read(50, timeout=2000) # More samples, longer timeout
measured_current = np.mean([s[0][1] for s in samples])
print(f"Requested {current}A, Measured {measured_current:.6f}A")
# Additional debug info
print(f"Channel A mode: {self.parent.dev.channels['A'].mode}")
if abs(measured_current) < 0.001: # If current is still near zero
print("Warning: Current not being applied - checking connection")
# Try resetting the device session
self.parent.dev.channels['A'].mode = pysmu.Mode.HI_Z
time.sleep(0.5)
self.parent.dev.channels['A'].mode = pysmu.Mode.SIMV
time.sleep(0.5)
self.parent.dev.channels['A'].constant(current if phase == "charge" else -current)
time.sleep(1.0)
# Re-measure
samples = self.parent.dev.read(50, timeout=2000)
measured_current = np.mean([s[0][1] for s in samples])
print(f"After reset: Requested {current}A, Measured {measured_current:.6f}A")
if abs(measured_current) < 0.001: # Still no current
raise RuntimeError(f"Failed to apply {current}A - check device connection and battery")
# Main phase loop
start_time = time.time()
last_update = start_time
while self._running:
with QMutexLocker(self._mutex):
if self.parent.request_stop:
break
if not self.parent.test_data['voltage']:
time.sleep(0.1)
continue
current_voltage = self.parent.test_data['voltage'][-1]
current_time = time.time() current_time = time.time()
delta_t = current_time - last_update delta_t = current_time - last_update
last_update = current_time last_update = current_time
# Update capacity # Update capacity
if phase == "charge": if phase == "charge":
self.parent.charge_capacity += abs(self.parent.current_data[-1]) * delta_t / 3600 self.parent.charge_capacity += abs(self.parent.test_data['current'][-1]) * delta_t / 3600
progress = (current_voltage - discharge_cutoff) / (target_voltage - discharge_cutoff) progress = (current_voltage - discharge_cutoff) / (target_voltage - discharge_cutoff)
else: else:
self.parent.capacity_ah += abs(self.parent.current_data[-1]) * delta_t / 3600 self.parent.capacity_ah += abs(self.parent.test_data['current'][-1]) * delta_t / 3600
progress = (charge_cutoff - current_voltage) / (charge_cutoff - target_voltage) progress = (charge_cutoff - current_voltage) / (charge_cutoff - target_voltage)
progress = max(0.0, min(1.0, progress)) progress = max(0.0, min(1.0, progress))
@ -258,40 +328,13 @@ class TestSequenceThread(QThread):
except Exception as e: except Exception as e:
self.error_occurred.emit(f"{phase.capitalize()} error: {str(e)}") self.error_occurred.emit(f"{phase.capitalize()} error: {str(e)}")
raise raise
finally:
def _execute_rest(self, phase: str): # Ensure channel is reset even if error occurs
"""Execute rest phase.
Args:
phase: Description of rest phase
"""
try: try:
self.progress_updated.emit(0.0, f"Resting ({phase})")
self.parent.dev.channels['A'].mode = pysmu.Mode.HI_Z self.parent.dev.channels['A'].mode = pysmu.Mode.HI_Z
self.parent.dev.channels['A'].constant(0) self.parent.dev.channels['A'].constant(0)
except:
rest_time = float(self.parent.rest_time_input.text()) * 3600 pass # Best effort cleanup
rest_end = time.time() + rest_time
while time.time() < rest_end and self._running:
with QMutexLocker(self._mutex):
if self.parent.request_stop:
break
progress = 1 - (rest_end - time.time()) / rest_time
self.progress_updated.emit(progress, f"Resting ({phase})")
time.sleep(1)
except Exception as e:
self.error_occurred.emit(f"Rest phase error: {str(e)}")
raise
def stop(self):
"""Safely stop the test sequence."""
with QMutexLocker(self._mutex):
self._running = False
self.wait(500) # Wait up to 500ms for clean exit
class BatteryTester(QMainWindow): class BatteryTester(QMainWindow):
"""Main application window for battery capacity testing.""" """Main application window for battery capacity testing."""
@ -299,22 +342,34 @@ class BatteryTester(QMainWindow):
error_signal = pyqtSignal(str) error_signal = pyqtSignal(str)
def __init__(self): def __init__(self):
"""Initialize the battery tester application.""" """Initialize the battery tester application with enhanced features."""
super().__init__() super().__init__()
self.error_signal.connect(self.handle_device_error) self.error_signal.connect(self.handle_device_error)
# Initialize data buffers # Initialize thread-safety objects
self.time_data = deque() self._mutex = QMutex() # For general thread safety
self.voltage_data = deque() self.data_mutex = QMutex() # For test data protection
self.current_data = deque()
self.phase_data = deque()
# Test variables # Enhanced data structure for flexible mode support
self.test_data = {
'time': deque(),
'voltage': deque(),
'current': deque(),
'mode': deque(), # Tracks operation mode (CC, CV, etc.)
'phase': deque(), # Tracks charge/discharge/rest
'capacity': deque(),
'energy': deque()
}
# Test control variables
self.test_phase = "Ready" self.test_phase = "Ready"
self.capacity_ah = 0.0 self.current_mode = "None" # Tracks current operation mode
self.charge_capacity = 0.0 self.capacity_ah = 0.0 # Discharge capacity
self.charge_capacity = 0.0 # Charge capacity
self.energy_wh = 0.0 # Energy in watt-hours
self.coulomb_efficiency = 0.0 self.coulomb_efficiency = 0.0
self.cycle_count = 0 self.cycle_count = 0
self.cycle_data = [] # Stores cycle statistics
# Color scheme # Color scheme
self.bg_color = QColor(46, 52, 64) self.bg_color = QColor(46, 52, 64)
@ -322,26 +377,32 @@ class BatteryTester(QMainWindow):
self.accent_color = QColor(94, 129, 172) self.accent_color = QColor(94, 129, 172)
self.warning_color = QColor(191, 97, 106) self.warning_color = QColor(191, 97, 106)
self.success_color = QColor(163, 190, 140) self.success_color = QColor(163, 190, 140)
self.cv_color = QColor(143, 188, 187) # Additional color for CV mode
# Device status # Device and test status
self.session_active = False self.session_active = False
self.measuring = False self.measuring = False
self.test_running = False self.test_running = False
self.continuous_mode = False self.continuous_mode = False
self.cv_mode_enabled = False # CV charge mode flag
self.request_stop = False self.request_stop = False
self.interval = 0.1 self.interval = 0.1 # Measurement interval
# Logging configuration
self.log_dir = os.path.expanduser("~/adalm1000/logs") self.log_dir = os.path.expanduser("~/adalm1000/logs")
os.makedirs(self.log_dir, exist_ok=True) os.makedirs(self.log_dir, exist_ok=True)
self.start_time = time.time() self.start_time = time.time()
self.log_buffer = []
self.current_cycle_file = None
# Thread management # Thread management
self.measurement_thread = None self.measurement_thread = None
self.test_thread = None self.test_thread = None
# Initialize UI # Initialize UI with all controls
self._setup_ui() self._setup_ui()
# Initialize device with delay # Initialize device with delay to avoid USB issues
QTimer.singleShot(100, self.safe_init_device) QTimer.singleShot(100, self.safe_init_device)
def _setup_ui(self): def _setup_ui(self):
@ -477,6 +538,13 @@ class BatteryTester(QMainWindow):
params_frame.setFrameShape(QFrame.StyledPanel) params_frame.setFrameShape(QFrame.StyledPanel)
params_layout = QGridLayout(params_frame) params_layout = QGridLayout(params_frame)
# Add CV cutoff current input
params_layout.addWidget(QLabel("CV Cutoff Current (A):"), 4, 0)
self.cv_cutoff_input = QLineEdit("0.02") # 20mA default
self.cv_cutoff_input.setFixedWidth(80)
self.cv_cutoff_input.setToolTip("Current at which CV charging should stop")
params_layout.addWidget(self.cv_cutoff_input, 4, 1)
# Battery capacity # Battery capacity
params_layout.addWidget(QLabel("Battery Capacity (Ah):"), 0, 0) params_layout.addWidget(QLabel("Battery Capacity (Ah):"), 0, 0)
self.capacity_input = QLineEdit("0.2") self.capacity_input = QLineEdit("0.2")
@ -520,6 +588,12 @@ class BatteryTester(QMainWindow):
button_layout = QVBoxLayout(button_frame) button_layout = QVBoxLayout(button_frame)
button_layout.setContentsMargins(0, 0, 0, 0) button_layout.setContentsMargins(0, 0, 0, 0)
# Add CV mode checkbox and cutoff current input
self.cv_mode_check = QCheckBox("Enable CV Charge")
self.cv_mode_check.setChecked(False)
self.cv_mode_check.setToolTip("Enable constant voltage charge phase after CC charge")
button_layout.addWidget(self.cv_mode_check)
self.start_button = QPushButton("START TEST") self.start_button = QPushButton("START TEST")
self.start_button.clicked.connect(self.start_test) self.start_button.clicked.connect(self.start_test)
self.start_button.setStyleSheet(f""" self.start_button.setStyleSheet(f"""
@ -557,6 +631,7 @@ class BatteryTester(QMainWindow):
self.continuous_check.setChecked(True) self.continuous_check.setChecked(True)
self.continuous_check.setToolTip("Run multiple charge/discharge cycles until stopped") self.continuous_check.setToolTip("Run multiple charge/discharge cycles until stopped")
button_layout.addWidget(self.continuous_check) button_layout.addWidget(self.continuous_check)
button_layout.addWidget(self.cv_mode_check)
controls_layout.addWidget(button_frame) controls_layout.addWidget(button_frame)
self.main_layout.addWidget(controls_frame) self.main_layout.addWidget(controls_frame)
@ -632,7 +707,7 @@ class BatteryTester(QMainWindow):
print("Waiting before initializing session...") print("Waiting before initializing session...")
time.sleep(1.5) # Delay helps avoid "device busy" issues time.sleep(1.5) # Delay helps avoid "device busy" issues
self.session = pysmu.Session(ignore_dataflow=True, queue_size=10000) self.session = pysmu.Session()
# 🔍 Log detected devices # 🔍 Log detected devices
print(f"Devices found: {self.session.devices}") print(f"Devices found: {self.session.devices}")
@ -664,34 +739,44 @@ class BatteryTester(QMainWindow):
raise Exception(f"Device initialization failed: {str(e)}") raise Exception(f"Device initialization failed: {str(e)}")
def cleanup_device(self): def cleanup_device(self):
"""Clean up device resources.""" """Clean up device resources efficiently with essential safeguards."""
print("Cleaning up device session...") print("Cleaning up device session...")
# Stop measurement thread # Stop threads with timeout
if self.measurement_thread is not None: for thread in [self.measurement_thread, getattr(self, 'test_thread', None)]:
if thread and thread.isRunning():
try: try:
self.measurement_thread.stop() thread.stop()
if not self.measurement_thread.wait(1000): if not thread.wait(800): # Reduced timeout
print("Warning: Measurement thread didn't stop cleanly") thread.terminate()
self.measurement_thread = None print(f"Warning: {thread.__class__.__name__} required termination")
except Exception as e: except Exception as e:
print(f"Error stopping measurement thread: {e}") print(f"Error stopping {thread.__class__.__name__}: {str(e)}")
# Stop and delete session # Clean up session if it exists
if hasattr(self, 'session'): if getattr(self, 'session', None):
try: try:
if self.session_active: if self.session_active:
time.sleep(0.1) # Quick channel reset if device exists
if hasattr(self, 'dev'):
try:
self.dev.channels['A'].mode = pysmu.Mode.HI_Z
self.dev.channels['B'].mode = pysmu.Mode.HI_Z
except:
pass # Best effort cleanup
self.session.end() self.session.end()
self.session_active = False
del self.session del self.session
print("Session ended successfully")
except Exception as e: except Exception as e:
print(f"Error ending session: {e}") print(f"Session cleanup error: {str(e)}")
finally: finally:
self.session_active = False self.session_active = False
# Reset UI indicators # Reset all states and UI
self.measuring = False
self.test_running = False
self.request_stop = True
self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") self.status_light.setStyleSheet("background-color: red; border-radius: 10px;")
self.connection_label.setText("Disconnected") self.connection_label.setText("Disconnected")
self.start_button.setEnabled(False) self.start_button.setEnabled(False)
@ -699,6 +784,10 @@ class BatteryTester(QMainWindow):
def start_measurement_thread(self): def start_measurement_thread(self):
"""Start the measurement thread.""" """Start the measurement thread."""
if not hasattr(self, 'dev') or self.dev is None:
print("Device not initialized, cannot start measurement thread")
return
if self.measurement_thread is not None: if self.measurement_thread is not None:
self.measurement_thread.stop() self.measurement_thread.stop()
self.measurement_thread.wait(500) self.measurement_thread.wait(500)
@ -754,14 +843,25 @@ class BatteryTester(QMainWindow):
pass pass
def start_test(self): def start_test(self):
"""Start the complete battery test cycle.""" """Start the complete battery test cycle with proper file initialization."""
if not self.test_running: # Check if test is already running
if self.test_running:
return
# Verify thread safety objects exist
if not hasattr(self, 'data_mutex') or self.data_mutex is None:
QMessageBox.critical(self, "Error", "Thread safety not initialized")
return
try:
with QMutexLocker(self.data_mutex):
# Get and validate input values with error handling
try: try:
# Get and validate input values
capacity = float(self.capacity_input.text()) capacity = float(self.capacity_input.text())
charge_cutoff = float(self.charge_cutoff_input.text()) charge_cutoff = float(self.charge_cutoff_input.text())
discharge_cutoff = float(self.discharge_cutoff_input.text()) discharge_cutoff = float(self.discharge_cutoff_input.text())
c_rate = float(self.c_rate_input.text()) c_rate = float(self.c_rate_input.text())
cv_cutoff = float(self.cv_cutoff_input.text())
if capacity <= 0: if capacity <= 0:
raise ValueError("Battery capacity must be positive") raise ValueError("Battery capacity must be positive")
@ -770,39 +870,78 @@ class BatteryTester(QMainWindow):
if c_rate <= 0: if c_rate <= 0:
raise ValueError("C-rate must be positive") raise ValueError("C-rate must be positive")
self.continuous_mode = self.continuous_check.isChecked()
test_current = c_rate * capacity test_current = c_rate * capacity
if test_current > 0.2: if test_current > 0.2:
raise ValueError("Current must be ≤200mA (0.2A) for ADALM1000") raise ValueError("Current must be ≤200mA (0.2A) for ADALM1000")
# Reset data except ValueError as e:
self.time_data.clear() QMessageBox.critical(self, "Input Error", str(e))
self.voltage_data.clear() return
self.current_data.clear()
# Reset test state and data structures
try:
for key in self.test_data:
self.test_data[key].clear()
self.capacity_ah = 0.0 self.capacity_ah = 0.0
self.charge_capacity = 0.0 self.charge_capacity = 0.0
self.coulomb_efficiency = 0.0 self.coulomb_efficiency = 0.0
self.cycle_count = 0 self.cycle_count = 0
except Exception as e:
QMessageBox.critical(self, "Data Error", f"Couldn't reset test data: {str(e)}")
return
# Initialize timing
self.start_time = time.time() self.start_time = time.time()
self.time_label.setText("00:00:00") self.time_label.setText("00:00:00")
self.reset_plot() self.reset_plot()
# Prepare log file # Initialize log file with error handling
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
self.base_filename = os.path.join(self.log_dir, f"battery_test_{timestamp}") self.filename = os.path.join(self.log_dir, f"battery_test_{timestamp}.csv")
# Start test with open(self.filename, 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow([
'Time(s)', 'Voltage(V)', 'Current(A)', 'Mode',
'Phase', 'Discharge(Ah)', 'Charge(Ah)', 'Efficiency(%)',
'Cycle'
])
self.current_cycle_file = open(self.filename, 'a', newline='')
self.log_writer = csv.writer(self.current_cycle_file)
self.log_buffer = []
except Exception as e:
QMessageBox.critical(
self,
"File Error",
f"Failed to initialize log file:\n{str(e)}\n\n"
f"Check directory permissions:\n{self.log_dir}"
)
return
# Start test with thread safety
try:
self.test_running = True self.test_running = True
self.start_time = time.time() self.request_stop = False
self.test_phase = "Initial Discharge" self.test_phase = "Initializing"
self.phase_label.setText(self.test_phase) self.current_mode = "CC"
self.continuous_mode = self.continuous_check.isChecked()
self.cv_mode_enabled = self.cv_mode_check.isChecked()
# UI updates
self.start_button.setEnabled(False) self.start_button.setEnabled(False)
self.stop_button.setEnabled(True) self.stop_button.setEnabled(True)
self.status_bar.setText(f"Test started | Discharging to {discharge_cutoff}V @ {test_current:.3f}A") self.status_bar.setText(
f"Test started | Target: {discharge_cutoff}V @ {test_current:.3f}A | "
f"CV Mode: {'ON' if self.cv_mode_enabled else 'OFF'}"
)
# Start test thread with cleanup guard
if self.test_thread and self.test_thread.isRunning():
self.test_thread.stop()
# Start test sequence in separate thread
self.test_thread = TestSequenceThread(self) self.test_thread = TestSequenceThread(self)
self.test_thread.progress_updated.connect(self.update_test_progress) self.test_thread.progress_updated.connect(self.update_test_progress)
self.test_thread.cycle_completed.connect(self.update_cycle_stats) self.test_thread.cycle_completed.connect(self.update_cycle_stats)
@ -811,7 +950,20 @@ class BatteryTester(QMainWindow):
self.test_thread.start() self.test_thread.start()
except Exception as e: except Exception as e:
QMessageBox.critical(self, "Error", str(e)) self.finalize_test(show_message=False)
QMessageBox.critical(
self,
"Test Error",
f"Failed to start test thread:\n{str(e)}"
)
except Exception as e:
self.finalize_test(show_message=False)
QMessageBox.critical(
self,
"Critical Error",
f"Unexpected error in test initialization:\n{str(e)}"
)
def update_test_progress(self, progress: float, phase: str): def update_test_progress(self, progress: float, phase: str):
"""Update test progress and phase display.""" """Update test progress and phase display."""
@ -867,55 +1019,125 @@ class BatteryTester(QMainWindow):
self.finalize_test(show_message=False) self.finalize_test(show_message=False)
def finalize_test(self, show_message: bool = True): def finalize_test(self, show_message: bool = True):
"""Final cleanup after test completion.""" """Final cleanup after test completion with robust error handling."""
# Set up error tracking
errors = []
try: try:
# Write log data # 1. Handle log buffer writing
if hasattr(self, 'log_buffer') and self.log_buffer: if getattr(self, 'log_writer', None) is not None:
try: try:
with open(self.filename, 'a', newline='') as f: if getattr(self, 'log_buffer', None):
writer = csv.writer(f) try:
writer.writerows(self.log_buffer) self.log_writer.writerows(self.log_buffer)
self.log_buffer.clear() self.log_buffer.clear()
except Exception as e: except Exception as e:
print(f"Error writing log buffer: {e}") errors.append(f"Failed to write log buffer: {str(e)}")
except Exception as e:
errors.append(f"Log buffer access error: {str(e)}")
# Close log file # 2. Handle file closure
if hasattr(self, 'current_cycle_file') and self.current_cycle_file: if getattr(self, 'current_cycle_file', None) is not None:
try: try:
self.current_cycle_file.flush() file = self.current_cycle_file
os.fsync(self.current_cycle_file.fileno()) if not file.closed:
self.current_cycle_file.close() try:
file.flush()
os.fsync(file.fileno())
except Exception as e: except Exception as e:
print(f"Error closing log file: {e}") errors.append(f"Failed to sync file: {str(e)}")
# Show notification
if show_message:
msg_box = QMessageBox(self)
msg_box.setWindowFlags(msg_box.windowFlags() |
Qt.WindowStaysOnTopHint)
msg_box.setIcon(QMessageBox.Information)
msg_box.setWindowTitle("Test Complete")
msg_box.setText(f"Test completed\nCycles: {self.cycle_count}")
msg_box.exec_()
except Exception as e:
print(f"Critical error in finalize_test: {e}")
finally: finally:
# Reset test status file.close()
except Exception as e:
errors.append(f"File closure error: {str(e)}")
finally:
if hasattr(self, 'current_cycle_file'):
del self.current_cycle_file
if hasattr(self, 'log_writer'):
del self.log_writer
# 3. Clean up thread references
try:
if hasattr(self, 'test_thread') and self.test_thread is not None:
if self.test_thread.isRunning():
self.test_thread.stop()
if not self.test_thread.wait(500): # 500ms timeout
errors.append("Test thread didn't stop cleanly")
self.test_thread = None
except Exception as e:
errors.append(f"Thread cleanup error: {str(e)}")
# 4. Reset test state
try:
self.test_running = False
self.request_stop = True
self.measuring = False
self.stop_button.setEnabled(False)
self.start_button.setEnabled(True)
except Exception as e:
errors.append(f"State reset error: {str(e)}")
# 5. Show completion message if requested
if show_message and self.isVisible():
try:
msg = "Test completed"
if errors:
msg += f"\n\nMinor issues occurred:\n- " + "\n- ".join(errors)
QMessageBox.information(
self,
"Test Complete",
f"{msg}\nCycles: {self.cycle_count}",
QMessageBox.Ok
)
except Exception as e:
print(f"Failed to show completion message: {str(e)}")
except Exception as e:
# Catastrophic error handling
print(f"Critical error in finalize_test: {str(e)}")
try:
if hasattr(self, 'current_cycle_file'):
try:
self.current_cycle_file.close()
except:
pass
except:
pass
# Ensure basic state is reset
self.test_running = False self.test_running = False
self.request_stop = True self.request_stop = True
self.measuring = False self.measuring = False
def update_measurements(self, voltage: float, current: float, current_time: float): finally:
"""Update measurements in the UI.""" # Final cleanup guarantees
if len(self.time_data) > 10000: # Limit data points try:
self.time_data.popleft() if hasattr(self, 'log_buffer'):
self.voltage_data.popleft() self.log_buffer.clear()
self.current_data.popleft() except:
pass
self.time_data.append(current_time) def update_measurements(self, voltage: float, current: float, current_time: float):
self.voltage_data.append(voltage) """Update measurements with enhanced data structure."""
self.current_data.append(current) if not hasattr(self, '_mutex'):
print("Critical Error: Mutex not initialized")
return
try:
with QMutexLocker(self._mutex):
# Limit data points
max_points = 10000
for key in self.test_data:
if len(self.test_data[key]) > max_points:
self.test_data[key].popleft()
# Store data
self.test_data['time'].append(current_time)
self.test_data['voltage'].append(voltage)
self.test_data['current'].append(current)
self.test_data['phase'].append(self.test_phase)
self.test_data['mode'].append(self.current_mode)
# Update UI # Update UI
self.voltage_label.setText(f"{voltage:.4f}") self.voltage_label.setText(f"{voltage:.4f}")
@ -923,11 +1145,11 @@ class BatteryTester(QMainWindow):
self.time_label.setText(self.format_time(current_time)) self.time_label.setText(self.format_time(current_time))
# Update plot periodically # Update plot periodically
if len(self.time_data) % 10 == 0: if len(self.test_data['time']) % 10 == 0:
self.update_plot() self.update_plot()
# Log data if test is running # Log data if test is running
if self.test_running and hasattr(self, 'current_cycle_file'): if self.test_running and hasattr(self, 'log_writer'):
self.log_buffer.append([ self.log_buffer.append([
f"{current_time:.3f}", f"{current_time:.3f}",
f"{voltage:.6f}", f"{voltage:.6f}",
@ -941,10 +1163,15 @@ class BatteryTester(QMainWindow):
# Write data in blocks # Write data in blocks
if len(self.log_buffer) >= 10: if len(self.log_buffer) >= 10:
with open(self.filename, 'a', newline='') as f: try:
writer = csv.writer(f) self.log_writer.writerows(self.log_buffer)
writer.writerows(self.log_buffer) if hasattr(self, 'current_cycle_file') and self.current_cycle_file:
self.current_cycle_file.flush()
self.log_buffer.clear() self.log_buffer.clear()
except Exception as e:
print(f"Log write error: {e}")
except Exception as e:
print(f"Error in update_measurements: {str(e)}")
def write_cycle_summary(self): def write_cycle_summary(self):
"""Write cycle summary to the current cycle's log file""" """Write cycle summary to the current cycle's log file"""
@ -969,22 +1196,28 @@ class BatteryTester(QMainWindow):
print(f"Error writing cycle summary: {e}") print(f"Error writing cycle summary: {e}")
def update_plot(self): def update_plot(self):
"""Update the plot with new data.""" """Update the plot with new data using test_data structure."""
if not self.time_data: if not self.test_data['time']:
return return
self.line_voltage.set_data(list(self.time_data), list(self.voltage_data)) self.line_voltage.set_data(
self.line_current.set_data(list(self.time_data), list(self.current_data)) list(self.test_data['time']),
list(self.test_data['voltage'])
)
self.line_current.set_data(
list(self.test_data['time']),
list(self.test_data['current'])
)
self.auto_scale_axes() self.auto_scale_axes()
self.canvas.draw_idle() self.canvas.draw_idle()
def auto_scale_axes(self): def auto_scale_axes(self):
"""Automatically adjust plot axes.""" """Automatically adjust plot axes using test_data."""
if not self.time_data: if not self.test_data['time']:
return return
# X-axis adjustment # X-axis adjustment
max_time = list(self.time_data)[-1] max_time = list(self.test_data['time'])[-1]
current_xlim = self.ax.get_xlim() current_xlim = self.ax.get_xlim()
if max_time > current_xlim[1] * 0.95: if max_time > current_xlim[1] * 0.95:
new_xmax = max_time * 1.10 # 10% padding new_xmax = max_time * 1.10 # 10% padding
@ -992,23 +1225,17 @@ class BatteryTester(QMainWindow):
self.ax2.set_xlim(0, new_xmax) self.ax2.set_xlim(0, new_xmax)
# Y-axes adjustment # Y-axes adjustment
if self.voltage_data: if self.test_data['voltage']:
voltage_padding = 0.2 voltage_padding = 0.2
min_v = max(0, min(list(self.voltage_data)) - voltage_padding) min_v = max(0, min(list(self.test_data['voltage'])) - voltage_padding)
max_v = min(5.0, max(list(self.voltage_data)) + voltage_padding) max_v = min(5.0, max(list(self.test_data['voltage'])) + voltage_padding)
current_ylim = self.ax.get_ylim() self.ax.set_ylim(min_v, max_v)
new_min = current_ylim[0] + (min_v - current_ylim[0]) * 0.1
new_max = current_ylim[1] + (max_v - current_ylim[1]) * 0.1
self.ax.set_ylim(new_min, new_max)
if self.current_data: if self.test_data['current']:
current_padding = 0.05 current_padding = 0.05
min_c = max(-0.25, min(list(self.current_data)) - current_padding) min_c = max(-0.25, min(list(self.test_data['current'])) - current_padding)
max_c = min(0.25, max(list(self.current_data)) + current_padding) max_c = min(0.25, max(list(self.test_data['current'])) + current_padding)
current_ylim = self.ax2.get_ylim() self.ax2.set_ylim(min_c, max_c)
new_min = current_ylim[0] + (min_c - current_ylim[0]) * 0.1
new_max = current_ylim[1] + (max_c - current_ylim[1]) * 0.1
self.ax2.set_ylim(new_min, new_max)
@staticmethod @staticmethod
def format_time(seconds: float) -> str: def format_time(seconds: float) -> str: