From 70a4b130d64aff2503271c75e8d23a01d83824e4 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 7 Aug 2025 14:17:11 +0200 Subject: [PATCH] MainCode/adalm1000_logger.py aktualisiert worki --- MainCode/adalm1000_logger.py | 762 +++++++++++++++++------------------ 1 file changed, 374 insertions(+), 388 deletions(-) diff --git a/MainCode/adalm1000_logger.py b/MainCode/adalm1000_logger.py index c39166f..0b59766 100644 --- a/MainCode/adalm1000_logger.py +++ b/MainCode/adalm1000_logger.py @@ -28,9 +28,12 @@ class DeviceManager: self.serial = dev.serial self.measurement_thread = None self.is_running = False - self.is_recording = False - self.log_file = None - self.log_writer = None + self.is_recording = False + self.log_file = None + self.log_writer = None + self._last_log_time = 0 + self.log_dir = os.path.expanduser("~/adalm1000/logs") + os.makedirs(self.log_dir, exist_ok=True) # Datenpuffer max_data_points = 36000 @@ -192,61 +195,59 @@ class MeasurementThread(QThread): # --- Handle empty samples --- if not samples: - consecutive_errors += 1 - if consecutive_errors >= max_consecutive_errors: + self.parent_manager.consecutive_read_errors += 1 + if self.parent_manager.consecutive_read_errors >= self.parent_manager.max_consecutive_errors: # Attempt device reset through parent manager if hasattr(self, 'parent_manager'): if not self.parent_manager.handle_read_error(): raise DeviceDisconnectedError("Persistent read failures") - consecutive_errors = 0 # Reset after handling time.sleep(0.1) continue - - # Reset error counter on successful read - consecutive_errors = 0 - + + # ✅ Reset error counter on successful read + self.parent_manager.consecutive_read_errors = 0 + # --- Process samples --- current_time = time.time() - self.start_time - + # Get voltage from Channel B (HI_Z mode) and current from Channel A 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]) * self.current_direction # Channel A current with direction - + # Update filter windows self.voltage_window.append(raw_voltage) self.current_window.append(raw_current) - + if len(self.voltage_window) > self.filter_window_size: self.voltage_window.pop(0) self.current_window.pop(0) - + voltage = np.mean(self.voltage_window) current = np.mean(self.current_window) - - # Validate measurements before processing - if not (0.0 <= voltage <= 5.0): # ADALM1000 voltage range + + # Validate measurements + if not (0.0 <= voltage <= 5.0): raise ValueError(f"Voltage out of range: {voltage:.4f}V") - if not (-0.25 <= current <= 0.25): # ADALM1000 current range + if not (-0.25 <= current <= 0.25): raise ValueError(f"Current out of range: {current:.4f}A") - + # Emit update self.update_signal.emit(voltage, current, current_time) - + # Store measurement try: self.measurement_queue.put_nowait((voltage, current)) except Full: - pass # It's OK to skip if queue is full - - # Adaptive sleep based on interval + pass + time.sleep(max(0.05, self.interval)) - + except DeviceDisconnectedError as e: self.error_signal.emit(f"Device disconnected: {str(e)}") if not self.parent_manager.handle_read_error(): - break # Stop if recovery failed - time.sleep(1) # Wait before retrying - + break + time.sleep(1) + except Exception as e: self.error_signal.emit(f"Read error: {str(e)}") if not self.parent_manager.handle_read_error(): @@ -575,6 +576,7 @@ class BatteryTester(QMainWindow): self.devices = {} self.active_device = None self.last_logged_phase = None + self.global_recording = False # Color scheme - MUST BE DEFINED FIRST self.bg_color = "#2E3440" @@ -647,28 +649,43 @@ class BatteryTester(QMainWindow): self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.main_layout = QVBoxLayout(self.central_widget) - self.main_layout.setContentsMargins(10, 10, 10, 10) + self.main_layout.setContentsMargins(8, 8, 8, 8) + self.main_layout.setSpacing(8) + + # Base style for consistent sizing + base_style = f""" + font-size: 10pt; + color: {self.fg_color}; + """ # Mode and device selection frame mode_frame = QFrame() mode_frame.setFrameShape(QFrame.StyledPanel) - mode_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}") + mode_frame.setStyleSheet(f""" + QFrame {{ + border: 1px solid {self.accent_color}; + border-radius: 5px; + padding: 5px; + }} + QLabel {base_style} + """) mode_layout = QHBoxLayout(mode_frame) + mode_layout.setContentsMargins(5, 2, 5, 2) # Test mode selection self.mode_label = QLabel("Test Mode:") - self.mode_label.setStyleSheet(f"color: {self.fg_color};") mode_layout.addWidget(self.mode_label) self.mode_combo = QComboBox() self.mode_combo.addItems(["Live Monitoring", "Discharge Test", "Charge Test", "Cycle Test"]) self.mode_combo.setStyleSheet(f""" QComboBox {{ + {base_style} background-color: #3B4252; - color: {self.fg_color}; border: 1px solid #4C566A; border-radius: 3px; padding: 2px; + min-height: 24px; }} """) self.mode_combo.currentTextChanged.connect(self.change_mode) @@ -676,17 +693,17 @@ class BatteryTester(QMainWindow): # Device selection self.device_label = QLabel("Device:") - self.device_label.setStyleSheet(f"color: {self.fg_color};") mode_layout.addWidget(self.device_label) self.device_combo = QComboBox() self.device_combo.setStyleSheet(f""" QComboBox {{ + {base_style} background-color: #3B4252; - color: {self.fg_color}; border: 1px solid #4C566A; border-radius: 3px; padding: 2px; + min-height: 24px; }} """) self.device_combo.currentIndexChanged.connect(self.change_device) @@ -706,68 +723,97 @@ class BatteryTester(QMainWindow): # Status indicator self.status_light = QLabel() - self.status_light.setFixedSize(20, 20) - self.status_light.setStyleSheet("background-color: red; border-radius: 10px;") + self.status_light.setFixedSize(16, 16) + self.status_light.setStyleSheet("background-color: red; border-radius: 8px;") header_layout.addWidget(self.status_light) self.connection_label = QLabel("Disconnected") + self.connection_label.setStyleSheet(base_style) header_layout.addWidget(self.connection_label) # Reconnect button self.reconnect_btn = QPushButton("Reconnect") + self.reconnect_btn.setStyleSheet(f""" + {base_style} + background-color: #4C566A; + padding: 2px 8px; + border-radius: 3px; + min-height: 24px; + """) self.reconnect_btn.clicked.connect(self.reconnect_device) header_layout.addWidget(self.reconnect_btn) self.main_layout.addWidget(header_frame) - - # Measurement display + + # Measurement display - 4 rows x 2 columns display_frame = QFrame() display_frame.setFrameShape(QFrame.StyledPanel) - display_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}") + display_frame.setStyleSheet(f""" + QFrame {{ + border: 1px solid {self.accent_color}; + border-radius: 5px; + padding: 3px; + }} + QLabel {base_style} + """) display_layout = QGridLayout(display_frame) - - # Measurement values - measurement_labels = [ - ("Voltage", "V"), ("Current", "A"), ("Test Phase", ""), - ("Elapsed Time", "s"), ("Capacity", "Ah"), ("Power", "W"), - ("Energy", "Wh"), ("Cycle Count", ""), ("Battery Temp", "°C") + display_layout.setHorizontalSpacing(5) + display_layout.setVerticalSpacing(2) + display_layout.setContentsMargins(5, 3, 5, 3) + + # Measurement fields in exact order + measurement_fields = [ + ("Voltage", "V"), ("Current", "A"), + ("Elapsed Time", "s"), ("Energy", "Wh"), + ("Test Phase", None), ("Capacity", "Ah"), # None for no unit + ("Cycle Count", None), ("Coulomb Eff.", "%") ] - for i, (label, unit) in enumerate(measurement_labels): - row = i // 3 - col = (i % 3) * 3 + for i, (label, unit) in enumerate(measurement_fields): + row = i // 2 + col = (i % 2) * 2 + # Container for each measurement with fixed height + container = QWidget() + container.setFixedHeight(24) # Fixed row height + container_layout = QHBoxLayout(container) + container_layout.setContentsMargins(2, 0, 2, 0) + container_layout.setSpacing(2) # Minimal spacing between elements + + # Label (fixed width) lbl = QLabel(f"{label}:") - lbl.setStyleSheet(f"color: {self.fg_color}; font-size: 11px;") - display_layout.addWidget(lbl, row, col) + lbl.setStyleSheet("min-width: 85px;") + container_layout.addWidget(lbl) - value_lbl = QLabel("0.000") - value_lbl.setStyleSheet(f""" - color: {self.fg_color}; - font-weight: bold; - font-size: 12px; - min-width: 60px; + # Value field (fixed width) + value_text = "0.000" if unit else ("Idle" if label == "Test Phase" else "0") + value_lbl = QLabel(value_text) + value_lbl.setAlignment(Qt.AlignRight) + value_lbl.setStyleSheet(""" + font-weight: bold; + min-width: 65px; + max-width: 65px; """) - display_layout.addWidget(value_lbl, row, col + 1) + container_layout.addWidget(value_lbl) + # Unit (only if exists) if unit: unit_lbl = QLabel(unit) - unit_lbl.setStyleSheet(f"color: {self.fg_color}; font-size: 11px;") - display_layout.addWidget(unit_lbl, row, col + 2) + unit_lbl.setStyleSheet("min-width: 20px;") + container_layout.addWidget(unit_lbl) + + display_layout.addWidget(container, row, col) - for i in range(9): - display_layout.setColumnStretch(i, 1 if i % 3 == 1 else 0) + # Assign references + widgets = [ + display_layout.itemAtPosition(r, c).widget().layout().itemAt(1).widget() + for r in range(4) for c in [0, 2] + ] + (self.voltage_label, self.current_label, + self.time_label, self.energy_label, + self.phase_label, self.capacity_label, + self.cycle_label, self.efficiency_label) = widgets - self.voltage_label = display_layout.itemAtPosition(0, 1).widget() - self.current_label = display_layout.itemAtPosition(0, 4).widget() - self.phase_label = display_layout.itemAtPosition(0, 7).widget() - self.time_label = display_layout.itemAtPosition(1, 1).widget() - self.capacity_label = display_layout.itemAtPosition(1, 4).widget() - self.power_label = display_layout.itemAtPosition(1, 7).widget() - self.energy_label = display_layout.itemAtPosition(2, 1).widget() - self.cycle_label = display_layout.itemAtPosition(2, 4).widget() - self.temp_label = display_layout.itemAtPosition(2, 7).widget() - self.main_layout.addWidget(display_frame) # Control area @@ -779,16 +825,32 @@ class BatteryTester(QMainWindow): # Parameters frame self.params_frame = QFrame() self.params_frame.setFrameShape(QFrame.StyledPanel) - self.params_frame.setStyleSheet(f"QFrame {{ border: 1px solid {self.accent_color}; border-radius: 5px; }}") + self.params_frame.setStyleSheet(f""" + QFrame {{ + border: 1px solid {self.accent_color}; + border-radius: 5px; + }} + QLabel {base_style} + QLineEdit {{ + {base_style} + background-color: #3B4252; + border: 1px solid #4C566A; + border-radius: 3px; + padding: 2px; + min-height: 24px; + }} + """) self.params_layout = QGridLayout(self.params_frame) + self.params_layout.setVerticalSpacing(3) + self.params_layout.setHorizontalSpacing(5) + self.params_layout.setContentsMargins(8, 5, 8, 5) # Add parameter inputs row = 0 # Battery Capacity - self.capacity_label = QLabel("Capacity (Ah):") - self.capacity_label.setStyleSheet(f"color: {self.fg_color};") - self.params_layout.addWidget(self.capacity_label, row, 0) + self.capacity_label_1 = QLabel("Capacity (Ah):") + self.params_layout.addWidget(self.capacity_label_1, row, 0) self.capacity_input = QLineEdit("1.0") self.capacity_input.setValidator(QDoubleValidator(0.001, 100, 3)) @@ -797,7 +859,6 @@ class BatteryTester(QMainWindow): # C-Rate self.c_rate_label = QLabel("C-Rate:") - self.c_rate_label.setStyleSheet(f"color: {self.fg_color};") self.params_layout.addWidget(self.c_rate_label, row, 0) self.c_rate_input = QLineEdit("0.1") @@ -807,7 +868,6 @@ class BatteryTester(QMainWindow): # Charge Cutoff Voltage self.charge_cutoff_label = QLabel("Charge Cutoff (V):") - self.charge_cutoff_label.setStyleSheet(f"color: {self.fg_color};") self.params_layout.addWidget(self.charge_cutoff_label, row, 0) self.charge_cutoff_input = QLineEdit("1.43") @@ -817,7 +877,6 @@ class BatteryTester(QMainWindow): # Discharge Cutoff Voltage self.discharge_cutoff_label = QLabel("Discharge Cutoff (V):") - self.discharge_cutoff_label.setStyleSheet(f"color: {self.fg_color};") self.params_layout.addWidget(self.discharge_cutoff_label, row, 0) self.discharge_cutoff_input = QLineEdit("0.01") @@ -827,7 +886,6 @@ class BatteryTester(QMainWindow): # Rest Time self.rest_time_label = QLabel("Rest Time (h):") - self.rest_time_label.setStyleSheet(f"color: {self.fg_color};") self.params_layout.addWidget(self.rest_time_label, row, 0) self.rest_time_input = QLineEdit("0.5") @@ -837,7 +895,6 @@ class BatteryTester(QMainWindow): # Test Conditions self.test_conditions_label = QLabel("Test Conditions:") - self.test_conditions_label.setStyleSheet(f"color: {self.fg_color};") self.params_layout.addWidget(self.test_conditions_label, row, 0) self.test_conditions_input = QLineEdit("Room Temperature") @@ -845,34 +902,28 @@ class BatteryTester(QMainWindow): controls_layout.addWidget(self.params_frame, 1) - # Button frame with single toggle button + # Button frame button_frame = QFrame() button_frame.setFrameShape(QFrame.NoFrame) button_layout = QVBoxLayout(button_frame) - button_layout.setContentsMargins(0, 0, 0, 0) + button_layout.setContentsMargins(5, 0, 0, 0) + button_layout.setSpacing(5) + + # Button style + button_style = f""" + {base_style} + font-weight: bold; + padding: 4px 8px; + border-radius: 4px; + min-height: 28px; + """ # Single toggle button (Start/Stop) self.toggle_button = QPushButton("START") self.toggle_button.setCheckable(True) - self.toggle_button.setStyleSheet(f""" - QPushButton {{ - background-color: {self.accent_color}; - color: {self.fg_color}; - font-weight: bold; - padding: 6px; - border-radius: 4px; - min-width: 80px; - }} - QPushButton:checked {{ - background-color: {self.warning_color}; - }} - QPushButton:pressed {{ - background-color: {self.warning_color}; - }} - QPushButton:disabled {{ - background-color: #4C566A; - color: #D8DEE9; - }} + self.toggle_button.setStyleSheet(button_style + f""" + background-color: {self.accent_color}; + min-width: 120px; """) self.toggle_button.clicked.connect(self.toggle_test) button_layout.addWidget(self.toggle_button) @@ -880,26 +931,18 @@ class BatteryTester(QMainWindow): # Continuous mode checkbox (only for Cycle mode) self.continuous_mode_check = QCheckBox("Continuous Mode") self.continuous_mode_check.setChecked(True) - self.continuous_mode_check.setStyleSheet(f"color: {self.fg_color};") + self.continuous_mode_check.setStyleSheet(base_style) button_layout.addWidget(self.continuous_mode_check) self.continuous_mode_check.hide() # Record button for Live mode - self.record_button = QPushButton("Start Recording") + self.record_button = QPushButton("● Start Recording") self.record_button.setCheckable(True) - self.record_button.setStyleSheet(f""" - QPushButton {{ - background-color: {self.success_color}; - color: {self.fg_color}; - font-weight: bold; - padding: 6px; - border-radius: 4px; - }} - QPushButton:checked {{ - background-color: {self.warning_color}; - }} + self.record_button.setStyleSheet(button_style + f""" + background-color: {self.success_color}; + min-width: 140px; """) - self.record_button.clicked.connect(self.toggle_recording) + self.record_button.clicked.connect(self.toggle_global_recording) button_layout.addWidget(self.record_button) self.record_button.hide() @@ -911,7 +954,7 @@ class BatteryTester(QMainWindow): # Status bar self.status_bar = self.statusBar() - self.status_bar.setStyleSheet(f"color: {self.fg_color};") + self.status_bar.setStyleSheet(f"color: {self.fg_color}; font-size: 9pt;") self.status_bar.showMessage("Ready") # Apply dark theme @@ -919,17 +962,43 @@ class BatteryTester(QMainWindow): QMainWindow {{ background-color: {self.bg_color}; }} - QLabel {{ - color: {self.fg_color}; - }} - QLineEdit {{ - background-color: #3B4252; - color: {self.fg_color}; - border: 1px solid #4C566A; - border-radius: 3px; - padding: 2px; + QWidget {{ + {base_style} }} """) + + 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_device_recording(device): + all_success = False + print(f"Failed to start recording for device {serial}") + + if all_success: + self.record_button.setText("■ Stop Recording") + self.record_button.setStyleSheet(f"background-color: {self.warning_color};") + 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") + else: + # Stop recording for all devices + for device in self.devices.values(): + self._stop_device_recording(device) + + self.record_button.setText("● Start Recording") + self.record_button.setStyleSheet(f"background-color: {self.success_color};") + self.status_bar.showMessage("Recording stopped for all devices") def toggle_test(self): """Toggle between start and stop based on button state""" @@ -1163,6 +1232,11 @@ class BatteryTester(QMainWindow): self.measurement_thread.update_signal.connect(self.update_measurements) self.measurement_thread.error_signal.connect(self.handle_device_error) + # Initialize recording state + self.global_recording = False + self.record_button.setChecked(False) + self.record_button.setText("Start Recording") + except Exception as e: self.handle_device_connection(False, f"Initialization failed: {str(e)}") @@ -1325,16 +1399,8 @@ class BatteryTester(QMainWindow): self.status_bar.showMessage(f"Switched to device: {serial}") # Update recording button state - if self.current_mode == "Live Monitoring": - self.record_button.setVisible(True) - if self.active_device.is_recording: # Check new device's state - self.record_button.setChecked(True) - self.record_button.setText("Stop Recording") - else: - self.record_button.setChecked(False) - self.record_button.setText("Start Recording") - else: - self.record_button.setVisible(False) + self.record_button.setChecked(self.global_recording) + self.record_button.setText("Stop Recording" if self.global_recording else "Start Recording") def update_ui_from_active_device(self): dev = self.active_device @@ -1372,59 +1438,41 @@ class BatteryTester(QMainWindow): return dev = self.active_device - - # Add measurement validation - if not self.validate_measurements(voltage, current): - print(f"Invalid measurement: V={voltage:.4f}, I={current:.4f}") - return - - with self.plot_mutex: - dev.time_data.append(current_time) - dev.voltage_data.append(voltage) - dev.current_data.append(current) - - # Aggregation for display - agg_buf = dev.aggregation_buffer - agg_buf['time'].append(current_time) - agg_buf['voltage'].append(voltage) - agg_buf['current'].append(current) - agg_buf['count'] += 1 - - now = time.time() - if now - agg_buf['last_plot_time'] >= 0.1: - agg_time = np.mean(agg_buf['time']) - agg_voltage = np.mean(agg_buf['voltage']) - agg_current = np.mean(agg_buf['current']) - - dev.display_time_data.append(agg_time) - dev.display_voltage_data.append(agg_voltage) - dev.display_current_data.append(agg_current) - - dev.aggregation_buffer = { - 'time': [], 'voltage': [], 'current': [], 'count': 0, 'last_plot_time': now - } - - # Update UI labels - self.voltage_label.setText(f"{voltage:.4f}") - self.current_label.setText(f"{abs(current):.4f}") - self.time_label.setText(self.format_time(current_time)) - - # Calculate capacity if we have enough data points + + # Update device data + dev.time_data.append(current_time) + dev.voltage_data.append(voltage) + dev.current_data.append(current) + + # Calculate metrics if len(dev.time_data) > 1: delta_t = dev.time_data[-1] - dev.time_data[-2] power = voltage * abs(current) - dev.capacity_ah += abs(current) * delta_t / 3600 - dev.energy += power * delta_t / 3600 - self.capacity_label.setText(f"{dev.capacity_ah:.4f}") - self.energy_label.setText(f"{dev.energy:.4f}") - - # Update plot periodically + dev.capacity_ah += abs(current) * delta_t / 3600 # Ah + dev.energy += power * delta_t / 3600 # Wh + + # 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 now = time.time() - if not hasattr(self, '_last_plot_update'): - self._last_plot_update = 0 - if now - self._last_plot_update >= 0.1: - self._last_plot_update = now - QTimer.singleShot(0, self.update_plot) + 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 + device.log_writer.writerow([ + f"{current_time:.2f}", + f"{voltage:.6f}", + f"{current:.6f}", + f"{dev.capacity_ah:.6f}", + f"{power:.6f}", + f"{dev.energy:.6f}", + dev.test_phase + ]) + device.log_file.flush() + device._last_log_time = now def adjust_downsampling(self): current_length = len(self.time_data) @@ -1450,69 +1498,45 @@ class BatteryTester(QMainWindow): """Update status information periodically""" now = time.time() - if not self.active_device: - return - - dev = self.active_device - - if self.test_running or (hasattr(self, 'record_button') and self.record_button.isChecked()): - if dev.time_data: - current_time = dev.time_data[-1] - if len(dev.time_data) > 1: - delta_t = dev.time_data[-1] - dev.time_data[-2] - if delta_t > 0: - current_current = abs(dev.current_data[-1]) - dev.capacity_ah += current_current * delta_t / 3600 - self.capacity_label.setText(f"{dev.capacity_ah:.4f}") - - # Logging (1x per second) - if (hasattr(self, 'log_writer') and - hasattr(self, 'current_cycle_file') and - self.current_cycle_file is not None and - not self.current_cycle_file.closed): + # Update all devices + for dev_manager in self.devices.values(): + # Only process if device has data + if not dev_manager.time_data: + continue - if not hasattr(self, '_last_log_time'): - self._last_log_time = now - - if dev.time_data and (now - self._last_log_time >= 1.0): - try: - current_time = dev.time_data[-1] - voltage = dev.voltage_data[-1] - current = dev.current_data[-1] - - if self.current_mode == "Cycle Test": - self.log_writer.writerow([ - f"{current_time:.4f}", - f"{voltage:.6f}", - f"{current:.6f}", - dev.test_phase, - f"{dev.capacity_ah:.4f}", - f"{dev.charge_capacity:.4f}", - f"{dev.coulomb_efficiency:.1f}", - f"{dev.cycle_count}" - ]) - else: - self.log_writer.writerow([ - f"{current_time:.4f}", - f"{voltage:.6f}", - f"{current:.6f}", - dev.test_phase if hasattr(dev, 'test_phase') else "Live", - f"{dev.capacity_ah:.4f}", - f"{voltage * current:.4f}", # Power - f"{dev.energy:.4f}", # Energy - f"{dev.cycle_count}" if hasattr(dev, 'cycle_count') else "1" - ]) - self.current_cycle_file.flush() - self._last_log_time = now - except Exception as e: - print(f"Error writing to log file: {e}") - if hasattr(self, 'current_cycle_file') and self.current_cycle_file is not None: - try: - self.current_cycle_file.close() - except: - pass - self.record_button.setChecked(False) - self.current_cycle_file = None + # Update capacity calculation + if len(dev_manager.time_data) > 1: + idx = len(dev_manager.time_data) - 1 + delta_t = dev_manager.time_data[idx] - dev_manager.time_data[idx-1] + current_val = abs(dev_manager.current_data[idx]) + power = dev_manager.voltage_data[idx] * current_val + dev_manager.capacity_ah += current_val * delta_t / 3600 + dev_manager.energy += power * delta_t / 3600 + + # Write to log if recording + if dev_manager.is_recording and dev_manager.log_writer: + try: + if now - getattr(dev_manager, '_last_log_time', 0) >= 1.0: + dev_manager.log_writer.writerow([ + f"{dev_manager.time_data[-1]:.4f}", + f"{dev_manager.voltage_data[-1]:.6f}", + f"{dev_manager.current_data[-1]:.6f}", + "Live", + f"{dev_manager.capacity_ah:.4f}", + f"{power:.4f}", + f"{dev_manager.energy:.4f}" + ]) + dev_manager.log_file.flush() + dev_manager._last_log_time = now + except Exception as e: + print(f"Log write error for device {dev_manager.serial}: {e}") + dev_manager.is_recording = False + + # Update UI for active device + if self.active_device and self.active_device.time_data: + dev = self.active_device + self.capacity_label.setText(f"{dev.capacity_ah:.4f}") + self.energy_label.setText(f"{dev.energy:.4f}") def start_test(self): """Start the selected test mode using the active device""" @@ -1589,7 +1613,7 @@ class BatteryTester(QMainWindow): return # Create log file - if not self.create_cycle_log_file(): + if not self._start_device_recording(self.active_device): self.stop_test() return @@ -2011,158 +2035,112 @@ class BatteryTester(QMainWindow): self.toggle_button.setText("START") self.toggle_button.setEnabled(True) - def start_live_monitoring(self): - """Start live monitoring mode for the active device""" - if not self.active_device: - QMessageBox.warning(self, "No Device", "No ADALM1000 selected.") - return - - dev_manager = self.active_device - dev = dev_manager.dev - + def start_live_monitoring(self, device): + """Initialize logging for a specific device (replaces create_device_log_file)""" try: - # Reset test state for active device - self.reset_test() - - # Reset measurement thread timing - if hasattr(dev_manager, 'measurement_thread'): - dev_manager.measurement_thread.start_time = time.time() - dev_manager.measurement_thread.voltage_window.clear() - dev_manager.measurement_thread.current_window.clear() - with dev_manager.measurement_thread.measurement_queue.mutex: - dev_manager.measurement_thread.measurement_queue.queue.clear() - - # Reset device state - dev.channels['A'].mode = pysmu.Mode.HI_Z - dev.channels['A'].constant(0) - dev.channels['B'].mode = pysmu.Mode.HI_Z - dev.channels['B'].constant(0) - - # Reset UI - self.test_running = True - dev_manager.test_phase = "Live Monitoring" - self.phase_label.setText(dev_manager.test_phase) - - # Status - self.status_bar.showMessage(f"Live monitoring started | Device: {dev.serial}") - - except Exception as e: - self.handle_device_error(f"Failed to start live monitoring: {str(e)}") - - def create_cycle_log_file(self): - """Create a new log file for the current test with device serial in filename""" - try: - self._last_log_time = time.time() - - # Schließe vorherige Datei - if hasattr(self, 'current_cycle_file') and self.current_cycle_file: - try: - self.current_cycle_file.close() - except Exception as e: - print(f"Error closing previous log file: {e}") - self.current_cycle_file = None - - # Stelle sicher, dass Log-Ordner existiert - os.makedirs(self.log_dir, exist_ok=True) - if not os.access(self.log_dir, os.W_OK): - QMessageBox.critical(self, "Error", f"No write permissions in {self.log_dir}") + if device.is_recording: + return True # Already recording + + # Ensure log directory exists + os.makedirs(device.log_dir, exist_ok=True) + if not os.access(device.log_dir, os.W_OK): + QMessageBox.critical(self, "Error", + f"No write permissions in {device.log_dir}") return False - # Generiere Timestamp + # Generate filename with device serial and timestamp timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - device_serial = self.active_device.serial[-2:] if self.active_device else "xx" + clean_serial = device.serial.replace(":", "_")[-8:] # Clean for filename + filename = os.path.join( + device.log_dir, + f"battery_test_{clean_serial}_{timestamp}.csv" + ) - # Generiere Dateinamen mit Seriennummer + # Open file and initialize writer + device.log_file = open(filename, 'w', newline='') + device.log_writer = csv.writer(device.log_file) + + # Write comprehensive header + device.log_file.write(f"# ADALM1000 Battery Test Log\n") + device.log_file.write(f"# Device Serial: {device.serial}\n") + device.log_file.write(f"# Start Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + device.log_file.write(f"# Test Mode: {self.current_mode}\n") + + if hasattr(self, 'capacity_input'): + device.log_file.write(f"# Battery Capacity: {self.capacity_input.text()} Ah\n") + + # Mode-specific parameters if self.current_mode == "Cycle Test": - self.filename = os.path.join(self.log_dir, f"battery_cycle_{timestamp}_{device_serial}.csv") - elif self.current_mode == "Discharge Test": - self.filename = os.path.join(self.log_dir, f"battery_discharge_{timestamp}_{device_serial}.csv") - else: # Live Monitoring - self.filename = os.path.join(self.log_dir, f"battery_live_{timestamp}_{device_serial}.csv") - - # Öffne neue Datei - try: - self.current_cycle_file = open(self.filename, 'w', newline='') - test_current = self.c_rate * self.capacity - test_conditions = self.test_conditions_input.text() if hasattr(self, 'test_conditions_input') else "N/A" - - # Schreibe Header - self.current_cycle_file.write(f"# ADALM1000 Battery Test Log - {self.current_mode}\n") - self.current_cycle_file.write(f"# Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - self.current_cycle_file.write(f"# Device Serial: {device_serial}\n") - self.current_cycle_file.write(f"# Battery Capacity: {self.capacity} Ah\n") - if self.current_mode != "Live Monitoring": - self.current_cycle_file.write(f"# Test Current: {test_current:.4f} A (C/{1/self.c_rate:.1f})\n") - if self.current_mode == "Cycle Test": - self.current_cycle_file.write(f"# Charge Cutoff: {self.charge_cutoff} V\n") - self.current_cycle_file.write(f"# Discharge Cutoff: {self.discharge_cutoff} V\n") - self.current_cycle_file.write(f"# Rest Time: {self.rest_time} hours\n") - elif self.current_mode == "Discharge Test": - self.current_cycle_file.write(f"# Discharge Cutoff: {self.discharge_cutoff} V\n") - self.current_cycle_file.write(f"# Test Conditions/Chemistry: {test_conditions}\n") - self.current_cycle_file.write("#\n") - - # CSV Writer - self.log_writer = csv.writer(self.current_cycle_file) - if self.current_mode == "Cycle Test": - self.log_writer.writerow([ - "Time(s)", "Voltage(V)", "Current(A)", "Phase", - "Discharge_Capacity(Ah)", "Charge_Capacity(Ah)", - "Coulomb_Eff(%)", "Cycle" - ]) - else: - self.log_writer.writerow([ - "Time(s)", "Voltage(V)", "Current(A)", "Phase", - "Capacity(Ah)", "Power(W)", "Energy(Wh)", "Cycle" - ]) - return True - - except Exception as e: - QMessageBox.critical(self, "Error", f"Failed to create log file: {e}") - return False - + device.log_file.write(f"# Charge Cutoff: {self.charge_cutoff_input.text()} V\n") + device.log_file.write(f"# Discharge Cutoff: {self.discharge_cutoff_input.text()} V\n") + device.log_file.write(f"# Rest Time: {self.rest_time_input.text()} h\n") + + device.log_file.write("#\n") # End of header + + # Write column headers + if self.current_mode == "Cycle Test": + device.log_writer.writerow([ + "Time(s)", "Voltage(V)", "Current(A)", "Phase", + "Discharge_Capacity(Ah)", "Charge_Capacity(Ah)", + "Coulomb_Eff(%)", "Cycle" + ]) + else: + device.log_writer.writerow([ + "Time(s)", "Voltage(V)", "Current(A)", "Phase", + "Capacity(Ah)", "Power(W)", "Energy(Wh)" + ]) + + device.is_recording = True + device._last_log_time = 0 + return True + except Exception as e: - print(f"Error in create_cycle_log_file: {e}") + error_msg = f"Failed to start recording for {device.serial}:\n{str(e)}" + print(error_msg) + QMessageBox.critical(self, "Recording Error", error_msg) + if device.log_file: + try: + device.log_file.close() + except: + pass + device.log_file = None + device.log_writer = None return False - def finalize_log_file(self): - """Finalize the current log file""" - if hasattr(self, 'current_cycle_file') and self.current_cycle_file: + def finalize_device_log_file(self, device): + """Properly finalize and close a device's log file""" + if not device.is_recording or not device.log_file: + return + + try: + # Write test summary footer + device.log_file.write("\n# TEST SUMMARY\n") + device.log_file.write(f"# End Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + + if device.time_data: + duration = device.time_data[-1] if device.time_data else 0 + device.log_file.write(f"# Duration: {self.format_time(duration)}\n") + device.log_file.write(f"# Final Voltage: {device.voltage_data[-1]:.4f} V\n") + device.log_file.write(f"# Final Current: {device.current_data[-1]:.4f} A\n") + + device.log_file.write(f"# Total Capacity: {device.capacity_ah:.6f} Ah\n") + device.log_file.write(f"# Total Energy: {device.energy:.6f} Wh\n") + + if self.current_mode == "Cycle Test": + device.log_file.write(f"# Cycles Completed: {device.cycle_count}\n") + if device.charge_capacity > 0: + device.log_file.write(f"# Coulomb Efficiency: {device.coulomb_efficiency:.2f}%\n") + + except Exception as e: + print(f"Error writing footer for {device.serial}: {str(e)}") + finally: try: - test_current = getattr(self, 'c_rate', 0) * getattr(self, 'capacity', 0) - test_conditions = getattr(self, 'test_conditions_input', lambda: "N/A").text() - - self.current_cycle_file.write("\n# TEST SUMMARY\n") - self.current_cycle_file.write(f"# Test Parameters:\n") - self.current_cycle_file.write(f"# - Battery Capacity: {getattr(self, 'capacity', 'N/A')} Ah\n") - - if getattr(self, 'current_mode', 'Live Monitoring') != "Live Monitoring": - self.current_cycle_file.write(f"# - Test Current: {test_current:.4f} A (C/{1/getattr(self, 'c_rate', 1):.1f})\n") - - if getattr(self, 'current_mode', '') == "Cycle Test": - self.current_cycle_file.write(f"# - Charge Cutoff: {getattr(self, 'charge_cutoff', 'N/A')} V\n") - self.current_cycle_file.write(f"# - Discharge Cutoff: {getattr(self, 'discharge_cutoff', 'N/A')} V\n") - self.current_cycle_file.write(f"# - Rest Time: {getattr(self, 'rest_time', 'N/A')} hours\n") - elif getattr(self, 'current_mode', '') == "Discharge Test": - self.current_cycle_file.write(f"# - Discharge Cutoff: {getattr(self, 'discharge_cutoff', 'N/A')} V\n") - - self.current_cycle_file.write(f"# - Test Conditions: {test_conditions}\n") - self.current_cycle_file.write(f"# Results:\n") - - if getattr(self, 'current_mode', '') == "Cycle Test": - self.current_cycle_file.write(f"# - Cycles Completed: {getattr(self, 'cycle_count', 0)}\n") - self.current_cycle_file.write(f"# - Final Discharge Capacity: {getattr(self, 'capacity_ah', 0):.4f} Ah\n") - self.current_cycle_file.write(f"# - Final Charge Capacity: {getattr(self, 'charge_capacity', 0):.4f} Ah\n") - self.current_cycle_file.write(f"# - Coulombic Efficiency: {getattr(self, 'coulomb_efficiency', 0):.1f}%\n") - else: - self.current_cycle_file.write(f"# - Capacity: {getattr(self, 'capacity_ah', 0):.4f} Ah\n") - self.current_cycle_file.write(f"# - Energy: {getattr(self, 'energy', 0):.4f} Wh\n") - - self.current_cycle_file.close() - except Exception as e: - print(f"Error closing log file: {e}") - finally: - self.current_cycle_file = None + device.log_file.close() + except: + pass + device.log_file = None + device.log_writer = None + device.is_recording = False def format_time(self, seconds): """Convert seconds to hh:mm:ss format""" @@ -2285,6 +2263,14 @@ class BatteryTester(QMainWindow): }} """) + def closeEvent(self, event): + """Ensure all log files are properly closed""" + if hasattr(self, 'global_recording') and self.global_recording: + self.toggle_global_recording() # This will stop all recordings + + # Additional cleanup if needed + super().closeEvent(event) + def reset_test_data(self): """Completely reset all test data""" if not self.active_device: