parent
e23e2e24b2
commit
2df374f4ef
@ -0,0 +1,775 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Database Manager Widget - vzug-e-hinge
|
||||
=======================================
|
||||
Comprehensive database management interface for inspecting and managing sessions.
|
||||
|
||||
Features:
|
||||
- Session browser with sort/filter/search
|
||||
- Session details with editable notes
|
||||
- Rename, delete, and export sessions
|
||||
- Delete individual runs
|
||||
- Session statistics
|
||||
|
||||
Author: Kynsight
|
||||
Version: 1.0.0
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QSplitter,
|
||||
QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QLabel, QPushButton, QLineEdit, QTextEdit,
|
||||
QGroupBox, QMessageBox, QInputDialog, QFileDialog,
|
||||
QCheckBox, QListWidget, QListWidgetItem
|
||||
)
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtGui import QFont
|
||||
from datetime import datetime
|
||||
import sqlite3
|
||||
|
||||
|
||||
class DatabaseManagerWidget(QWidget):
|
||||
"""
|
||||
Database Manager Widget.
|
||||
|
||||
Provides comprehensive session management:
|
||||
- Browse all sessions
|
||||
- View session details and runs
|
||||
- Rename, delete, export sessions
|
||||
- Add notes to sessions
|
||||
- Delete individual runs
|
||||
"""
|
||||
|
||||
def __init__(self, db_manager):
|
||||
"""
|
||||
Initialize Database Manager Widget.
|
||||
|
||||
Args:
|
||||
db_manager: DatabaseManager instance
|
||||
"""
|
||||
super().__init__()
|
||||
self.db_manager = db_manager
|
||||
self.selected_session_id = None
|
||||
self._ensure_notes_column()
|
||||
self._init_ui()
|
||||
self._load_sessions()
|
||||
|
||||
def _ensure_notes_column(self):
|
||||
"""Ensure notes column exists in sessions table (migration)."""
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
# Check if notes column exists
|
||||
cursor = conn.execute("PRAGMA table_info(sessions)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
|
||||
if 'notes' not in columns:
|
||||
# Add notes column
|
||||
conn.execute("ALTER TABLE sessions ADD COLUMN notes TEXT DEFAULT ''")
|
||||
conn.commit()
|
||||
print("[INFO] Added 'notes' column to sessions table")
|
||||
except Exception as e:
|
||||
print(f"[WARN] Failed to add notes column: {e}")
|
||||
|
||||
def _init_ui(self):
|
||||
"""Initialize user interface."""
|
||||
layout = QVBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
# Main splitter (left: session browser, right: details)
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
layout.addWidget(splitter)
|
||||
|
||||
# Left panel: Session Browser
|
||||
left_panel = self._create_session_browser()
|
||||
splitter.addWidget(left_panel)
|
||||
|
||||
# Right panel: Session Details
|
||||
right_panel = self._create_session_details()
|
||||
splitter.addWidget(right_panel)
|
||||
|
||||
# Set splitter ratio (40% left, 60% right)
|
||||
splitter.setStretchFactor(0, 4)
|
||||
splitter.setStretchFactor(1, 6)
|
||||
|
||||
def _create_session_browser(self):
|
||||
"""Create session browser panel (left side)."""
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
panel.setLayout(layout)
|
||||
|
||||
# Search and filter
|
||||
search_layout = QHBoxLayout()
|
||||
search_layout.addWidget(QLabel("Search:"))
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("Filter sessions by name...")
|
||||
self.search_input.textChanged.connect(self._filter_sessions)
|
||||
search_layout.addWidget(self.search_input)
|
||||
layout.addLayout(search_layout)
|
||||
|
||||
# Session table
|
||||
self.session_table = QTableWidget()
|
||||
self.session_table.setColumnCount(5)
|
||||
self.session_table.setHorizontalHeaderLabels([
|
||||
"Select", "Session Name", "Date", "Runs", "Size (MB)"
|
||||
])
|
||||
|
||||
# Configure table
|
||||
header = self.session_table.horizontalHeader()
|
||||
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||
|
||||
self.session_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.session_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
||||
self.session_table.itemSelectionChanged.connect(self._on_session_selected)
|
||||
self.session_table.setSortingEnabled(True)
|
||||
|
||||
layout.addWidget(self.session_table)
|
||||
|
||||
# Batch action buttons
|
||||
btn_layout = QHBoxLayout()
|
||||
|
||||
self.btn_select_all = QPushButton("Select All")
|
||||
self.btn_select_all.clicked.connect(self._select_all_sessions)
|
||||
btn_layout.addWidget(self.btn_select_all)
|
||||
|
||||
self.btn_clear_selection = QPushButton("Clear")
|
||||
self.btn_clear_selection.clicked.connect(self._clear_selection)
|
||||
btn_layout.addWidget(self.btn_clear_selection)
|
||||
|
||||
btn_layout.addStretch()
|
||||
|
||||
self.btn_delete_selected = QPushButton("Delete Selected")
|
||||
self.btn_delete_selected.clicked.connect(self._delete_selected_sessions)
|
||||
btn_layout.addWidget(self.btn_delete_selected)
|
||||
|
||||
self.btn_refresh = QPushButton("Refresh")
|
||||
self.btn_refresh.clicked.connect(self._load_sessions)
|
||||
btn_layout.addWidget(self.btn_refresh)
|
||||
|
||||
layout.addLayout(btn_layout)
|
||||
|
||||
return panel
|
||||
|
||||
def _create_session_details(self):
|
||||
"""Create session details panel (right side)."""
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
panel.setLayout(layout)
|
||||
|
||||
# Session info group
|
||||
info_group = QGroupBox("Session Information")
|
||||
info_layout = QVBoxLayout()
|
||||
info_group.setLayout(info_layout)
|
||||
|
||||
# Session ID
|
||||
self.label_session_id = QLabel("No session selected")
|
||||
self.label_session_id.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
||||
info_layout.addWidget(self.label_session_id)
|
||||
|
||||
# Created date
|
||||
self.label_created = QLabel("")
|
||||
info_layout.addWidget(self.label_created)
|
||||
|
||||
# Notes
|
||||
notes_layout = QHBoxLayout()
|
||||
notes_layout.addWidget(QLabel("Notes:"))
|
||||
self.text_notes = QTextEdit()
|
||||
self.text_notes.setMaximumHeight(80)
|
||||
self.text_notes.setPlaceholderText("Add notes or description for this session...")
|
||||
self.text_notes.textChanged.connect(self._on_notes_changed)
|
||||
notes_layout.addWidget(self.text_notes)
|
||||
info_layout.addLayout(notes_layout)
|
||||
|
||||
# Session actions
|
||||
action_layout = QHBoxLayout()
|
||||
|
||||
self.btn_rename = QPushButton("Rename Session")
|
||||
self.btn_rename.clicked.connect(self._rename_session)
|
||||
action_layout.addWidget(self.btn_rename)
|
||||
|
||||
self.btn_delete = QPushButton("Delete Session")
|
||||
self.btn_delete.clicked.connect(self._delete_session)
|
||||
action_layout.addWidget(self.btn_delete)
|
||||
|
||||
self.btn_export = QPushButton("Export CSV")
|
||||
self.btn_export.clicked.connect(self._export_session)
|
||||
action_layout.addWidget(self.btn_export)
|
||||
|
||||
action_layout.addStretch()
|
||||
info_layout.addLayout(action_layout)
|
||||
|
||||
layout.addWidget(info_group)
|
||||
|
||||
# Runs group
|
||||
runs_group = QGroupBox("Runs in Session")
|
||||
runs_layout = QVBoxLayout()
|
||||
runs_group.setLayout(runs_layout)
|
||||
|
||||
self.runs_list = QListWidget()
|
||||
runs_layout.addWidget(self.runs_list)
|
||||
|
||||
# Run actions
|
||||
run_action_layout = QHBoxLayout()
|
||||
|
||||
self.btn_delete_runs = QPushButton("Delete Selected Runs")
|
||||
self.btn_delete_runs.clicked.connect(self._delete_selected_runs)
|
||||
run_action_layout.addWidget(self.btn_delete_runs)
|
||||
|
||||
run_action_layout.addStretch()
|
||||
runs_layout.addLayout(run_action_layout)
|
||||
|
||||
layout.addWidget(runs_group)
|
||||
|
||||
# Statistics group
|
||||
stats_group = QGroupBox("Session Statistics")
|
||||
stats_layout = QVBoxLayout()
|
||||
stats_group.setLayout(stats_layout)
|
||||
|
||||
self.label_stats = QLabel("Select a session to view statistics")
|
||||
self.label_stats.setFont(QFont("Monospace", 9))
|
||||
stats_layout.addWidget(self.label_stats)
|
||||
|
||||
layout.addWidget(stats_group)
|
||||
|
||||
# Disable all controls initially
|
||||
self._set_details_enabled(False)
|
||||
|
||||
return panel
|
||||
|
||||
def _set_details_enabled(self, enabled: bool):
|
||||
"""Enable or disable detail panel controls."""
|
||||
self.text_notes.setEnabled(enabled)
|
||||
self.btn_rename.setEnabled(enabled)
|
||||
self.btn_delete.setEnabled(enabled)
|
||||
self.btn_export.setEnabled(enabled)
|
||||
self.btn_delete_runs.setEnabled(enabled)
|
||||
|
||||
def _load_sessions(self):
|
||||
"""Load all sessions from database."""
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
cursor = conn.execute("""
|
||||
SELECT
|
||||
session_id,
|
||||
session_name,
|
||||
created_at,
|
||||
total_runs,
|
||||
notes,
|
||||
(SELECT COUNT(*) FROM telemetry_decoded WHERE telemetry_decoded.session_id = sessions.session_id) as data_points
|
||||
FROM sessions
|
||||
ORDER BY created_at DESC
|
||||
""")
|
||||
|
||||
sessions = cursor.fetchall()
|
||||
|
||||
# Store all sessions for filtering
|
||||
self.all_sessions = sessions
|
||||
|
||||
# Display sessions
|
||||
self._display_sessions(sessions)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to load sessions:\n{str(e)}")
|
||||
|
||||
def _display_sessions(self, sessions):
|
||||
"""Display sessions in table."""
|
||||
self.session_table.setSortingEnabled(False)
|
||||
self.session_table.setRowCount(0)
|
||||
|
||||
for row_idx, session in enumerate(sessions):
|
||||
session_id, session_name, created_at, total_runs, notes, data_points = session
|
||||
|
||||
self.session_table.insertRow(row_idx)
|
||||
|
||||
# Checkbox
|
||||
checkbox = QCheckBox()
|
||||
checkbox_widget = QWidget()
|
||||
checkbox_layout = QHBoxLayout(checkbox_widget)
|
||||
checkbox_layout.addWidget(checkbox)
|
||||
checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
checkbox_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.session_table.setCellWidget(row_idx, 0, checkbox_widget)
|
||||
|
||||
# Session name
|
||||
name_item = QTableWidgetItem(session_name)
|
||||
name_item.setData(Qt.ItemDataRole.UserRole, session_id)
|
||||
self.session_table.setItem(row_idx, 1, name_item)
|
||||
|
||||
# Date
|
||||
date_item = QTableWidgetItem(created_at)
|
||||
self.session_table.setItem(row_idx, 2, date_item)
|
||||
|
||||
# Runs
|
||||
runs_item = QTableWidgetItem(str(total_runs))
|
||||
runs_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.session_table.setItem(row_idx, 3, runs_item)
|
||||
|
||||
# Size (estimate: ~100 bytes per data point)
|
||||
size_mb = (data_points * 100) / (1024 * 1024)
|
||||
size_item = QTableWidgetItem(f"{size_mb:.2f}")
|
||||
size_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.session_table.setItem(row_idx, 4, size_item)
|
||||
|
||||
self.session_table.setSortingEnabled(True)
|
||||
|
||||
def _filter_sessions(self):
|
||||
"""Filter sessions based on search text."""
|
||||
search_text = self.search_input.text().lower()
|
||||
|
||||
if not search_text:
|
||||
# Show all sessions
|
||||
self._display_sessions(self.all_sessions)
|
||||
else:
|
||||
# Filter sessions
|
||||
filtered = [s for s in self.all_sessions if search_text in s[1].lower()]
|
||||
self._display_sessions(filtered)
|
||||
|
||||
def _on_session_selected(self):
|
||||
"""Handle session selection."""
|
||||
selected_items = self.session_table.selectedItems()
|
||||
|
||||
if not selected_items:
|
||||
self._set_details_enabled(False)
|
||||
self.selected_session_id = None
|
||||
self.label_session_id.setText("No session selected")
|
||||
self.label_created.setText("")
|
||||
self.text_notes.clear()
|
||||
self.runs_list.clear()
|
||||
self.label_stats.setText("Select a session to view statistics")
|
||||
return
|
||||
|
||||
# Get session ID from selected row
|
||||
row = selected_items[0].row()
|
||||
session_id = self.session_table.item(row, 1).data(Qt.ItemDataRole.UserRole)
|
||||
|
||||
self.selected_session_id = session_id
|
||||
self._load_session_details(session_id)
|
||||
self._set_details_enabled(True)
|
||||
|
||||
def _load_session_details(self, session_id: str):
|
||||
"""Load details for selected session."""
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
# Get session info
|
||||
cursor = conn.execute("""
|
||||
SELECT session_id, session_name, created_at, total_runs, notes
|
||||
FROM sessions
|
||||
WHERE session_id = ?
|
||||
""", (session_id,))
|
||||
|
||||
session = cursor.fetchone()
|
||||
|
||||
if not session:
|
||||
return
|
||||
|
||||
session_id, session_name, created_at, total_runs, notes = session
|
||||
|
||||
# Update UI
|
||||
self.label_session_id.setText(f"Session: {session_name}")
|
||||
self.label_created.setText(f"Created: {created_at}")
|
||||
|
||||
# Block signals while setting notes to avoid triggering save
|
||||
self.text_notes.blockSignals(True)
|
||||
self.text_notes.setPlainText(notes or "")
|
||||
self.text_notes.blockSignals(False)
|
||||
|
||||
# Load runs
|
||||
self._load_runs(session_id)
|
||||
|
||||
# Load statistics
|
||||
self._load_statistics(session_id)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to load session details:\n{str(e)}")
|
||||
|
||||
def _load_runs(self, session_id: str):
|
||||
"""Load runs for selected session."""
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
cursor = conn.execute("""
|
||||
SELECT DISTINCT
|
||||
t.run_no,
|
||||
COALESCE(u.command_name, i.command_name, 'Unknown') as command_name,
|
||||
COUNT(*) as sample_count
|
||||
FROM telemetry_decoded t
|
||||
LEFT JOIN uart_commands u ON t.run_command_id = u.command_id
|
||||
LEFT JOIN i2c_commands i ON t.run_command_id = i.command_id
|
||||
WHERE t.session_id = ?
|
||||
GROUP BY t.run_no
|
||||
ORDER BY t.run_no
|
||||
""", (session_id,))
|
||||
|
||||
runs = cursor.fetchall()
|
||||
|
||||
self.runs_list.clear()
|
||||
|
||||
for run_no, command_name, sample_count in runs:
|
||||
item = QListWidgetItem(f"Run {run_no}: {command_name} ({sample_count:,} samples)")
|
||||
item.setData(Qt.ItemDataRole.UserRole, run_no)
|
||||
item.setCheckState(Qt.CheckState.Unchecked)
|
||||
self.runs_list.addItem(item)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to load runs:\n{str(e)}")
|
||||
|
||||
def _load_statistics(self, session_id: str):
|
||||
"""Load statistics for selected session."""
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
# Get statistics
|
||||
cursor = conn.execute("""
|
||||
SELECT
|
||||
COUNT(DISTINCT run_no) as total_runs,
|
||||
COUNT(*) as total_samples,
|
||||
MIN(t_ns) as start_time,
|
||||
MAX(t_ns) as end_time
|
||||
FROM telemetry_decoded
|
||||
WHERE session_id = ?
|
||||
""", (session_id,))
|
||||
|
||||
stats = cursor.fetchone()
|
||||
|
||||
if stats and stats[0] > 0:
|
||||
total_runs, total_samples, start_time, end_time = stats
|
||||
|
||||
# Calculate duration
|
||||
duration_ns = end_time - start_time
|
||||
duration_s = duration_ns / 1_000_000_000
|
||||
|
||||
# Format statistics
|
||||
stats_text = f"• Total Runs: {total_runs}\n"
|
||||
stats_text += f"• Total Samples: {total_samples:,}\n"
|
||||
stats_text += f"• Avg Samples/Run: {total_samples // total_runs:,}\n"
|
||||
stats_text += f"• Duration: {duration_s:.1f} seconds\n"
|
||||
|
||||
self.label_stats.setText(stats_text)
|
||||
else:
|
||||
self.label_stats.setText("No data available for this session")
|
||||
|
||||
except Exception as e:
|
||||
self.label_stats.setText(f"Error loading statistics: {str(e)}")
|
||||
|
||||
def _on_notes_changed(self):
|
||||
"""Handle notes text change - auto-save."""
|
||||
if not self.selected_session_id:
|
||||
return
|
||||
|
||||
notes = self.text_notes.toPlainText()
|
||||
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
conn.execute("""
|
||||
UPDATE sessions
|
||||
SET notes = ?
|
||||
WHERE session_id = ?
|
||||
""", (notes, self.selected_session_id))
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
print(f"Failed to save notes: {e}")
|
||||
|
||||
def _rename_session(self):
|
||||
"""Rename selected session."""
|
||||
if not self.selected_session_id:
|
||||
return
|
||||
|
||||
# Get current name
|
||||
current_name = self.label_session_id.text().replace("Session: ", "")
|
||||
|
||||
# Ask for new name
|
||||
new_name, ok = QInputDialog.getText(
|
||||
self,
|
||||
"Rename Session",
|
||||
"Enter new session name:",
|
||||
text=current_name
|
||||
)
|
||||
|
||||
if not ok or not new_name.strip():
|
||||
return
|
||||
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
# Update session name in sessions table
|
||||
conn.execute("""
|
||||
UPDATE sessions
|
||||
SET session_name = ?
|
||||
WHERE session_id = ?
|
||||
""", (new_name.strip(), self.selected_session_id))
|
||||
|
||||
# Update session name in telemetry_raw table
|
||||
conn.execute("""
|
||||
UPDATE telemetry_raw
|
||||
SET session_name = ?
|
||||
WHERE session_id = ?
|
||||
""", (new_name.strip(), self.selected_session_id))
|
||||
|
||||
# Update session name in telemetry_decoded table
|
||||
conn.execute("""
|
||||
UPDATE telemetry_decoded
|
||||
SET session_name = ?
|
||||
WHERE session_id = ?
|
||||
""", (new_name.strip(), self.selected_session_id))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Reload sessions
|
||||
self._load_sessions()
|
||||
|
||||
# Reload details
|
||||
self._load_session_details(self.selected_session_id)
|
||||
|
||||
QMessageBox.information(self, "Success", "Session renamed successfully!")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to rename session:\n{str(e)}")
|
||||
|
||||
def _delete_session(self):
|
||||
"""Delete selected session."""
|
||||
if not self.selected_session_id:
|
||||
return
|
||||
|
||||
# Confirm
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm Delete",
|
||||
f"Delete session '{self.selected_session_id}'?\n\n"
|
||||
"This will delete all runs and data for this session.\n"
|
||||
"This action cannot be undone!",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
conn.execute("DELETE FROM telemetry_decoded WHERE session_id = ?", (self.selected_session_id,))
|
||||
conn.execute("DELETE FROM telemetry_raw WHERE session_id = ?", (self.selected_session_id,))
|
||||
conn.execute("DELETE FROM sessions WHERE session_id = ?", (self.selected_session_id,))
|
||||
conn.commit()
|
||||
|
||||
self.selected_session_id = None
|
||||
self._load_sessions()
|
||||
self._set_details_enabled(False)
|
||||
|
||||
QMessageBox.information(self, "Success", "Session deleted successfully!")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to delete session:\n{str(e)}")
|
||||
|
||||
def _export_session(self):
|
||||
"""Export selected session to CSV."""
|
||||
if not self.selected_session_id:
|
||||
return
|
||||
|
||||
# Get save location
|
||||
filename = f"{self.selected_session_id}_export.csv"
|
||||
filepath, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Export Session",
|
||||
filename,
|
||||
"CSV Files (*.csv);;All Files (*)"
|
||||
)
|
||||
|
||||
if not filepath:
|
||||
return
|
||||
|
||||
try:
|
||||
import csv
|
||||
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
# Get all data for session
|
||||
cursor = conn.execute("""
|
||||
SELECT
|
||||
session_id, session_name, run_no, t_ns, time_ms,
|
||||
motor_current, encoder_value, relative_encoder_value,
|
||||
v24_pec_diff, pwm,
|
||||
i2c_raw14, i2c_zero_raw14, i2c_delta_raw14,
|
||||
i2c_angle_deg, i2c_zero_angle_deg,
|
||||
angular_velocity, angular_acceleration
|
||||
FROM telemetry_decoded
|
||||
WHERE session_id = ?
|
||||
ORDER BY run_no, t_ns
|
||||
""", (self.selected_session_id,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
# Write to CSV
|
||||
with open(filepath, 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
|
||||
# Header
|
||||
writer.writerow([
|
||||
'session_id', 'session_name', 'run_no', 't_ns', 'time_ms',
|
||||
'motor_current', 'encoder_value', 'relative_encoder_value',
|
||||
'v24_pec_diff', 'pwm',
|
||||
'i2c_raw14', 'i2c_zero_raw14', 'i2c_delta_raw14',
|
||||
'i2c_angle_deg', 'i2c_zero_angle_deg',
|
||||
'angular_velocity', 'angular_acceleration'
|
||||
])
|
||||
|
||||
# Data
|
||||
writer.writerows(rows)
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Export Complete",
|
||||
f"Session exported successfully!\n\n"
|
||||
f"File: {filepath}\n"
|
||||
f"Rows: {len(rows):,}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to export session:\n{str(e)}")
|
||||
|
||||
def _delete_selected_runs(self):
|
||||
"""Delete selected runs from session."""
|
||||
if not self.selected_session_id:
|
||||
return
|
||||
|
||||
# Get checked runs
|
||||
selected_runs = []
|
||||
for i in range(self.runs_list.count()):
|
||||
item = self.runs_list.item(i)
|
||||
if item.checkState() == Qt.CheckState.Checked:
|
||||
run_no = item.data(Qt.ItemDataRole.UserRole)
|
||||
selected_runs.append(run_no)
|
||||
|
||||
if not selected_runs:
|
||||
QMessageBox.warning(self, "No Selection", "Please select runs to delete.")
|
||||
return
|
||||
|
||||
# Confirm
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm Delete",
|
||||
f"Delete {len(selected_runs)} run(s) from session?\n\n"
|
||||
"This will permanently delete all data for these runs.\n"
|
||||
"This action cannot be undone!",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
for run_no in selected_runs:
|
||||
conn.execute("""
|
||||
DELETE FROM telemetry_decoded
|
||||
WHERE session_id = ? AND run_no = ?
|
||||
""", (self.selected_session_id, run_no))
|
||||
|
||||
conn.execute("""
|
||||
DELETE FROM telemetry_raw
|
||||
WHERE session_id = ? AND run_no = ?
|
||||
""", (self.selected_session_id, run_no))
|
||||
|
||||
# Update total_runs in sessions table
|
||||
cursor = conn.execute("""
|
||||
SELECT COUNT(DISTINCT run_no)
|
||||
FROM telemetry_decoded
|
||||
WHERE session_id = ?
|
||||
""", (self.selected_session_id,))
|
||||
|
||||
new_run_count = cursor.fetchone()[0]
|
||||
|
||||
conn.execute("""
|
||||
UPDATE sessions
|
||||
SET total_runs = ?
|
||||
WHERE session_id = ?
|
||||
""", (new_run_count, self.selected_session_id))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Reload details
|
||||
self._load_session_details(self.selected_session_id)
|
||||
self._load_sessions()
|
||||
|
||||
QMessageBox.information(self, "Success", f"Deleted {len(selected_runs)} run(s) successfully!")
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to delete runs:\n{str(e)}")
|
||||
|
||||
def _select_all_sessions(self):
|
||||
"""Select all checkboxes in session table."""
|
||||
for row in range(self.session_table.rowCount()):
|
||||
checkbox_widget = self.session_table.cellWidget(row, 0)
|
||||
checkbox = checkbox_widget.findChild(QCheckBox)
|
||||
if checkbox:
|
||||
checkbox.setChecked(True)
|
||||
|
||||
def _clear_selection(self):
|
||||
"""Clear all checkboxes in session table."""
|
||||
for row in range(self.session_table.rowCount()):
|
||||
checkbox_widget = self.session_table.cellWidget(row, 0)
|
||||
checkbox = checkbox_widget.findChild(QCheckBox)
|
||||
if checkbox:
|
||||
checkbox.setChecked(False)
|
||||
|
||||
def _delete_selected_sessions(self):
|
||||
"""Delete all checked sessions."""
|
||||
# Get checked sessions
|
||||
selected_sessions = []
|
||||
for row in range(self.session_table.rowCount()):
|
||||
checkbox_widget = self.session_table.cellWidget(row, 0)
|
||||
checkbox = checkbox_widget.findChild(QCheckBox)
|
||||
if checkbox and checkbox.isChecked():
|
||||
session_id = self.session_table.item(row, 1).data(Qt.ItemDataRole.UserRole)
|
||||
session_name = self.session_table.item(row, 1).text()
|
||||
selected_sessions.append((session_id, session_name))
|
||||
|
||||
if not selected_sessions:
|
||||
QMessageBox.warning(self, "No Selection", "Please select sessions to delete.")
|
||||
return
|
||||
|
||||
# Confirm
|
||||
session_list = "\n".join([f"• {name}" for _, name in selected_sessions])
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Confirm Delete",
|
||||
f"Delete {len(selected_sessions)} session(s)?\n\n{session_list}\n\n"
|
||||
"This will permanently delete all data for these sessions.\n"
|
||||
"This action cannot be undone!",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
if reply != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
|
||||
try:
|
||||
conn = self.db_manager.get_connection()
|
||||
|
||||
for session_id, _ in selected_sessions:
|
||||
conn.execute("DELETE FROM telemetry_decoded WHERE session_id = ?", (session_id,))
|
||||
conn.execute("DELETE FROM telemetry_raw WHERE session_id = ?", (session_id,))
|
||||
conn.execute("DELETE FROM sessions WHERE session_id = ?", (session_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Vacuum database
|
||||
self.db_manager.vacuum()
|
||||
|
||||
self.selected_session_id = None
|
||||
self._load_sessions()
|
||||
self._set_details_enabled(False)
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Success",
|
||||
f"Deleted {len(selected_sessions)} session(s) successfully!\n\n"
|
||||
"Database vacuumed."
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to delete sessions:\n{str(e)}")
|
||||
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue