From 96e2f45c506328db9bbd1d20669065aa220a953d Mon Sep 17 00:00:00 2001 From: x01da Date: Thu, 3 Sep 2026 08:24:29 +0200 Subject: [PATCH] wip --- .../widgets/scheduler/item_dialog.py | 115 +++++++++++++++++- .../widgets/scheduler/notifications.py | 2 +- .../widgets/scheduler/qt_widgets.py | 61 ++++++++++ .../widgets/scheduler/scheduler.py | 74 ++--------- 4 files changed, 187 insertions(+), 65 deletions(-) create mode 100644 debye_bec/bec_widgets/widgets/scheduler/qt_widgets.py diff --git a/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py b/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py index 69c4942..3f5bf6d 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py +++ b/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py @@ -41,15 +41,19 @@ from qtpy.QtWidgets import ( QDialogButtonBox, QDoubleSpinBox, QFormLayout, + QGroupBox, + QHBoxLayout, QLabel, QLineEdit, QMessageBox, + QPushButton, QTabWidget, QVBoxLayout, QWidget, ) from ..scan_control_xas.scan_control_xas import ScanControlXAS +from .qt_widgets import MyButton logger = bec_logger.logger @@ -204,10 +208,119 @@ class ScheduleItemDialog(QDialog): ) ) self.custom_edit = QLineEdit() + # TODO Change to a different placeholder text self.custom_edit.setPlaceholderText("scans.xas_simple_scan(12000, 14000, 2, 10)") layout.addWidget(self.custom_edit) + + ic_form = self._create_ionization_chamber_form() + if ic_form is not None: + layout.addWidget(ic_form) + + reffoil_form = self._create_reffoil_form() + if reffoil_form is not None: + layout.addWidget(reffoil_form) + layout.addStretch(1) - self.tabs.addTab(tab, "Custom") + self.tabs.addTab(tab, "Other") + + def _create_ionization_chamber_form(self): + if all(key in self._dev for key in ("ic0", "ic1", "ic2")): + ic_group = QGroupBox("Ionization chamber filling") + layout = QVBoxLayout(ic_group) + form = QFormLayout() + layout.addLayout(form) + form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.FieldsStayAtSizeHint) + self.ic_selector = QComboBox() + self.ic_selector.addItems(["IC0", "IC1", "IC2"]) + gases = ["He", "N2", "Ar", "Kr"] + self.gas1 = QComboBox() + self.gas2 = QComboBox() + self.gas1.addItems(gases) + self.gas2.addItems(gases) + self.conc1 = QDoubleSpinBox() + self.conc2 = QDoubleSpinBox() + for conc in [self.conc1, self.conc2]: + conc.setDecimals(0) + conc.setSuffix(" %") + conc.setMinimum(0) + conc.setMaximum(100) + conc.setSingleStep(1) + self.pressure = QDoubleSpinBox() + self.pressure.setDecimals(3) + self.pressure.setSuffix(" bar abs") + self.pressure.setMinimum(1) + self.pressure.setMaximum(3) + self.pressure.setSingleStep(0.1) + + form.addRow("Ionization chamber", self.ic_selector) + form.addRow("Gas 1", self.gas1) + form.addRow("Concentration 1", self.conc1) + form.addRow("Gas 2", self.gas2) + form.addRow("Concentration 2", self.conc2) + form.addRow("Pressure", self.pressure) + + button_layout = QHBoxLayout() + generate_cmd = MyButton("Generate command", "default") + button_layout.addWidget(generate_cmd) + button_layout.addStretch(1) + layout.addLayout(button_layout) + + self.conc1.valueChanged.connect(self._equalize_ic_conc) + self.conc2.valueChanged.connect(self._equalize_ic_conc) + generate_cmd.clicked.connect(self._generate_ic_command) + + return ic_group + return None + + def _equalize_ic_conc(self, new_val): + if self.conc1.value() == new_val: # conc1 was changed + self.conc2.setValue(100 - new_val) + else: + self.conc1.setValue(100 - new_val) + + def _generate_ic_command(self): + match self.ic_selector.currentText(): + case "IC0": + ic = "ic0" + case "IC1": + ic = "ic1" + case "IC2": + ic = "ic2" + cmd = ( + f"dev.{ic}.fill(" + + f"gas1='{self.gas1.currentText()}', conc1={self.conc1.value()}, " + + f"gas2='{self.gas2.currentText()}', conc2={self.conc2.value()}, " + + f"pressure={self.pressure.value()}, wait=True)" + ) + self.custom_edit.setText(cmd) + + def _create_reffoil_form(self): + if "reffoilchanger" in self._dev: + reffoil_group = QGroupBox("Reference foil changer") + layout = QVBoxLayout(reffoil_group) + form = QFormLayout() + layout.addLayout(form) + form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.FieldsStayAtSizeHint) + self.reffoil_selector = QComboBox() + available_foils = self._dev.reffoilchanger.get_all_foils() + self.reffoil_selector.addItems(available_foils) + + form.addRow("Reference foil", self.reffoil_selector) + + button_layout = QHBoxLayout() + generate_cmd = MyButton("Generate command", "default") + button_layout.addWidget(generate_cmd) + button_layout.addStretch(1) + layout.addLayout(button_layout) + + generate_cmd.clicked.connect(self._generate_reffoil_command) + + return reffoil_group + return None + + def _generate_reffoil_command(self): + cmd = f"dev.reffoilchanger.insert(ref='{self.reffoil_selector.currentText()}', wait=True)" + self.custom_edit.setText(cmd) def _collect_custom_result(self) -> dict: text = self.custom_edit.text().strip() diff --git a/debye_bec/bec_widgets/widgets/scheduler/notifications.py b/debye_bec/bec_widgets/widgets/scheduler/notifications.py index 16b9f34..bd8fbf3 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/notifications.py +++ b/debye_bec/bec_widgets/widgets/scheduler/notifications.py @@ -129,7 +129,7 @@ def notify_item_finished( text = text + f"\nScan number: {scan_number}" text = text + f"\n\nCommand: {command}" if error: - text += f"\n{error.strip().splitlines()[-1]}" + text += f"\n\n{error.strip().splitlines()[-1]}" try: send_notification(client, settings, text) diff --git a/debye_bec/bec_widgets/widgets/scheduler/qt_widgets.py b/debye_bec/bec_widgets/widgets/scheduler/qt_widgets.py new file mode 100644 index 0000000..4f47e49 --- /dev/null +++ b/debye_bec/bec_widgets/widgets/scheduler/qt_widgets.py @@ -0,0 +1,61 @@ +from bec_widgets.utils.colors import get_accent_colors +from qtpy.QtCore import Qt, Signal + +# pylint: disable=E0611 +from qtpy.QtGui import QKeySequence +from qtpy.QtWidgets import QListWidget, QPushButton + + +class MyListWidget(QListWidget): + deletePressed = Signal() + emptySpaceClicked = Signal() + copyPressed = Signal() + pastePressed = Signal() + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Delete and self.currentItem() is not None: + self.deletePressed.emit() + event.accept() + return + + if event.matches(QKeySequence.StandardKey.Copy) and self.currentItem() is not None: + self.copyPressed.emit() + event.accept() + return + + if event.matches(QKeySequence.StandardKey.Paste): + self.pastePressed.emit() + event.accept() + return + + super().keyPressEvent(event) + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and self.itemAt(event.position().toPoint()) is None: + self.clearSelection() + self.setCurrentRow(-1) + self.emptySpaceClicked.emit() + return + + super().mousePressEvent(event) + + +class MyButton(QPushButton): + def __init__(self, text="", color="default", parent=None): + self.color = color + super().__init__(text, parent) + self.apply_theme() + + def apply_theme(self): + if self.isEnabled(): + colors = get_accent_colors() + color = getattr(colors, self.color).name() + self.setStyleSheet(f"QPushButton {{ background-color: {color}; color: white; }}") + else: + self.setStyleSheet( + "QPushButton {{background-color: rgb(120, 120, 120); color: white;}}" + ) + + def setEnabled(self, enable: bool = True): + super().setEnabled(enable) + self.apply_theme() diff --git a/debye_bec/bec_widgets/widgets/scheduler/scheduler.py b/debye_bec/bec_widgets/widgets/scheduler/scheduler.py index 78cd84c..1a42bbe 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/scheduler.py +++ b/debye_bec/bec_widgets/widgets/scheduler/scheduler.py @@ -22,13 +22,11 @@ from bec_lib.messages import VariableMessage from bec_qthemes import material_icon from bec_widgets.utils.bec_connector import ConnectionConfig from bec_widgets.utils.bec_widget import BECWidget -from bec_widgets.utils.colors import get_accent_colors from bec_widgets.utils.error_popups import SafeSlot from pydantic import ValidationError # pylint: disable=E0611 from qtpy.QtCore import Qt, QTimer, Signal -from qtpy.QtGui import QKeySequence from qtpy.QtWidgets import ( QApplication, QDialog, @@ -37,11 +35,9 @@ from qtpy.QtWidgets import ( QHBoxLayout, QLabel, QLineEdit, - QListWidget, QListWidgetItem, QMessageBox, QPlainTextEdit, - QPushButton, QVBoxLayout, QWidget, ) @@ -58,6 +54,7 @@ from .notifications import ( notify_item_finished, notify_schedule_state, ) +from .qt_widgets import MyButton, MyListWidget from .schedule_item import Schedule, ScheduleItem from .schedule_logic import index_of, pick_next_runnable, protected_prefix_length @@ -97,60 +94,6 @@ class ScheduleWidgetConfig(ConnectionConfig): schedule_name: str = "default_schedule" -class MyListWidget(QListWidget): - deletePressed = Signal() - emptySpaceClicked = Signal() - copyPressed = Signal() - pastePressed = Signal() - - def keyPressEvent(self, event): - if event.key() == Qt.Key_Delete and self.currentItem() is not None: - self.deletePressed.emit() - event.accept() - return - - if event.matches(QKeySequence.StandardKey.Copy) and self.currentItem() is not None: - self.copyPressed.emit() - event.accept() - return - - if event.matches(QKeySequence.StandardKey.Paste): - self.pastePressed.emit() - event.accept() - return - - super().keyPressEvent(event) - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton and self.itemAt(event.position().toPoint()) is None: - self.clearSelection() - self.setCurrentRow(-1) - self.emptySpaceClicked.emit() - return - - super().mousePressEvent(event) - - -class MyButton(QPushButton): - def __init__(self, text="", color="default", parent=None): - self.color = color - super().__init__(text, parent) - - def apply_theme(self): - if self.isEnabled(): - colors = get_accent_colors() - color = getattr(colors, self.color).name() - self.setStyleSheet(f"QPushButton {{ background-color: {color}; color: white; }}") - else: - self.setStyleSheet( - "QPushButton {{background-color: rgb(120, 120, 120); color: white;}}" - ) - - def setEnabled(self, enable: bool = True): - super().setEnabled(enable) - self.apply_theme() - - class Scheduler(BECWidget, QWidget): """Schedule, persist and execute a sequence of BEC scan/device commands.""" @@ -1192,7 +1135,7 @@ class Scheduler(BECWidget, QWidget): def _run_all(self, repeat_aborted_item): notify_schedule_state( - self.client, self.schedule.notifications, self.schedule_name, "started" + self.client, self.schedule.notifications, self.schedule.schedule_name, "started" ) namespace = {"scans": self.scans, "dev": self.dev} while not self._abort_requested and not self._closing.is_set(): @@ -1207,15 +1150,18 @@ class Scheduler(BECWidget, QWidget): self._execute_item(next_item, namespace) if self._abort_requested: notify_schedule_state( - self.client, self.schedule.notifications, self.schedule_name, "aborted" + self.client, self.schedule.notifications, self.schedule.schedule_name, "aborted" ) elif self._closing.is_set(): notify_schedule_state( - self.client, self.schedule.notifications, self.schedule_name, "widget_closed" + self.client, + self.schedule.notifications, + self.schedule.schedule_name, + "widget_closed", ) else: notify_schedule_state( - self.client, self.schedule.notifications, self.schedule_name, "finished" + self.client, self.schedule.notifications, self.schedule.schedule_name, "finished" ) with self._lock: logger.info("in _run_all, set is_running to false") @@ -1252,7 +1198,9 @@ class Scheduler(BECWidget, QWidget): item.request_id = getattr(request, "requestID", None) self._persist_locked() - report.wait() + # RPC commands may not return a status, thus have no wait() + if item.kind != "custom": + report.wait() if self._closing.is_set(): # The widget was closed mid-item; exit without changing status in Redis.