GUI: added automation_progress.py and updated comapct automation view
Build and Publish / test (push) Successful in 1m39s
Build and Publish / build (push) Successful in 18s
Build and Publish / Build and Deploy Docs (push) Successful in 38s

This commit is contained in:
appleb_m committed 2026-06-09 13:37:33 +02:00
1 parent 86ceebb120
commit 675549c1c2
3 files changed
+327 -96

No files matched your search

+7
View File
@@ -412,6 +412,9 @@ class MainWindow(QMainWindow):
self.job_list_panel.step_through_changed.connect(
self.compact_automation_panel.set_step_through
)
self.job_list_panel.samples_in_queue_changed.connect(
self.compact_automation_panel.set_samples_in_queue
)
self.job_list_panel.samples_in_queue_changed.connect(
self._refresh_compact_queue_preview
)
@@ -422,6 +425,9 @@ class MainWindow(QMainWindow):
self.automation_progress_panel.set_running(self.job_list_panel.is_running())
self.compact_automation_panel.set_running(self.job_list_panel.is_running())
self.compact_automation_panel.set_step_through(self.job_list_panel.is_step_through())
self.compact_automation_panel.set_samples_in_queue(
len(self.job_list_panel.table_model.samples)
)
self._refresh_compact_queue_preview()
# smargon trace panel
@@ -515,6 +521,7 @@ class MainWindow(QMainWindow):
self.daq.baton_response_result.connect(self._on_baton_response_result)
self.daq.baton_timeout_checked.connect(self._on_baton_timeout_checked)
self.daq.automation_progress.connect(self.automation_progress_panel.set_progress)
self.daq.automation_progress.connect(self.compact_automation_panel.set_progress)
self.status_bar.baton_request_received.connect(self._show_baton_request_dialog)
self.status_bar.baton_request_accepted.connect(self._accept_baton_request)
+139 -96
View File
@@ -10,7 +10,9 @@ from PySide6.QtWidgets import (
QWidget,
)
from aare.common.automation_models import AutomationProgress
from aare.common.models import SampleShortInfo
from aare.gui.widgets.automation_progress import CompactAutomationProgressStrip
class CompactAutomationPanel(QFrame):
@@ -35,162 +37,169 @@ class CompactAutomationPanel(QFrame):
self.setStyleSheet(
"""
QFrame#compactAutomationPanel {
background: #eef4ff;
background: #edf3fb;
border: none;
border-radius: 18px;
}
QFrame#compactTopCard,
QFrame#compactProgressCard,
QFrame#compactControlsCard {
background: #ffffff;
border: 1px solid #d9e4f2;
border-radius: 16px;
}
QFrame#compactAutomationHeader {
background: #1f3b63;
border-radius: 14px;
QFrame#compactQueueItem {
background: #f7faff;
border: 1px solid #dbe6f5;
border-radius: 12px;
}
QLabel#compactAutomationTitle {
color: white;
font-size: 20px;
QLabel#compactQueueTitle {
color: #4b5563;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
QLabel#compactQueueValue {
color: #0f172a;
font-size: 14px;
font-weight: 700;
}
QLabel#compactAutomationSubtitle {
color: #d7e6ff;
QLabel#compactDashboardLabel {
color: #334155;
font-size: 12px;
font-weight: 700;
}
QToolButton#compactMenuButton {
background: rgba(255, 255, 255, 0.14);
color: white;
border: 1px solid rgba(255, 255, 255, 0.22);
border-radius: 18px;
padding: 8px 12px;
background: #eff6ff;
color: #1d4ed8;
border: 1px solid #bfdbfe;
border-radius: 14px;
padding: 10px 14px;
font-size: 18px;
font-weight: 700;
}
QToolButton#compactMenuButton:hover {
background: rgba(255, 255, 255, 0.24);
background: #dbeafe;
}
QFrame#compactInfoPanel {
background: white;
border: 1px solid #d5e0f2;
border-radius: 14px;
QFrame#compactAutomationProgressStrip {
background: transparent;
border: none;
}
QLabel#compactSectionTitle {
color: #1f3b63;
font-size: 14px;
font-weight: 700;
}
QLabel#compactInfoLabel {
background: #f6f9ff;
border: 1px solid #dde7f6;
border-radius: 10px;
color: #24364d;
padding: 10px 12px;
QLabel#compactProgressSummary {
color: #1e293b;
font-size: 13px;
padding: 2px 2px 6px 2px;
}
QLabel#compactProgressStep {
background: #f8fbff;
border: 1px solid #dbe6f5;
border-radius: 10px;
padding: 8px 6px;
}
QPushButton#compactPrimaryButton {
background: #2f6fed;
background: #2563eb;
color: white;
border: none;
border-radius: 12px;
padding: 12px 16px;
font-size: 14px;
border-radius: 14px;
padding: 14px 18px;
font-size: 15px;
font-weight: 700;
}
QPushButton#compactPrimaryButton:hover {
background: #255fd0;
background: #1d4ed8;
}
QPushButton#compactSecondaryButton {
background: white;
color: #1f3b63;
border: 1px solid #c8d7ef;
border-radius: 12px;
padding: 12px 16px;
background: #ffffff;
color: #0f172a;
border: 1px solid #cbd5e1;
border-radius: 14px;
padding: 14px 18px;
font-size: 14px;
font-weight: 700;
}
QPushButton#compactSecondaryButton:hover,
QToolButton#compactSecondaryButton:hover {
background: #f4f8ff;
background: #f8fafc;
}
QToolButton#compactSecondaryButton {
background: white;
color: #1f3b63;
border: 1px solid #c8d7ef;
border-radius: 12px;
padding: 12px 16px;
font-size: 16px;
background: #ffffff;
color: #0f172a;
border: 1px solid #cbd5e1;
border-radius: 14px;
padding: 14px 18px;
font-size: 14px;
font-weight: 700;
}
"""
)
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setSpacing(14)
main_layout.setContentsMargins(18, 18, 18, 18)
main_layout.setSpacing(12)
header = QFrame(self)
header.setObjectName("compactAutomationHeader")
header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(14, 12, 14, 12)
header_layout.setSpacing(10)
top_card = QFrame(self)
top_card.setObjectName("compactTopCard")
top_layout = QHBoxLayout(top_card)
top_layout.setContentsMargins(12, 12, 12, 12)
top_layout.setSpacing(10)
self.current_card = self._build_queue_item("Current", "—")
self.next_card = self._build_queue_item("Next", "—")
self.next_next_card = self._build_queue_item("Then", "—")
top_layout.addWidget(self.current_card["frame"], 2)
top_layout.addWidget(self.next_card["frame"], 2)
top_layout.addWidget(self.next_next_card["frame"], 2)
self.menu_button = QToolButton(self)
self.menu_button.setObjectName("compactMenuButton")
self.menu_button.setText("☰")
self.menu_button.setToolTip("Return to full view")
self.menu_button.setToolTip("Return to main view")
self.menu_button.clicked.connect(self.show_full_view_requested.emit)
top_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop)
title_container = QWidget(self)
title_layout = QVBoxLayout(title_container)
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(2)
self.title_label = QLabel("Automation View", self)
self.title_label.setObjectName("compactAutomationTitle")
self.subtitle_label = QLabel("Focused queue control and sample camera", self)
self.subtitle_label.setObjectName("compactAutomationSubtitle")
title_layout.addWidget(self.title_label)
title_layout.addWidget(self.subtitle_label)
header_layout.addWidget(self.menu_button, 0, Qt.AlignmentFlag.AlignTop)
header_layout.addWidget(title_container, 1)
main_layout.addWidget(header)
main_layout.addWidget(top_card)
main_layout.addWidget(camera_widget, 1)
info_panel = QFrame(self)
info_panel.setObjectName("compactInfoPanel")
info_layout = QVBoxLayout(info_panel)
info_layout.setContentsMargins(14, 14, 14, 14)
info_layout.setSpacing(8)
progress_card = QFrame(self)
progress_card.setObjectName("compactProgressCard")
progress_layout = QVBoxLayout(progress_card)
progress_layout.setContentsMargins(12, 10, 12, 10)
progress_layout.setSpacing(6)
info_title = QLabel("Queue preview", self)
info_title.setObjectName("compactSectionTitle")
progress_title = QLabel("Automation progress", self)
progress_title.setObjectName("compactDashboardLabel")
progress_layout.addWidget(progress_title)
self.current_label = QLabel("Current: —", self)
self.next_label = QLabel("Next: —", self)
self.next_next_label = QLabel("Then: —", self)
self.progress_strip = CompactAutomationProgressStrip(self)
progress_layout.addWidget(self.progress_strip)
for label in (self.current_label, self.next_label, self.next_next_label):
label.setObjectName("compactInfoLabel")
label.setWordWrap(True)
main_layout.addWidget(progress_card)
info_layout.addWidget(info_title)
info_layout.addWidget(self.current_label)
info_layout.addWidget(self.next_label)
info_layout.addWidget(self.next_next_label)
controls_card = QFrame(self)
controls_card.setObjectName("compactControlsCard")
controls_layout = QVBoxLayout(controls_card)
controls_layout.setContentsMargins(12, 12, 12, 12)
controls_layout.setSpacing(10)
main_layout.addWidget(info_panel)
controls_title = QLabel("Controls", self)
controls_title.setObjectName("compactDashboardLabel")
controls_layout.addWidget(controls_title)
button_row = QHBoxLayout()
button_row.setSpacing(10)
@@ -226,12 +235,38 @@ class CompactAutomationPanel(QFrame):
button_row.addWidget(self.step_button, 1)
button_row.addWidget(self.annotation_button, 1)
main_layout.addLayout(button_row)
controls_layout.addLayout(button_row)
main_layout.addWidget(controls_card)
def _build_queue_item(self, title: str, value: str) -> dict[str, QWidget | QLabel]:
frame = QFrame(self)
frame.setObjectName("compactQueueItem")
layout = QVBoxLayout(frame)
layout.setContentsMargins(12, 10, 12, 10)
layout.setSpacing(4)
title_label = QLabel(title, self)
title_label.setObjectName("compactQueueTitle")
value_label = QLabel(value, self)
value_label.setObjectName("compactQueueValue")
value_label.setWordWrap(True)
layout.addWidget(title_label)
layout.addWidget(value_label)
return {
"frame": frame,
"title": title_label,
"value": value_label,
}
@Slot(bool)
def set_running(self, running: bool) -> None:
self._running = bool(running)
self.play_pause_button.setText("⏸ Pause" if self._running else "▶ Play")
self.progress_strip.set_running(running)
@Slot(bool)
def set_step_through(self, enabled: bool) -> None:
@@ -240,6 +275,14 @@ class CompactAutomationPanel(QFrame):
self.step_button.setChecked(self._step_through)
self.step_button.blockSignals(False)
@Slot(int)
def set_samples_in_queue(self, count: int) -> None:
self.progress_strip.set_samples_in_queue(count)
@Slot(object)
def set_progress(self, progress: AutomationProgress) -> None:
self.progress_strip.set_progress(progress)
@Slot(object, object, object)
def set_samples(
self,
@@ -251,9 +294,9 @@ class CompactAutomationPanel(QFrame):
self._next_sample = next_sample
self._next_next_sample = next_next_sample
self.current_label.setText(f"Current: {self._format_sample(current_sample)}")
self.next_label.setText(f"Next: {self._format_sample(next_sample)}")
self.next_next_label.setText(f"Then: {self._format_sample(next_next_sample)}")
self.current_card["value"].setText(self._format_sample(current_sample))
self.next_card["value"].setText(self._format_sample(next_sample))
self.next_next_card["value"].setText(self._format_sample(next_next_sample))
def _on_step_toggled(self, checked: bool) -> None:
self._step_through = checked
+181
View File
@@ -0,0 +1,181 @@
from __future__ import annotations
import time
from datetime import datetime
from PySide6.QtCore import QTimer, Slot
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout, QWidget
from aare.common.automation_models import AutomationProgress, StepStatus, WorkflowStateKind
class CompactAutomationProgressStrip(QFrame):
DEFAULT_SAMPLE_ESTIMATE_S = 150.0
def __init__(self, parent: QWidget | None = None):
super().__init__(parent)
self._progress: AutomationProgress | None = None
self._queue_count = 0
self._running = False
self._step_labels: dict[WorkflowStateKind, QLabel] = {}
self._summary_label: QLabel | None = None
self._timer = QTimer(self)
self._timer.setInterval(1000)
self._timer.timeout.connect(self._refresh_live_view)
self.setObjectName("compactAutomationProgressStrip")
layout = QVBoxLayout(self)
layout.setContentsMargins(14, 12, 14, 12)
layout.setSpacing(10)
self._summary_label = QLabel("Automation idle", self)
self._summary_label.setObjectName("compactProgressSummary")
self._summary_label.setWordWrap(True)
layout.addWidget(self._summary_label)
steps_row = QHBoxLayout()
steps_row.setSpacing(8)
for step in (
WorkflowStateKind.MOUNT,
WorkflowStateKind.LOOP_CENTRE,
WorkflowStateKind.RASTER,
WorkflowStateKind.DATA_COLLECTION,
WorkflowStateKind.FINAL,
):
label = QLabel(self)
label.setObjectName("compactProgressStep")
label.setMinimumWidth(90)
label.setWordWrap(True)
steps_row.addWidget(label, 1)
self._step_labels[step] = label
layout.addLayout(steps_row)
self._apply_empty_state()
def _apply_empty_state(self) -> None:
if self._summary_label is not None:
self._summary_label.setText("Automation idle")
for step, label in self._step_labels.items():
label.setText(self._format_step_html(step, StepStatus.PENDING))
@staticmethod
def _step_title(step: WorkflowStateKind) -> str:
return {
WorkflowStateKind.MOUNT: "Mount",
WorkflowStateKind.LOOP_CENTRE: "Center",
WorkflowStateKind.RASTER: "Raster",
WorkflowStateKind.DATA_COLLECTION: "Collect",
WorkflowStateKind.FINAL: "Finish",
}.get(step, str(step.value))
@staticmethod
def _step_icon(status: StepStatus) -> str:
return {
StepStatus.PENDING: "○",
StepStatus.RUNNING: "◉",
StepStatus.SUCCESS: "✓",
StepStatus.FAILED: "✕",
StepStatus.SKIPPED: "↷",
StepStatus.PAUSED: "⏸",
}.get(status, "○")
@staticmethod
def _step_color(status: StepStatus) -> str:
return {
StepStatus.PENDING: "#64748b",
StepStatus.RUNNING: "#2563eb",
StepStatus.SUCCESS: "#15803d",
StepStatus.FAILED: "#b91c1c",
StepStatus.SKIPPED: "#64748b",
StepStatus.PAUSED: "#c2410c",
}.get(status, "#64748b")
def _format_step_html(self, step: WorkflowStateKind, status: StepStatus) -> str:
color = self._step_color(status)
icon = self._step_icon(status)
title = self._step_title(step)
return (
f"<div style='text-align:center;'>"
f"<div style='font-size:18px; color:{color}; font-weight:700;'>{icon}</div>"
f"<div style='font-size:12px; color:{color}; font-weight:700;'>{title}</div>"
f"</div>"
)
@staticmethod
def _format_duration(seconds: float | None) -> str:
if seconds is None or seconds <= 0:
return "0m 00s"
total = int(round(seconds))
minutes, secs = divmod(total, 60)
if minutes < 60:
return f"{minutes}m {secs:02d}s"
hours, minutes = divmod(minutes, 60)
return f"{hours}h {minutes:02d}m"
@staticmethod
def _format_eta(epoch_seconds: float | None) -> str:
if epoch_seconds is None:
return "N/A"
return datetime.fromtimestamp(epoch_seconds).strftime("%H:%M:%S")
def _refresh_live_view(self) -> None:
if self._progress is not None:
self.set_progress(self._progress)
@Slot(bool)
def set_running(self, running: bool) -> None:
self._running = bool(running)
if self._progress is not None:
self.set_progress(self._progress)
@Slot(int)
def set_samples_in_queue(self, count: int) -> None:
self._queue_count = max(0, int(count))
if self._progress is not None:
self.set_progress(self._progress)
@Slot(object)
def set_progress(self, progress: AutomationProgress) -> None:
self._progress = progress
any_running = any(step.status == StepStatus.RUNNING for step in progress.steps)
if any_running and self._running:
if not self._timer.isActive():
self._timer.start()
else:
self._timer.stop()
current_sample = progress.current_sample_name or "None"
avg_time = progress.avg_time_per_sample if progress.avg_time_per_sample > 0 else self.DEFAULT_SAMPLE_ESTIMATE_S
queue_remaining = avg_time * self._queue_count
eta = time.time() + queue_remaining if queue_remaining > 0 else None
state_text = "Paused"
state_color = "#c2410c"
if progress.finished and progress.success is True:
state_text = "Completed"
state_color = "#15803d"
elif progress.finished and progress.success is False:
state_text = "Failed"
state_color = "#b91c1c"
elif self._running:
state_text = "Running"
state_color = "#2563eb"
elif not self._running:
state_text = "Paused"
state_color = "#c2410c"
if self._summary_label is not None:
self._summary_label.setText(
f"<b>Status:</b> <span style='color:{state_color}; font-weight:700;'>{state_text}</span>"
f" &nbsp;&nbsp; <b>Current:</b> {current_sample}"
f" &nbsp;&nbsp; <b>Queue:</b> {self._queue_count}"
f" &nbsp;&nbsp; <b>ETA:</b> {self._format_duration(queue_remaining)}"
f" &nbsp;&nbsp; <b>Done by:</b> {self._format_eta(eta)}"
)
states = {step.step: step.status for step in progress.steps}
for step, label in self._step_labels.items():
label.setText(self._format_step_html(step, states.get(step, StepStatus.PENDING)))