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.is_recording = False
|
||||||
self.log_file = None
|
self.log_file = None
|
||||||
self.log_writer = 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
|
# Datenpuffer
|
||||||
max_data_points = 36000
|
max_data_points = 36000
|
||||||
@ -192,18 +195,17 @@ class MeasurementThread(QThread):
|
|||||||
|
|
||||||
# --- Handle empty samples ---
|
# --- Handle empty samples ---
|
||||||
if not samples:
|
if not samples:
|
||||||
consecutive_errors += 1
|
self.parent_manager.consecutive_read_errors += 1
|
||||||
if consecutive_errors >= max_consecutive_errors:
|
if self.parent_manager.consecutive_read_errors >= self.parent_manager.max_consecutive_errors:
|
||||||
# Attempt device reset through parent manager
|
# Attempt device reset through parent manager
|
||||||
if hasattr(self, 'parent_manager'):
|
if hasattr(self, 'parent_manager'):
|
||||||
if not self.parent_manager.handle_read_error():
|
if not self.parent_manager.handle_read_error():
|
||||||
raise DeviceDisconnectedError("Persistent read failures")
|
raise DeviceDisconnectedError("Persistent read failures")
|
||||||
consecutive_errors = 0 # Reset after handling
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Reset error counter on successful read
|
# ✅ Reset error counter on successful read
|
||||||
consecutive_errors = 0
|
self.parent_manager.consecutive_read_errors = 0
|
||||||
|
|
||||||
# --- Process samples ---
|
# --- Process samples ---
|
||||||
current_time = time.time() - self.start_time
|
current_time = time.time() - self.start_time
|
||||||
@ -223,10 +225,10 @@ class MeasurementThread(QThread):
|
|||||||
voltage = np.mean(self.voltage_window)
|
voltage = np.mean(self.voltage_window)
|
||||||
current = np.mean(self.current_window)
|
current = np.mean(self.current_window)
|
||||||
|
|
||||||
# Validate measurements before processing
|
# Validate measurements
|
||||||
if not (0.0 <= voltage <= 5.0): # ADALM1000 voltage range
|
if not (0.0 <= voltage <= 5.0):
|
||||||
raise ValueError(f"Voltage out of range: {voltage:.4f}V")
|
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")
|
raise ValueError(f"Current out of range: {current:.4f}A")
|
||||||
|
|
||||||
# Emit update
|
# Emit update
|
||||||
@ -236,16 +238,15 @@ class MeasurementThread(QThread):
|
|||||||
try:
|
try:
|
||||||
self.measurement_queue.put_nowait((voltage, current))
|
self.measurement_queue.put_nowait((voltage, current))
|
||||||
except Full:
|
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))
|
time.sleep(max(0.05, self.interval))
|
||||||
|
|
||||||
except DeviceDisconnectedError as e:
|
except DeviceDisconnectedError as e:
|
||||||
self.error_signal.emit(f"Device disconnected: {str(e)}")
|
self.error_signal.emit(f"Device disconnected: {str(e)}")
|
||||||
if not self.parent_manager.handle_read_error():
|
if not self.parent_manager.handle_read_error():
|
||||||
break # Stop if recovery failed
|
break
|
||||||
time.sleep(1) # Wait before retrying
|
time.sleep(1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_signal.emit(f"Read error: {str(e)}")
|
self.error_signal.emit(f"Read error: {str(e)}")
|
||||||
@ -575,6 +576,7 @@ class BatteryTester(QMainWindow):
|
|||||||
self.devices = {}
|
self.devices = {}
|
||||||
self.active_device = None
|
self.active_device = None
|
||||||
self.last_logged_phase = None
|
self.last_logged_phase = None
|
||||||
|
self.global_recording = False
|
||||||
|
|
||||||
# Color scheme - MUST BE DEFINED FIRST
|
# Color scheme - MUST BE DEFINED FIRST
|
||||||
self.bg_color = "#2E3440"
|
self.bg_color = "#2E3440"
|
||||||
@ -647,28 +649,43 @@ class BatteryTester(QMainWindow):
|
|||||||
self.central_widget = QWidget()
|
self.central_widget = QWidget()
|
||||||
self.setCentralWidget(self.central_widget)
|
self.setCentralWidget(self.central_widget)
|
||||||
self.main_layout = QVBoxLayout(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 and device selection frame
|
||||||
mode_frame = QFrame()
|
mode_frame = QFrame()
|
||||||
mode_frame.setFrameShape(QFrame.StyledPanel)
|
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 = QHBoxLayout(mode_frame)
|
||||||
|
mode_layout.setContentsMargins(5, 2, 5, 2)
|
||||||
|
|
||||||
# Test mode selection
|
# Test mode selection
|
||||||
self.mode_label = QLabel("Test Mode:")
|
self.mode_label = QLabel("Test Mode:")
|
||||||
self.mode_label.setStyleSheet(f"color: {self.fg_color};")
|
|
||||||
mode_layout.addWidget(self.mode_label)
|
mode_layout.addWidget(self.mode_label)
|
||||||
|
|
||||||
self.mode_combo = QComboBox()
|
self.mode_combo = QComboBox()
|
||||||
self.mode_combo.addItems(["Live Monitoring", "Discharge Test", "Charge Test", "Cycle Test"])
|
self.mode_combo.addItems(["Live Monitoring", "Discharge Test", "Charge Test", "Cycle Test"])
|
||||||
self.mode_combo.setStyleSheet(f"""
|
self.mode_combo.setStyleSheet(f"""
|
||||||
QComboBox {{
|
QComboBox {{
|
||||||
|
{base_style}
|
||||||
background-color: #3B4252;
|
background-color: #3B4252;
|
||||||
color: {self.fg_color};
|
|
||||||
border: 1px solid #4C566A;
|
border: 1px solid #4C566A;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
min-height: 24px;
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
self.mode_combo.currentTextChanged.connect(self.change_mode)
|
self.mode_combo.currentTextChanged.connect(self.change_mode)
|
||||||
@ -676,17 +693,17 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Device selection
|
# Device selection
|
||||||
self.device_label = QLabel("Device:")
|
self.device_label = QLabel("Device:")
|
||||||
self.device_label.setStyleSheet(f"color: {self.fg_color};")
|
|
||||||
mode_layout.addWidget(self.device_label)
|
mode_layout.addWidget(self.device_label)
|
||||||
|
|
||||||
self.device_combo = QComboBox()
|
self.device_combo = QComboBox()
|
||||||
self.device_combo.setStyleSheet(f"""
|
self.device_combo.setStyleSheet(f"""
|
||||||
QComboBox {{
|
QComboBox {{
|
||||||
|
{base_style}
|
||||||
background-color: #3B4252;
|
background-color: #3B4252;
|
||||||
color: {self.fg_color};
|
|
||||||
border: 1px solid #4C566A;
|
border: 1px solid #4C566A;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
min-height: 24px;
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
self.device_combo.currentIndexChanged.connect(self.change_device)
|
self.device_combo.currentIndexChanged.connect(self.change_device)
|
||||||
@ -706,67 +723,96 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Status indicator
|
# Status indicator
|
||||||
self.status_light = QLabel()
|
self.status_light = QLabel()
|
||||||
self.status_light.setFixedSize(20, 20)
|
self.status_light.setFixedSize(16, 16)
|
||||||
self.status_light.setStyleSheet("background-color: red; border-radius: 10px;")
|
self.status_light.setStyleSheet("background-color: red; border-radius: 8px;")
|
||||||
header_layout.addWidget(self.status_light)
|
header_layout.addWidget(self.status_light)
|
||||||
|
|
||||||
self.connection_label = QLabel("Disconnected")
|
self.connection_label = QLabel("Disconnected")
|
||||||
|
self.connection_label.setStyleSheet(base_style)
|
||||||
header_layout.addWidget(self.connection_label)
|
header_layout.addWidget(self.connection_label)
|
||||||
|
|
||||||
# Reconnect button
|
# Reconnect button
|
||||||
self.reconnect_btn = QPushButton("Reconnect")
|
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)
|
self.reconnect_btn.clicked.connect(self.reconnect_device)
|
||||||
header_layout.addWidget(self.reconnect_btn)
|
header_layout.addWidget(self.reconnect_btn)
|
||||||
|
|
||||||
self.main_layout.addWidget(header_frame)
|
self.main_layout.addWidget(header_frame)
|
||||||
|
|
||||||
# Measurement display
|
# Measurement display - 4 rows x 2 columns
|
||||||
display_frame = QFrame()
|
display_frame = QFrame()
|
||||||
display_frame.setFrameShape(QFrame.StyledPanel)
|
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 = QGridLayout(display_frame)
|
||||||
|
display_layout.setHorizontalSpacing(5)
|
||||||
|
display_layout.setVerticalSpacing(2)
|
||||||
|
display_layout.setContentsMargins(5, 3, 5, 3)
|
||||||
|
|
||||||
# Measurement values
|
# Measurement fields in exact order
|
||||||
measurement_labels = [
|
measurement_fields = [
|
||||||
("Voltage", "V"), ("Current", "A"), ("Test Phase", ""),
|
("Voltage", "V"), ("Current", "A"),
|
||||||
("Elapsed Time", "s"), ("Capacity", "Ah"), ("Power", "W"),
|
("Elapsed Time", "s"), ("Energy", "Wh"),
|
||||||
("Energy", "Wh"), ("Cycle Count", ""), ("Battery Temp", "°C")
|
("Test Phase", None), ("Capacity", "Ah"), # None for no unit
|
||||||
|
("Cycle Count", None), ("Coulomb Eff.", "%")
|
||||||
]
|
]
|
||||||
|
|
||||||
for i, (label, unit) in enumerate(measurement_labels):
|
for i, (label, unit) in enumerate(measurement_fields):
|
||||||
row = i // 3
|
row = i // 2
|
||||||
col = (i % 3) * 3
|
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 = QLabel(f"{label}:")
|
||||||
lbl.setStyleSheet(f"color: {self.fg_color}; font-size: 11px;")
|
lbl.setStyleSheet("min-width: 85px;")
|
||||||
display_layout.addWidget(lbl, row, col)
|
container_layout.addWidget(lbl)
|
||||||
|
|
||||||
value_lbl = QLabel("0.000")
|
# Value field (fixed width)
|
||||||
value_lbl.setStyleSheet(f"""
|
value_text = "0.000" if unit else ("Idle" if label == "Test Phase" else "0")
|
||||||
color: {self.fg_color};
|
value_lbl = QLabel(value_text)
|
||||||
|
value_lbl.setAlignment(Qt.AlignRight)
|
||||||
|
value_lbl.setStyleSheet("""
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
min-width: 65px;
|
||||||
min-width: 60px;
|
max-width: 65px;
|
||||||
""")
|
""")
|
||||||
display_layout.addWidget(value_lbl, row, col + 1)
|
container_layout.addWidget(value_lbl)
|
||||||
|
|
||||||
|
# Unit (only if exists)
|
||||||
if unit:
|
if unit:
|
||||||
unit_lbl = QLabel(unit)
|
unit_lbl = QLabel(unit)
|
||||||
unit_lbl.setStyleSheet(f"color: {self.fg_color}; font-size: 11px;")
|
unit_lbl.setStyleSheet("min-width: 20px;")
|
||||||
display_layout.addWidget(unit_lbl, row, col + 2)
|
container_layout.addWidget(unit_lbl)
|
||||||
|
|
||||||
for i in range(9):
|
display_layout.addWidget(container, row, col)
|
||||||
display_layout.setColumnStretch(i, 1 if i % 3 == 1 else 0)
|
|
||||||
|
|
||||||
self.voltage_label = display_layout.itemAtPosition(0, 1).widget()
|
# Assign references
|
||||||
self.current_label = display_layout.itemAtPosition(0, 4).widget()
|
widgets = [
|
||||||
self.phase_label = display_layout.itemAtPosition(0, 7).widget()
|
display_layout.itemAtPosition(r, c).widget().layout().itemAt(1).widget()
|
||||||
self.time_label = display_layout.itemAtPosition(1, 1).widget()
|
for r in range(4) for c in [0, 2]
|
||||||
self.capacity_label = display_layout.itemAtPosition(1, 4).widget()
|
]
|
||||||
self.power_label = display_layout.itemAtPosition(1, 7).widget()
|
(self.voltage_label, self.current_label,
|
||||||
self.energy_label = display_layout.itemAtPosition(2, 1).widget()
|
self.time_label, self.energy_label,
|
||||||
self.cycle_label = display_layout.itemAtPosition(2, 4).widget()
|
self.phase_label, self.capacity_label,
|
||||||
self.temp_label = display_layout.itemAtPosition(2, 7).widget()
|
self.cycle_label, self.efficiency_label) = widgets
|
||||||
|
|
||||||
self.main_layout.addWidget(display_frame)
|
self.main_layout.addWidget(display_frame)
|
||||||
|
|
||||||
@ -779,16 +825,32 @@ class BatteryTester(QMainWindow):
|
|||||||
# Parameters frame
|
# Parameters frame
|
||||||
self.params_frame = QFrame()
|
self.params_frame = QFrame()
|
||||||
self.params_frame.setFrameShape(QFrame.StyledPanel)
|
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 = 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
|
# Add parameter inputs
|
||||||
row = 0
|
row = 0
|
||||||
|
|
||||||
# Battery Capacity
|
# Battery Capacity
|
||||||
self.capacity_label = QLabel("Capacity (Ah):")
|
self.capacity_label_1 = QLabel("Capacity (Ah):")
|
||||||
self.capacity_label.setStyleSheet(f"color: {self.fg_color};")
|
self.params_layout.addWidget(self.capacity_label_1, row, 0)
|
||||||
self.params_layout.addWidget(self.capacity_label, row, 0)
|
|
||||||
|
|
||||||
self.capacity_input = QLineEdit("1.0")
|
self.capacity_input = QLineEdit("1.0")
|
||||||
self.capacity_input.setValidator(QDoubleValidator(0.001, 100, 3))
|
self.capacity_input.setValidator(QDoubleValidator(0.001, 100, 3))
|
||||||
@ -797,7 +859,6 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# C-Rate
|
# C-Rate
|
||||||
self.c_rate_label = QLabel("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.params_layout.addWidget(self.c_rate_label, row, 0)
|
||||||
|
|
||||||
self.c_rate_input = QLineEdit("0.1")
|
self.c_rate_input = QLineEdit("0.1")
|
||||||
@ -807,7 +868,6 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Charge Cutoff Voltage
|
# Charge Cutoff Voltage
|
||||||
self.charge_cutoff_label = QLabel("Charge Cutoff (V):")
|
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.params_layout.addWidget(self.charge_cutoff_label, row, 0)
|
||||||
|
|
||||||
self.charge_cutoff_input = QLineEdit("1.43")
|
self.charge_cutoff_input = QLineEdit("1.43")
|
||||||
@ -817,7 +877,6 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Discharge Cutoff Voltage
|
# Discharge Cutoff Voltage
|
||||||
self.discharge_cutoff_label = QLabel("Discharge Cutoff (V):")
|
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.params_layout.addWidget(self.discharge_cutoff_label, row, 0)
|
||||||
|
|
||||||
self.discharge_cutoff_input = QLineEdit("0.01")
|
self.discharge_cutoff_input = QLineEdit("0.01")
|
||||||
@ -827,7 +886,6 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Rest Time
|
# Rest Time
|
||||||
self.rest_time_label = QLabel("Rest Time (h):")
|
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.params_layout.addWidget(self.rest_time_label, row, 0)
|
||||||
|
|
||||||
self.rest_time_input = QLineEdit("0.5")
|
self.rest_time_input = QLineEdit("0.5")
|
||||||
@ -837,7 +895,6 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Test Conditions
|
# Test Conditions
|
||||||
self.test_conditions_label = QLabel("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.params_layout.addWidget(self.test_conditions_label, row, 0)
|
||||||
|
|
||||||
self.test_conditions_input = QLineEdit("Room Temperature")
|
self.test_conditions_input = QLineEdit("Room Temperature")
|
||||||
@ -845,34 +902,28 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
controls_layout.addWidget(self.params_frame, 1)
|
controls_layout.addWidget(self.params_frame, 1)
|
||||||
|
|
||||||
# Button frame with single toggle button
|
# Button frame
|
||||||
button_frame = QFrame()
|
button_frame = QFrame()
|
||||||
button_frame.setFrameShape(QFrame.NoFrame)
|
button_frame.setFrameShape(QFrame.NoFrame)
|
||||||
button_layout = QVBoxLayout(button_frame)
|
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)
|
# Single toggle button (Start/Stop)
|
||||||
self.toggle_button = QPushButton("START")
|
self.toggle_button = QPushButton("START")
|
||||||
self.toggle_button.setCheckable(True)
|
self.toggle_button.setCheckable(True)
|
||||||
self.toggle_button.setStyleSheet(f"""
|
self.toggle_button.setStyleSheet(button_style + f"""
|
||||||
QPushButton {{
|
|
||||||
background-color: {self.accent_color};
|
background-color: {self.accent_color};
|
||||||
color: {self.fg_color};
|
min-width: 120px;
|
||||||
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.clicked.connect(self.toggle_test)
|
self.toggle_button.clicked.connect(self.toggle_test)
|
||||||
button_layout.addWidget(self.toggle_button)
|
button_layout.addWidget(self.toggle_button)
|
||||||
@ -880,26 +931,18 @@ class BatteryTester(QMainWindow):
|
|||||||
# Continuous mode checkbox (only for Cycle mode)
|
# Continuous mode checkbox (only for Cycle mode)
|
||||||
self.continuous_mode_check = QCheckBox("Continuous Mode")
|
self.continuous_mode_check = QCheckBox("Continuous Mode")
|
||||||
self.continuous_mode_check.setChecked(True)
|
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)
|
button_layout.addWidget(self.continuous_mode_check)
|
||||||
self.continuous_mode_check.hide()
|
self.continuous_mode_check.hide()
|
||||||
|
|
||||||
# Record button for Live mode
|
# 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.setCheckable(True)
|
||||||
self.record_button.setStyleSheet(f"""
|
self.record_button.setStyleSheet(button_style + f"""
|
||||||
QPushButton {{
|
|
||||||
background-color: {self.success_color};
|
background-color: {self.success_color};
|
||||||
color: {self.fg_color};
|
min-width: 140px;
|
||||||
font-weight: bold;
|
|
||||||
padding: 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}}
|
|
||||||
QPushButton:checked {{
|
|
||||||
background-color: {self.warning_color};
|
|
||||||
}}
|
|
||||||
""")
|
""")
|
||||||
self.record_button.clicked.connect(self.toggle_recording)
|
self.record_button.clicked.connect(self.toggle_global_recording)
|
||||||
button_layout.addWidget(self.record_button)
|
button_layout.addWidget(self.record_button)
|
||||||
self.record_button.hide()
|
self.record_button.hide()
|
||||||
|
|
||||||
@ -911,7 +954,7 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
# Status bar
|
# Status bar
|
||||||
self.status_bar = self.statusBar()
|
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")
|
self.status_bar.showMessage("Ready")
|
||||||
|
|
||||||
# Apply dark theme
|
# Apply dark theme
|
||||||
@ -919,18 +962,44 @@ class BatteryTester(QMainWindow):
|
|||||||
QMainWindow {{
|
QMainWindow {{
|
||||||
background-color: {self.bg_color};
|
background-color: {self.bg_color};
|
||||||
}}
|
}}
|
||||||
QLabel {{
|
QWidget {{
|
||||||
color: {self.fg_color};
|
{base_style}
|
||||||
}}
|
|
||||||
QLineEdit {{
|
|
||||||
background-color: #3B4252;
|
|
||||||
color: {self.fg_color};
|
|
||||||
border: 1px solid #4C566A;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 2px;
|
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
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):
|
def toggle_test(self):
|
||||||
"""Toggle between start and stop based on button state"""
|
"""Toggle between start and stop based on button state"""
|
||||||
if self.toggle_button.isChecked():
|
if self.toggle_button.isChecked():
|
||||||
@ -1163,6 +1232,11 @@ class BatteryTester(QMainWindow):
|
|||||||
self.measurement_thread.update_signal.connect(self.update_measurements)
|
self.measurement_thread.update_signal.connect(self.update_measurements)
|
||||||
self.measurement_thread.error_signal.connect(self.handle_device_error)
|
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:
|
except Exception as e:
|
||||||
self.handle_device_connection(False, f"Initialization failed: {str(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}")
|
self.status_bar.showMessage(f"Switched to device: {serial}")
|
||||||
|
|
||||||
# Update recording button state
|
# Update recording button state
|
||||||
if self.current_mode == "Live Monitoring":
|
self.record_button.setChecked(self.global_recording)
|
||||||
self.record_button.setVisible(True)
|
self.record_button.setText("Stop Recording" if self.global_recording else "Start Recording")
|
||||||
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)
|
|
||||||
|
|
||||||
def update_ui_from_active_device(self):
|
def update_ui_from_active_device(self):
|
||||||
dev = self.active_device
|
dev = self.active_device
|
||||||
@ -1373,58 +1439,40 @@ class BatteryTester(QMainWindow):
|
|||||||
|
|
||||||
dev = self.active_device
|
dev = self.active_device
|
||||||
|
|
||||||
# Add measurement validation
|
# Update device data
|
||||||
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.time_data.append(current_time)
|
||||||
dev.voltage_data.append(voltage)
|
dev.voltage_data.append(voltage)
|
||||||
dev.current_data.append(current)
|
dev.current_data.append(current)
|
||||||
|
|
||||||
# Aggregation for display
|
# Calculate metrics
|
||||||
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
|
|
||||||
if len(dev.time_data) > 1:
|
if len(dev.time_data) > 1:
|
||||||
delta_t = dev.time_data[-1] - dev.time_data[-2]
|
delta_t = dev.time_data[-1] - dev.time_data[-2]
|
||||||
power = voltage * abs(current)
|
power = voltage * abs(current)
|
||||||
dev.capacity_ah += abs(current) * delta_t / 3600
|
dev.capacity_ah += abs(current) * delta_t / 3600 # Ah
|
||||||
dev.energy += power * delta_t / 3600
|
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.capacity_label.setText(f"{dev.capacity_ah:.4f}")
|
||||||
self.energy_label.setText(f"{dev.energy:.4f}")
|
self.energy_label.setText(f"{dev.energy:.4f}")
|
||||||
|
|
||||||
# Update plot periodically
|
# Handle recording for ALL devices
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if not hasattr(self, '_last_plot_update'):
|
for device in self.devices.values():
|
||||||
self._last_plot_update = 0
|
if device.is_recording and device.log_writer and device.time_data:
|
||||||
if now - self._last_plot_update >= 0.1:
|
if now - device._last_log_time >= 1.0: # Log at 1Hz
|
||||||
self._last_plot_update = now
|
device.log_writer.writerow([
|
||||||
QTimer.singleShot(0, self.update_plot)
|
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):
|
def adjust_downsampling(self):
|
||||||
current_length = len(self.time_data)
|
current_length = len(self.time_data)
|
||||||
@ -1450,69 +1498,45 @@ class BatteryTester(QMainWindow):
|
|||||||
"""Update status information periodically"""
|
"""Update status information periodically"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
if not self.active_device:
|
# Update all devices
|
||||||
return
|
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()):
|
# Write to log if recording
|
||||||
if dev.time_data:
|
if dev_manager.is_recording and dev_manager.log_writer:
|
||||||
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):
|
|
||||||
|
|
||||||
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:
|
try:
|
||||||
current_time = dev.time_data[-1]
|
if now - getattr(dev_manager, '_last_log_time', 0) >= 1.0:
|
||||||
voltage = dev.voltage_data[-1]
|
dev_manager.log_writer.writerow([
|
||||||
current = dev.current_data[-1]
|
f"{dev_manager.time_data[-1]:.4f}",
|
||||||
|
f"{dev_manager.voltage_data[-1]:.6f}",
|
||||||
if self.current_mode == "Cycle Test":
|
f"{dev_manager.current_data[-1]:.6f}",
|
||||||
self.log_writer.writerow([
|
"Live",
|
||||||
f"{current_time:.4f}",
|
f"{dev_manager.capacity_ah:.4f}",
|
||||||
f"{voltage:.6f}",
|
f"{power:.4f}",
|
||||||
f"{current:.6f}",
|
f"{dev_manager.energy:.4f}"
|
||||||
dev.test_phase,
|
|
||||||
f"{dev.capacity_ah:.4f}",
|
|
||||||
f"{dev.charge_capacity:.4f}",
|
|
||||||
f"{dev.coulomb_efficiency:.1f}",
|
|
||||||
f"{dev.cycle_count}"
|
|
||||||
])
|
])
|
||||||
else:
|
dev_manager.log_file.flush()
|
||||||
self.log_writer.writerow([
|
dev_manager._last_log_time = now
|
||||||
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:
|
except Exception as e:
|
||||||
print(f"Error writing to log file: {e}")
|
print(f"Log write error for device {dev_manager.serial}: {e}")
|
||||||
if hasattr(self, 'current_cycle_file') and self.current_cycle_file is not None:
|
dev_manager.is_recording = False
|
||||||
try:
|
|
||||||
self.current_cycle_file.close()
|
# Update UI for active device
|
||||||
except:
|
if self.active_device and self.active_device.time_data:
|
||||||
pass
|
dev = self.active_device
|
||||||
self.record_button.setChecked(False)
|
self.capacity_label.setText(f"{dev.capacity_ah:.4f}")
|
||||||
self.current_cycle_file = None
|
self.energy_label.setText(f"{dev.energy:.4f}")
|
||||||
|
|
||||||
def start_test(self):
|
def start_test(self):
|
||||||
"""Start the selected test mode using the active device"""
|
"""Start the selected test mode using the active device"""
|
||||||
@ -1589,7 +1613,7 @@ class BatteryTester(QMainWindow):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Create log file
|
# Create log file
|
||||||
if not self.create_cycle_log_file():
|
if not self._start_device_recording(self.active_device):
|
||||||
self.stop_test()
|
self.stop_test()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -2011,158 +2035,112 @@ class BatteryTester(QMainWindow):
|
|||||||
self.toggle_button.setText("START")
|
self.toggle_button.setText("START")
|
||||||
self.toggle_button.setEnabled(True)
|
self.toggle_button.setEnabled(True)
|
||||||
|
|
||||||
def start_live_monitoring(self):
|
def start_live_monitoring(self, device):
|
||||||
"""Start live monitoring mode for the active device"""
|
"""Initialize logging for a specific device (replaces create_device_log_file)"""
|
||||||
if not self.active_device:
|
|
||||||
QMessageBox.warning(self, "No Device", "No ADALM1000 selected.")
|
|
||||||
return
|
|
||||||
|
|
||||||
dev_manager = self.active_device
|
|
||||||
dev = dev_manager.dev
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Reset test state for active device
|
if device.is_recording:
|
||||||
self.reset_test()
|
return True # Already recording
|
||||||
|
|
||||||
# Reset measurement thread timing
|
# Ensure log directory exists
|
||||||
if hasattr(dev_manager, 'measurement_thread'):
|
os.makedirs(device.log_dir, exist_ok=True)
|
||||||
dev_manager.measurement_thread.start_time = time.time()
|
if not os.access(device.log_dir, os.W_OK):
|
||||||
dev_manager.measurement_thread.voltage_window.clear()
|
QMessageBox.critical(self, "Error",
|
||||||
dev_manager.measurement_thread.current_window.clear()
|
f"No write permissions in {device.log_dir}")
|
||||||
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}")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Generiere Timestamp
|
# Generate filename with device serial and timestamp
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
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":
|
if self.current_mode == "Cycle Test":
|
||||||
self.filename = os.path.join(self.log_dir, f"battery_cycle_{timestamp}_{device_serial}.csv")
|
device.log_file.write(f"# Charge Cutoff: {self.charge_cutoff_input.text()} V\n")
|
||||||
elif self.current_mode == "Discharge Test":
|
device.log_file.write(f"# Discharge Cutoff: {self.discharge_cutoff_input.text()} V\n")
|
||||||
self.filename = os.path.join(self.log_dir, f"battery_discharge_{timestamp}_{device_serial}.csv")
|
device.log_file.write(f"# Rest Time: {self.rest_time_input.text()} h\n")
|
||||||
else: # Live Monitoring
|
|
||||||
self.filename = os.path.join(self.log_dir, f"battery_live_{timestamp}_{device_serial}.csv")
|
|
||||||
|
|
||||||
# Öffne neue Datei
|
device.log_file.write("#\n") # End of header
|
||||||
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
|
# Write column headers
|
||||||
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":
|
if self.current_mode == "Cycle Test":
|
||||||
self.current_cycle_file.write(f"# Charge Cutoff: {self.charge_cutoff} V\n")
|
device.log_writer.writerow([
|
||||||
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",
|
"Time(s)", "Voltage(V)", "Current(A)", "Phase",
|
||||||
"Discharge_Capacity(Ah)", "Charge_Capacity(Ah)",
|
"Discharge_Capacity(Ah)", "Charge_Capacity(Ah)",
|
||||||
"Coulomb_Eff(%)", "Cycle"
|
"Coulomb_Eff(%)", "Cycle"
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
self.log_writer.writerow([
|
device.log_writer.writerow([
|
||||||
"Time(s)", "Voltage(V)", "Current(A)", "Phase",
|
"Time(s)", "Voltage(V)", "Current(A)", "Phase",
|
||||||
"Capacity(Ah)", "Power(W)", "Energy(Wh)", "Cycle"
|
"Capacity(Ah)", "Power(W)", "Energy(Wh)"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
device.is_recording = True
|
||||||
|
device._last_log_time = 0
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to create log file: {e}")
|
error_msg = f"Failed to start recording for {device.serial}:\n{str(e)}"
|
||||||
return False
|
print(error_msg)
|
||||||
|
QMessageBox.critical(self, "Recording Error", error_msg)
|
||||||
except Exception as e:
|
if device.log_file:
|
||||||
print(f"Error in create_cycle_log_file: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def finalize_log_file(self):
|
|
||||||
"""Finalize the current log file"""
|
|
||||||
if hasattr(self, 'current_cycle_file') and self.current_cycle_file:
|
|
||||||
try:
|
try:
|
||||||
test_current = getattr(self, 'c_rate', 0) * getattr(self, 'capacity', 0)
|
device.log_file.close()
|
||||||
test_conditions = getattr(self, 'test_conditions_input', lambda: "N/A").text()
|
except:
|
||||||
|
pass
|
||||||
|
device.log_file = None
|
||||||
|
device.log_writer = None
|
||||||
|
return False
|
||||||
|
|
||||||
self.current_cycle_file.write("\n# TEST SUMMARY\n")
|
def finalize_device_log_file(self, device):
|
||||||
self.current_cycle_file.write(f"# Test Parameters:\n")
|
"""Properly finalize and close a device's log file"""
|
||||||
self.current_cycle_file.write(f"# - Battery Capacity: {getattr(self, 'capacity', 'N/A')} Ah\n")
|
if not device.is_recording or not device.log_file:
|
||||||
|
return
|
||||||
|
|
||||||
if getattr(self, 'current_mode', 'Live Monitoring') != "Live Monitoring":
|
try:
|
||||||
self.current_cycle_file.write(f"# - Test Current: {test_current:.4f} A (C/{1/getattr(self, 'c_rate', 1):.1f})\n")
|
# 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 getattr(self, 'current_mode', '') == "Cycle Test":
|
if device.time_data:
|
||||||
self.current_cycle_file.write(f"# - Charge Cutoff: {getattr(self, 'charge_cutoff', 'N/A')} V\n")
|
duration = device.time_data[-1] if device.time_data else 0
|
||||||
self.current_cycle_file.write(f"# - Discharge Cutoff: {getattr(self, 'discharge_cutoff', 'N/A')} V\n")
|
device.log_file.write(f"# Duration: {self.format_time(duration)}\n")
|
||||||
self.current_cycle_file.write(f"# - Rest Time: {getattr(self, 'rest_time', 'N/A')} hours\n")
|
device.log_file.write(f"# Final Voltage: {device.voltage_data[-1]:.4f} V\n")
|
||||||
elif getattr(self, 'current_mode', '') == "Discharge Test":
|
device.log_file.write(f"# Final Current: {device.current_data[-1]:.4f} A\n")
|
||||||
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")
|
device.log_file.write(f"# Total Capacity: {device.capacity_ah:.6f} Ah\n")
|
||||||
self.current_cycle_file.write(f"# Results:\n")
|
device.log_file.write(f"# Total Energy: {device.energy:.6f} Wh\n")
|
||||||
|
|
||||||
if getattr(self, 'current_mode', '') == "Cycle Test":
|
if self.current_mode == "Cycle Test":
|
||||||
self.current_cycle_file.write(f"# - Cycles Completed: {getattr(self, 'cycle_count', 0)}\n")
|
device.log_file.write(f"# Cycles Completed: {device.cycle_count}\n")
|
||||||
self.current_cycle_file.write(f"# - Final Discharge Capacity: {getattr(self, 'capacity_ah', 0):.4f} Ah\n")
|
if device.charge_capacity > 0:
|
||||||
self.current_cycle_file.write(f"# - Final Charge Capacity: {getattr(self, 'charge_capacity', 0):.4f} Ah\n")
|
device.log_file.write(f"# Coulomb Efficiency: {device.coulomb_efficiency:.2f}%\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:
|
except Exception as e:
|
||||||
print(f"Error closing log file: {e}")
|
print(f"Error writing footer for {device.serial}: {str(e)}")
|
||||||
finally:
|
finally:
|
||||||
self.current_cycle_file = None
|
try:
|
||||||
|
device.log_file.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
device.log_file = None
|
||||||
|
device.log_writer = None
|
||||||
|
device.is_recording = False
|
||||||
|
|
||||||
def format_time(self, seconds):
|
def format_time(self, seconds):
|
||||||
"""Convert seconds to hh:mm:ss format"""
|
"""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):
|
def reset_test_data(self):
|
||||||
"""Completely reset all test data"""
|
"""Completely reset all test data"""
|
||||||
if not self.active_device:
|
if not self.active_device:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user