MainCode/adalm1000_logger.py aktualisiert
not working
This commit is contained in:
parent
7b5b03357a
commit
bb25bcbcad
@ -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:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user