MainCode/adalm1000_logger.py aktualisiert
worki
This commit is contained in:
parent
ded39ec158
commit
70a4b130d6
@ -31,6 +31,9 @@ class DeviceManager:
|
||||
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,18 +195,17 @@ 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
|
||||
@ -223,10 +225,10 @@ class MeasurementThread(QThread):
|
||||
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
|
||||
@ -236,16 +238,15 @@ class MeasurementThread(QThread):
|
||||
try:
|
||||
self.measurement_queue.put_nowait((voltage, current))
|
||||
except Full:
|
||||
pass # It's OK to skip if queue is full
|
||||
pass
|
||||
|
||||
# Adaptive sleep based on interval
|
||||
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)}")
|
||||
@ -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,67 +723,96 @@ 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)
|
||||
display_layout.setHorizontalSpacing(5)
|
||||
display_layout.setVerticalSpacing(2)
|
||||
display_layout.setContentsMargins(5, 3, 5, 3)
|
||||
|
||||
# Measurement values
|
||||
measurement_labels = [
|
||||
("Voltage", "V"), ("Current", "A"), ("Test Phase", ""),
|
||||
("Elapsed Time", "s"), ("Capacity", "Ah"), ("Power", "W"),
|
||||
("Energy", "Wh"), ("Cycle Count", ""), ("Battery Temp", "°C")
|
||||
# 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};
|
||||
# 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;
|
||||
font-size: 12px;
|
||||
min-width: 60px;
|
||||
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)
|
||||
|
||||
for i in range(9):
|
||||
display_layout.setColumnStretch(i, 1 if i % 3 == 1 else 0)
|
||||
display_layout.addWidget(container, row, col)
|
||||
|
||||
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()
|
||||
# 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.main_layout.addWidget(display_frame)
|
||||
|
||||
@ -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,18 +962,44 @@ 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"""
|
||||
if self.toggle_button.isChecked():
|
||||
@ -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
|
||||
@ -1373,58 +1439,40 @@ class BatteryTester(QMainWindow):
|
||||
|
||||
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
|
||||
# Update device data
|
||||
dev.time_data.append(current_time)
|
||||
dev.voltage_data.append(voltage)
|
||||
dev.current_data.append(current)
|
||||
|
||||
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
|
||||
# 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}")
|
||||
dev.capacity_ah += abs(current) * delta_t / 3600 # Ah
|
||||
dev.energy += power * delta_t / 3600 # Wh
|
||||
|
||||
# Update plot periodically
|
||||
# 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
|
||||
# Update all devices
|
||||
for dev_manager in self.devices.values():
|
||||
# Only process if device has data
|
||||
if not dev_manager.time_data:
|
||||
continue
|
||||
|
||||
dev = self.active_device
|
||||
# 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
|
||||
|
||||
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}")
|
||||
# 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
|
||||
|
||||
# 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):
|
||||
|
||||
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 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()
|
||||
if device.is_recording:
|
||||
return True # Already recording
|
||||
|
||||
# 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}")
|
||||
# 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")
|
||||
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")
|
||||
|
||||
# Ö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"
|
||||
device.log_file.write("#\n") # End of header
|
||||
|
||||
# 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")
|
||||
# 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)"
|
||||
])
|
||||
|
||||
# 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.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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user