From 8a657a3b25dc362356325343b256300a0200522e Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Sun, 2 Jun 2024 19:51:44 -0500 Subject: [PATCH 01/10] Added a bunch of functionality to the queueserver UI in Firefly. Finished out the run engine control actions, plus the ability to request the queue stop after the current scan. Status bar now shows how many items are in the queue. Autostart action responds to changes in the queueserver's autostart state. Bunch of other UX improvements too. --- src/firefly/application.py | 74 +++++++++++++++----- src/firefly/main_window.py | 26 ++++--- src/firefly/queue_client.py | 94 ++++++++++++++++++++++---- src/firefly/tests/test_queue_client.py | 86 ++++++++++++++++++++++- src/haven/instrument/ion_chamber.py | 4 +- src/haven/instrument/motor.py | 3 +- 6 files changed, 242 insertions(+), 45 deletions(-) diff --git a/src/firefly/application.py b/src/firefly/application.py index 9c4dd6c7..f31e9f4b 100644 --- a/src/firefly/application.py +++ b/src/firefly/application.py @@ -16,6 +16,7 @@ from qtpy import QtCore, QtWidgets from qtpy.QtCore import Signal from qtpy.QtWidgets import QAction +from qtpy.QtGui import QKeySequence from haven import load_config from haven import load_instrument as load_haven_instrument @@ -87,10 +88,12 @@ class FireflyApplication(PyDMApplication): # Signals responding to queueserver changes queue_length_changed = Signal(int) queue_status_changed = Signal(dict) + queue_in_use_changed = Signal(bool) # length > 0, or running queue_environment_opened = Signal(bool) # Opened or closed queue_environment_state_changed = Signal(str) # New state queue_manager_state_changed = Signal(str) # New state queue_re_state_changed = Signal(str) # New state + queue_empty_changed = Signal(bool) # Whether queue is empty # queue_devices_changed = Signal(dict) # New list of devices # Actions for controlling the queueserver @@ -103,6 +106,7 @@ class FireflyApplication(PyDMApplication): halt_runengine_action: QAction start_queue: QAction queue_autostart_action: QAction + queue_stop_action: QAction queue_open_environment_action: QAction check_queue_status_action: QAction @@ -126,11 +130,15 @@ def __del__(self): self._queue_thread.wait(msecs=5000) assert not self._queue_thread.isRunning() - def _setup_window_action(self, action_name: str, text: str, slot: QtCore.Slot): + def _setup_window_action(self, action_name: str, text: str, slot: QtCore.Slot, shortcut=None, icon=None): action = QtWidgets.QAction(self) action.setObjectName(action_name) action.setText(text) action.triggered.connect(slot) + if shortcut is not None: + action.setShortcut(QKeySequence(shortcut)) + if icon is not None: + action.setIcon(qta.icon(icon)) setattr(self, action_name, action) return action @@ -255,13 +263,13 @@ def setup_window_actions(self): ) # Actions for executing plans plans = [ - # (plan_name, text, display file) - ("count", "&Count", "count.py"), - ("line_scan", "&Line scan", "line_scan.py"), - ("xafs_scan", "&XAFS Scan", "xafs_scan.py"), + # (plan_name, text, display file, shortcut) + ("count", "&Count", "count.py", "Ctrl+Shift+C"), + ("line_scan", "&Line scan", "line_scan.py", "Ctrl+Shift+L"), + ("xafs_scan", "&XAFS Scan", "xafs_scan.py", "Ctrl+Shift+X"), ] self.plan_actions = [] - for plan_name, text, display_file in plans: + for plan_name, text, display_file, shortcut in plans: slot = partial( self.show_plan_window, name=plan_name, display_file=display_file ) @@ -271,6 +279,8 @@ def setup_window_actions(self): action.setObjectName(action_name) action.setText(text) action.triggered.connect(slot) + if shortcut is not None: + action.setShortcut(QKeySequence(shortcut)) self.plan_actions.append(action) # Action for showing the run browser window self._setup_window_action( @@ -283,6 +293,7 @@ def setup_window_actions(self): action_name="launch_queuemonitor_action", text="Queue Monitor", slot=self.launch_queuemonitor, + shortcut="Ctrl+q", ) # Action for showing the beamline scheduling window self._setup_window_action( @@ -301,6 +312,7 @@ def setup_window_actions(self): action_name="show_voltmeters_window_action", text="&Voltmeters", slot=self.show_voltmeters_window, + icon="ph.faders-horizontal", ) # Launch log window self._setup_window_action( @@ -313,6 +325,7 @@ def setup_window_actions(self): action_name="show_energy_window_action", text="Energy", slot=self.show_energy_window, + shortcut="Ctrl+E", ) self.show_energy_window_action.setIcon(qta.icon("mdi.sine-wave")) # Launch camera overview @@ -341,26 +354,38 @@ def setup_runengine_actions(self): self.check_queue_status_action = QtWidgets.QAction(self) # Navbar actions for controlling the run engine actions = [ - ("pause_runengine_action", "Pause", "fa5s.stopwatch"), - ("pause_runengine_now_action", "Pause Now", "fa5s.pause"), - ("resume_runengine_action", "Resume", "fa5s.play"), - ("stop_runengine_action", "Stop", "fa5s.stop"), - ("abort_runengine_action", "Abort", "fa5s.eject"), - ("halt_runengine_action", "Halt", "fa5s.ban"), - ("start_queue_action", "Start", "fa5s.play"), + # (action_name, text, icon, shortcut, tooltip) + ("pause_runengine_action", "Pause", "fa5s.stopwatch", None, + "Pause the current plan at the next checkpoint."), + ("pause_runengine_now_action", "Pause Now", "fa5s.pause", "Ctrl+C", + "Pause the run engine now."), + ("resume_runengine_action", "Resume", "fa5s.play", None, + "Resume the previously-paused run engine at the last checkpoint."), + ("stop_runengine_action", "Stop", "fa5s.stop", None, + "End the current plan, marking as success."), + ("abort_runengine_action", "Abort", "fa5s.eject", None, + "End the current plan, marking as failure."), + ("halt_runengine_action", "Halt", "fa5s.ban", None, + "End the current plan immediately, do not clean up."), + ("start_queue_action", "Start", "fa5s.play", None, + "Start the queue."), ] self.queue_action_group = QtWidgets.QActionGroup(self) - for name, text, icon_name in actions: + for name, text, icon_name, shortcut, tooltip in actions: action = QtWidgets.QAction(self.queue_action_group) icon = qta.icon(icon_name) action.setText(text) - action.setCheckable(True) + action.setCheckable(False) action.setIcon(icon) + action.setToolTip(tooltip) + if shortcut is not None: + action.setShortcut(QKeySequence(shortcut)) setattr(self, name, action) # Actions that control how the queue operates actions = [ # Attr, object name, text ("queue_autostart_action", "queue_autostart_action", "&Autoplay"), + ("queue_stop_action", "queue_stop_action", "Stop Queue"), ( "queue_open_environment_action", "queue_open_environment_action", @@ -375,6 +400,10 @@ def setup_runengine_actions(self): # Customize some specific actions self.queue_autostart_action.setCheckable(True) self.queue_autostart_action.setChecked(True) + self.queue_stop_action.setCheckable(True) + self.queue_stop_action.setToolTip( + "Tell the queueserver to stop after this current plan is done." + ) self.queue_open_environment_action.setCheckable(True) def _prepare_device_windows( @@ -464,7 +493,7 @@ def _prepare_device_windows( action.triggered.connect(slot) window_slots.append(slot) - def prepare_queue_client(self, api=None): + def prepare_queue_client(self, client=None, api=None): """Set up the QueueClient object that talks to the queue server. Parameters @@ -476,7 +505,8 @@ def prepare_queue_client(self, api=None): if api is None: api = queueserver_api() # Create the client object - client = QueueClient(api=api) + if client is None: + client = QueueClient(api=api) self.queue_open_environment_action.triggered.connect(client.open_environment) self._queue_client = client # Connect actions to slots for controlling the queueserver @@ -487,6 +517,11 @@ def prepare_queue_client(self, api=None): partial(client.request_pause, defer=False) ) self.start_queue_action.triggered.connect(client.start_queue) + self.resume_runengine_action.triggered.connect(client.resume_runengine) + self.stop_runengine_action.triggered.connect(client.stop_runengine) + self.halt_runengine_action.triggered.connect(client.halt_runengine) + self.abort_runengine_action.triggered.connect(client.abort_runengine) + self.queue_stop_action.triggered.connect(client.stop_queue) self.check_queue_status_action.triggered.connect( partial(client.check_queue_status, True) ) @@ -495,11 +530,14 @@ def prepare_queue_client(self, api=None): # Connect signals/slots for queueserver state changes client.status_changed.connect(self.queue_status_changed) client.length_changed.connect(self.queue_length_changed) + client.in_use_changed.connect(self.queue_in_use_changed) + client.autostart_changed.connect(self.queue_autostart_action.setChecked) client.environment_opened.connect(self.queue_environment_opened) self.queue_environment_opened.connect(self.set_open_environment_action_state) client.environment_state_changed.connect(self.queue_environment_state_changed) client.manager_state_changed.connect(self.queue_manager_state_changed) client.re_state_changed.connect(self.queue_re_state_changed) + client.queue_stop_changed.connect(self.queue_stop_action.setChecked) client.devices_changed.connect(self.update_devices_allowed) self.queue_autostart_action.toggled.connect( self.check_queue_status_action.trigger @@ -646,7 +684,7 @@ def show_device_window( def show_status_window(self, stylesheet_path=None): """Instantiate a new main window for this application.""" self.show_window( - FireflyMainWindow, ui_dir / "status.py", name="beamline_status" + PlanMainWindow, ui_dir / "status.py", name="beamline_status" ) @QtCore.Slot() diff --git a/src/firefly/main_window.py b/src/firefly/main_window.py index afe99fef..14c6a60e 100644 --- a/src/firefly/main_window.py +++ b/src/firefly/main_window.py @@ -93,6 +93,12 @@ def customize_ui(self): _label = QtWidgets.QLabel() _label.setText("Queue:") bar.addPermanentWidget(_label) + self.ui.queue_length_label = QtWidgets.QLabel() + self.ui.queue_length_label.setToolTip( + "The length of the queue, not including the running plan." + ) + self.ui.queue_length_label.setText("(??)") + bar.addPermanentWidget(self.ui.queue_length_label) self.ui.environment_label = QtWidgets.QLabel() self.ui.environment_label.setToolTip( "The current state of the queue server environment." @@ -108,11 +114,13 @@ def customize_ui(self): bar.addPermanentWidget(self.ui.re_label) # Connect signals to the status bar app.queue_environment_state_changed.connect(self.ui.environment_label.setText) + app.queue_length_changed.connect(self.update_queue_length) app.queue_re_state_changed.connect(self.ui.re_label.setText) # Log viewer window if hasattr(app, "show_logs_window_action"): self.ui.menuView.addAction(app.show_logs_window_action) # Setup menu + print(type(self.ui.menubar)) self.ui.menuSetup = QtWidgets.QMenu(self.ui.menubar) self.ui.menuSetup.setObjectName("menuSetup") self.ui.menuSetup.setTitle("Set&up") @@ -124,6 +132,7 @@ def customize_ui(self): self.ui.menubar.addAction(self.ui.queue_menu.menuAction()) for action in app.queue_action_group.actions(): self.ui.queue_menu.addAction(action) + self.ui.queue_menu.addAction(app.queue_stop_action) self.ui.queue_menu.addSeparator() # Queue settings for the queue client self.ui.queue_menu.addAction(app.launch_queuemonitor_action) @@ -218,6 +227,9 @@ def customize_ui(self): self.ui.menuView.addAction(app.show_status_window_action) self.ui.menuSetup.addAction(app.show_bss_window_action) self.ui.menuSetup.addAction(app.show_iocs_window_action) + # Make tooltips show up for menu actions + for menu in [self.ui.menuSetup, self.ui.detectors_menu, self.ui.queue_menu]: + menu.setToolTipsVisible(True) def show_status(self, message, timeout=0): """Show a message in the status bar.""" @@ -237,6 +249,9 @@ def update_window_title(self): title += " [Read Only Mode]" self.setWindowTitle(title) + def update_queue_length(self, new_length: int): + self.ui.queue_length_label.setText(f"({new_length})") + class PlanMainWindow(FireflyMainWindow): """A Qt window that has extra controls for a bluesky runengine.""" @@ -260,7 +275,7 @@ def setup_navbar(self): navbar.addAction(app.resume_runengine_action) navbar.addAction(app.stop_runengine_action) navbar.addAction(app.abort_runengine_action) - navbar.addAction(app.halt_runengine_action) + # navbar.addAction(app.halt_runengine_action) def customize_ui(self): super().customize_ui() @@ -269,14 +284,7 @@ def customize_ui(self): from .application import FireflyApplication app = FireflyApplication.instance() - app.queue_length_changed.connect(self.set_navbar_visibility) - - @QtCore.Slot(int) - def set_navbar_visibility(self, queue_length: int): - """Determine whether to make the navbar be visible.""" - log.debug(f"Setting navbar visibility. Queue length: {queue_length}") - navbar = self.ui.navbar - navbar.setVisible(queue_length > 0) + app.queue_in_use_changed.connect(self.ui.navbar.setVisible) # ----------------------------------------------------------------------------- diff --git a/src/firefly/queue_client.py b/src/firefly/queue_client.py index f3a41e96..a6690642 100644 --- a/src/firefly/queue_client.py +++ b/src/firefly/queue_client.py @@ -1,7 +1,7 @@ import logging import time import warnings -from typing import Optional +from typing import Optional, Mapping from bluesky_queueserver_api import comm_base from bluesky_queueserver_api.zmq.aio import REManagerAPI @@ -31,6 +31,9 @@ class QueueClient(QObject): # Signals responding to queue changes status_changed = Signal(dict) length_changed = Signal(int) + in_use_changed = Signal(bool) # If length > 0, or queue is running + autostart_changed = Signal(bool) + queue_stop_changed = Signal(bool) # If a queue stop has been requested environment_opened = Signal(bool) # Opened (True) or closed (False) environment_state_changed = Signal(str) # New state manager_state_changed = Signal(str) # New state @@ -100,29 +103,84 @@ async def request_pause(self, defer: bool = True): @asyncSlot(object) async def add_queue_item(self, item): log.info(f"Client adding item to queue: {item}") - result = await self.api.item_add(item=item) - if result["success"]: - log.info(f"Item added. New queue length: {result['qsize']}") - new_length = result["qsize"] - self.length_changed.emit(result["qsize"]) + try: + result = await self.api.item_add(item=item) + self.check_result(result) + except (RuntimeError, comm_base.RequestFailedError): + # Request failed, so force a UI update + await self.check_queue_status(force=True) else: - log.error(f"Did not add queue item to queue: {result}") - raise RuntimeError(result) - + await self.check_queue_status(force=False) + @asyncSlot(bool) async def toggle_autostart(self, enable: bool): - print(f"Toggling auto-start: {enable}") - await self.api.queue_autostart(enable) + log.debug(f"Toggling auto-start: {enable}") + try: + result = await self.api.queue_autostart(enable) + self.check_result(result, task="toggle auto-start") + except (RuntimeError, comm_base.RequestFailedError): + # Request failed, so force a UI update + await self.check_queue_status(force=True) + else: + await self.check_queue_status(force=False) + + @asyncSlot(bool) + async def stop_queue(self, stop: bool): + """Turn on/off whether the queue will stop after the current plan.""" + # Determine which call to usee + if stop: + api_call = self.api.queue_stop() + else: + api_call = self.api.queue_stop_cancel() + # Execute the call + try: + result = await api_call + self.check_result(result, task="toggle stop queue") + except (RuntimeError, comm_base.RequestFailedError): + # Request failed, so force a UI update + await self.check_queue_status(force=True) + else: + await self.check_queue_status(force=False) @asyncSlot() async def start_queue(self): result = await self.api.queue_start() + self.check_result(result, task="start queue") + + @asyncSlot() + async def resume_runengine(self): + result = await self.api.re_resume() + self.check_result(result, task="resume run engine") + + @asyncSlot() + async def stop_runengine(self): + result = await self.api.re_stop() + self.check_result(result, task="stop run engine") + + @asyncSlot() + async def abort_runengine(self): + result = await self.api.re_abort() + self.check_result(result, task="abort run engine") + + @asyncSlot() + async def halt_runengine(self): + result = await self.api.re_halt() + self.check_result(result, task="halt run engine") + + def check_result(self, result: Mapping, task: str = "control queue server"): + """Send the result of an API call to the correct logger. + + Expects *result* to have at least the "success" key. + + """ # Report results if result["success"] is True: - log.debug(f"Started queue server: {result}") + log.debug(f"{task}: {result}") else: - log.error(f"Failed to start queue server: {result}") - raise RuntimeError(result) + msg = f"Failed to {task}: {result}" + log.error(msg) + raise RuntimeError(msg) + @asyncSlot() async def check_queue_status(self, force=False, *args, **kwargs): @@ -169,6 +227,10 @@ async def _check_queue_status(self, force: bool = False): """ new_status = await self.api.status() + # Add a new key for whether the queue is busy (length > 0 or running) + has_queue = new_status['items_in_queue'] > 0 + is_running = new_status['manager_state'] in ["paused", "starting_queue", "executing_queue", "executing_task"] + new_status.setdefault("in_use", has_queue or is_running) # Check individual components of the status if they've changed signals_to_check = [ # (status key, signal to emit) @@ -176,6 +238,10 @@ async def _check_queue_status(self, force: bool = False): ("worker_environment_state", self.environment_state_changed), ("manager_state", self.manager_state_changed), ("re_state", self.re_state_changed), + ("items_in_queue", self.length_changed), + ("in_use", self.in_use_changed), + ("queue_stop_pending", self.queue_stop_changed), + ("queue_autostart_enabled", self.autostart_changed), ] if force: log.debug(f"Forcing queue server status update: {new_status}") diff --git a/src/firefly/tests/test_queue_client.py b/src/firefly/tests/test_queue_client.py index c16c62b4..531814fe 100644 --- a/src/firefly/tests/test_queue_client.py +++ b/src/firefly/tests/test_queue_client.py @@ -16,6 +16,7 @@ "running_item_uid": None, "manager_state": "idle", "queue_stop_pending": False, + "queue_autostart_enabled": False, "worker_environment_exists": False, "worker_environment_state": "closed", "worker_background_tasks": 0, @@ -218,9 +219,24 @@ def client(): api.queue_start.return_value = { "success": True, } + api.re_resume.return_value = { + "success": True, + } + api.re_stop.return_value = { + "success": True, + } + api.re_abort.return_value = { + "success": True, + } + api.re_halt.return_value = { + "success": True, + } api.devices_allowed.return_value = {"success": True, "devices_allowed": {}} api.environment_open.return_value = {"success": True} api.environment_close.return_value = {"success": True} + api.queue_autostart.return_value = {"success": True} + api.queue_stop.return_value = {"success": True} + api.queue_stop_cancel.return_value = {"success": True} # Create the client using the fake API autoplay_action = QAction() autoplay_action.setCheckable(True) @@ -230,6 +246,12 @@ def client(): yield client +@pytest.fixture() +def ffapp(ffapp, client): + ffapp.prepare_queue_client(api=client.api, client=client) + return ffapp + + def test_client_timer(client): assert isinstance(client.timer, QTimer) @@ -252,6 +274,22 @@ async def test_queue_re_control(client): await client.start_queue() # Check if the queue started api.queue_start.assert_called_once() + # Resume a paused queue + api.reset_mock() + await client.resume_runengine() + api.re_resume.assert_called_once() + # Stop a paused queue + api.reset_mock() + await client.stop_runengine() + api.re_stop.assert_called_once() + # Abort a paused queue + api.reset_mock() + await client.abort_runengine() + api.re_abort.assert_called_once() + # Halt a paused queue + api.reset_mock() + await client.halt_runengine() + api.re_halt.assert_called_once() @pytest.mark.asyncio @@ -269,7 +307,7 @@ async def test_run_plan(client, qtbot): @pytest.mark.asyncio -async def test_autoplay(client, qtbot): +async def test_toggle_autostart(client, qtbot): """Test how queuing a plan starts the runengine.""" api = client.api # Check that it doesn't start the queue if the autoplay action is off @@ -279,6 +317,50 @@ async def test_autoplay(client, qtbot): api.queue_autostart.assert_called_once_with(True) +def test_autostart_changed(client, ffapp): + """Does the action respond to changes in the queue autostart + status? + + """ + assert ffapp.queue_autostart_action.isChecked() + client.autostart_changed.emit(False) + assert not ffapp.queue_autostart_action.isChecked() + client.autostart_changed.emit(True) + assert ffapp.queue_autostart_action.isChecked() + + +# def test_start_queue(ffapp, client, qtbot): +# ffapp.start_queue_action.trigger() +# qtbot.wait(1000) +# client.api.queue_start.assert_called_once() + +@pytest.mark.asyncio +async def test_stop_queue(client, qtbot): + """Test how queuing a plan starts the runengine.""" + api = client.api + # Check that it doesn't start the queue if the autoplay action is off + assert not api.queue_autostart.called + # Check the queue stop was requested + await client.stop_queue(True) + api.queue_stop.assert_called_once() + # Check the queue stop can be cancelled + api.clear_mock() + await client.stop_queue(False) + api.queue_stop_cancel.assert_called_once() + + +def test_queue_stopped(client, ffapp): + """Does the action respond to changes in the queue stopped pending? + + """ + assert not ffapp.queue_stop_action.isChecked() + client.queue_stop_changed.emit(True) + assert ffapp.queue_stop_action.isChecked() + client.queue_stop_changed.emit(False) + assert not ffapp.queue_stop_action.isChecked() + + + @pytest.mark.asyncio async def test_check_queue_status(client, qtbot): # Check that the queue length is changed @@ -287,7 +369,9 @@ async def test_check_queue_status(client, qtbot): client.environment_opened, client.environment_state_changed, client.re_state_changed, + client.autostart_changed, client.manager_state_changed, + client.in_use_changed, ] with qtbot.waitSignals(signals): await client.check_queue_status() diff --git a/src/haven/instrument/ion_chamber.py b/src/haven/instrument/ion_chamber.py index 356286ad..65efd05b 100644 --- a/src/haven/instrument/ion_chamber.py +++ b/src/haven/instrument/ion_chamber.py @@ -620,7 +620,7 @@ async def make_ion_chamber_device( ch_num=ch_num, name=name, preamp_prefix=preamp_prefix, - labels={"ion_chambers"}, + labels={"ion_chambers", "detectors"}, ) try: await await_for_connection(ic) @@ -733,7 +733,7 @@ def load_ion_chambers(config=None): name=defn["name"], preamp_prefix=defn["preamp_prefix"], voltmeter_prefix=defn["voltmeter_prefix"], - labels={"ion_chambers", defn["section"]}, + labels={"ion_chambers", defn["section"], "detectors"}, counts_per_volt_second=defn["counts_per_volt_second"], ) ) diff --git a/src/haven/instrument/motor.py b/src/haven/instrument/motor.py index aec3761c..529c8519 100644 --- a/src/haven/instrument/motor.py +++ b/src/haven/instrument/motor.py @@ -72,7 +72,8 @@ def load_motors( prefix = config["prefix"] num_motors = config["num_motors"] log.info( - f"Preparing {num_motors} motors from IOC: " "{section_name} ({prefix})" + f"Preparing {num_motors} motors from IOC: " + f"{section_name} ({prefix})" ) for idx in range(num_motors): motor_prefix = f"{prefix}m{idx+1}" From 92d9a2a0deb4ac86c288f55ef0af181365d478c1 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Mon, 3 Jun 2024 13:13:48 -0500 Subject: [PATCH 02/10] Flake8, black, and isort. --- src/firefly/application.py | 67 ++++++++++++++++++-------- src/firefly/main_window.py | 2 +- src/firefly/queue_client.py | 14 ++++-- src/firefly/tests/test_queue_client.py | 6 +-- src/haven/instrument/motor.py | 3 +- 5 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/firefly/application.py b/src/firefly/application.py index f31e9f4b..17788cdf 100644 --- a/src/firefly/application.py +++ b/src/firefly/application.py @@ -15,8 +15,8 @@ from PyQt5.QtWidgets import QStyleFactory from qtpy import QtCore, QtWidgets from qtpy.QtCore import Signal -from qtpy.QtWidgets import QAction from qtpy.QtGui import QKeySequence +from qtpy.QtWidgets import QAction from haven import load_config from haven import load_instrument as load_haven_instrument @@ -130,7 +130,9 @@ def __del__(self): self._queue_thread.wait(msecs=5000) assert not self._queue_thread.isRunning() - def _setup_window_action(self, action_name: str, text: str, slot: QtCore.Slot, shortcut=None, icon=None): + def _setup_window_action( + self, action_name: str, text: str, slot: QtCore.Slot, shortcut=None, icon=None + ): action = QtWidgets.QAction(self) action.setObjectName(action_name) action.setText(text) @@ -355,20 +357,49 @@ def setup_runengine_actions(self): # Navbar actions for controlling the run engine actions = [ # (action_name, text, icon, shortcut, tooltip) - ("pause_runengine_action", "Pause", "fa5s.stopwatch", None, - "Pause the current plan at the next checkpoint."), - ("pause_runengine_now_action", "Pause Now", "fa5s.pause", "Ctrl+C", - "Pause the run engine now."), - ("resume_runengine_action", "Resume", "fa5s.play", None, - "Resume the previously-paused run engine at the last checkpoint."), - ("stop_runengine_action", "Stop", "fa5s.stop", None, - "End the current plan, marking as success."), - ("abort_runengine_action", "Abort", "fa5s.eject", None, - "End the current plan, marking as failure."), - ("halt_runengine_action", "Halt", "fa5s.ban", None, - "End the current plan immediately, do not clean up."), - ("start_queue_action", "Start", "fa5s.play", None, - "Start the queue."), + ( + "pause_runengine_action", + "Pause", + "fa5s.stopwatch", + None, + "Pause the current plan at the next checkpoint.", + ), + ( + "pause_runengine_now_action", + "Pause Now", + "fa5s.pause", + "Ctrl+C", + "Pause the run engine now.", + ), + ( + "resume_runengine_action", + "Resume", + "fa5s.play", + None, + "Resume the previously-paused run engine at the last checkpoint.", + ), + ( + "stop_runengine_action", + "Stop", + "fa5s.stop", + None, + "End the current plan, marking as success.", + ), + ( + "abort_runengine_action", + "Abort", + "fa5s.eject", + None, + "End the current plan, marking as failure.", + ), + ( + "halt_runengine_action", + "Halt", + "fa5s.ban", + None, + "End the current plan immediately, do not clean up.", + ), + ("start_queue_action", "Start", "fa5s.play", None, "Start the queue."), ] self.queue_action_group = QtWidgets.QActionGroup(self) for name, text, icon_name, shortcut, tooltip in actions: @@ -683,9 +714,7 @@ def show_device_window( def show_status_window(self, stylesheet_path=None): """Instantiate a new main window for this application.""" - self.show_window( - PlanMainWindow, ui_dir / "status.py", name="beamline_status" - ) + self.show_window(PlanMainWindow, ui_dir / "status.py", name="beamline_status") @QtCore.Slot() def show_run_browser_window(self): diff --git a/src/firefly/main_window.py b/src/firefly/main_window.py index 14c6a60e..214fc1ab 100644 --- a/src/firefly/main_window.py +++ b/src/firefly/main_window.py @@ -5,7 +5,7 @@ import qtawesome as qta from pydm import data_plugins from pydm.main_window import PyDMMainWindow -from qtpy import QtCore, QtGui, QtWidgets +from qtpy import QtGui, QtWidgets from haven import load_config diff --git a/src/firefly/queue_client.py b/src/firefly/queue_client.py index a6690642..e8b01c4d 100644 --- a/src/firefly/queue_client.py +++ b/src/firefly/queue_client.py @@ -1,7 +1,7 @@ import logging import time import warnings -from typing import Optional, Mapping +from typing import Mapping, Optional from bluesky_queueserver_api import comm_base from bluesky_queueserver_api.zmq.aio import REManagerAPI @@ -111,7 +111,7 @@ async def add_queue_item(self, item): await self.check_queue_status(force=True) else: await self.check_queue_status(force=False) - + @asyncSlot(bool) async def toggle_autostart(self, enable: bool): log.debug(f"Toggling auto-start: {enable}") @@ -181,7 +181,6 @@ def check_result(self, result: Mapping, task: str = "control queue server"): log.error(msg) raise RuntimeError(msg) - @asyncSlot() async def check_queue_status(self, force=False, *args, **kwargs): """Get an update queue status from queue server and notify slots. @@ -228,8 +227,13 @@ async def _check_queue_status(self, force: bool = False): """ new_status = await self.api.status() # Add a new key for whether the queue is busy (length > 0 or running) - has_queue = new_status['items_in_queue'] > 0 - is_running = new_status['manager_state'] in ["paused", "starting_queue", "executing_queue", "executing_task"] + has_queue = new_status["items_in_queue"] > 0 + is_running = new_status["manager_state"] in [ + "paused", + "starting_queue", + "executing_queue", + "executing_task", + ] new_status.setdefault("in_use", has_queue or is_running) # Check individual components of the status if they've changed signals_to_check = [ diff --git a/src/firefly/tests/test_queue_client.py b/src/firefly/tests/test_queue_client.py index 531814fe..e93d0ce4 100644 --- a/src/firefly/tests/test_queue_client.py +++ b/src/firefly/tests/test_queue_client.py @@ -334,6 +334,7 @@ def test_autostart_changed(client, ffapp): # qtbot.wait(1000) # client.api.queue_start.assert_called_once() + @pytest.mark.asyncio async def test_stop_queue(client, qtbot): """Test how queuing a plan starts the runengine.""" @@ -350,9 +351,7 @@ async def test_stop_queue(client, qtbot): def test_queue_stopped(client, ffapp): - """Does the action respond to changes in the queue stopped pending? - - """ + """Does the action respond to changes in the queue stopped pending?""" assert not ffapp.queue_stop_action.isChecked() client.queue_stop_changed.emit(True) assert ffapp.queue_stop_action.isChecked() @@ -360,7 +359,6 @@ def test_queue_stopped(client, ffapp): assert not ffapp.queue_stop_action.isChecked() - @pytest.mark.asyncio async def test_check_queue_status(client, qtbot): # Check that the queue length is changed diff --git a/src/haven/instrument/motor.py b/src/haven/instrument/motor.py index 529c8519..984dbb15 100644 --- a/src/haven/instrument/motor.py +++ b/src/haven/instrument/motor.py @@ -72,8 +72,7 @@ def load_motors( prefix = config["prefix"] num_motors = config["num_motors"] log.info( - f"Preparing {num_motors} motors from IOC: " - f"{section_name} ({prefix})" + f"Preparing {num_motors} motors from IOC: " f"{section_name} ({prefix})" ) for idx in range(num_motors): motor_prefix = f"{prefix}m{idx+1}" From 0eaaff87528fd74c8c2f594429a68d8b377f8914 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Tue, 4 Jun 2024 11:48:23 -0500 Subject: [PATCH 03/10] Fixed broken tests. --- src/firefly/tests/test_main_window.py | 8 ++++---- src/firefly/tests/test_queue_client.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/firefly/tests/test_main_window.py b/src/firefly/tests/test_main_window.py index 654b1868..50b77445 100644 --- a/src/firefly/tests/test_main_window.py +++ b/src/firefly/tests/test_main_window.py @@ -21,12 +21,12 @@ def test_navbar_autohide(ffapp, qtbot): window.show() navbar = window.ui.navbar # Pretend the queue has some things in it - with qtbot.waitSignal(ffapp.queue_length_changed): - ffapp.queue_length_changed.emit(3) + with qtbot.waitSignal(ffapp.queue_in_use_changed): + ffapp.queue_in_use_changed.emit(True) assert navbar.isVisible() # Make the queue be empty - with qtbot.waitSignal(ffapp.queue_length_changed): - ffapp.queue_length_changed.emit(0) + with qtbot.waitSignal(ffapp.queue_in_use_changed): + ffapp.queue_in_use_changed.emit(False) assert not navbar.isVisible() diff --git a/src/firefly/tests/test_queue_client.py b/src/firefly/tests/test_queue_client.py index e93d0ce4..ddd20179 100644 --- a/src/firefly/tests/test_queue_client.py +++ b/src/firefly/tests/test_queue_client.py @@ -297,6 +297,9 @@ async def test_run_plan(client, qtbot): """Test if a plan can be queued in the queueserver.""" api = client.api api.item_add.return_value = {"success": True, "qsize": 2} + new_status = qs_status.copy() + new_status["items_in_queue"] = 2 + api.status.return_value = new_status # Send a plan with qtbot.waitSignal( client.length_changed, timeout=1000, check_params_cb=lambda l: l == 2 @@ -317,15 +320,18 @@ async def test_toggle_autostart(client, qtbot): api.queue_autostart.assert_called_once_with(True) -def test_autostart_changed(client, ffapp): +def test_autostart_changed(client, ffapp, qtbot): """Does the action respond to changes in the queue autostart status? """ + ffapp.queue_autostart_action.setChecked(True) assert ffapp.queue_autostart_action.isChecked() - client.autostart_changed.emit(False) + with qtbot.waitSignal(client.autostart_changed, timeout=3): + client.autostart_changed.emit(False) assert not ffapp.queue_autostart_action.isChecked() - client.autostart_changed.emit(True) + with qtbot.waitSignal(client.autostart_changed, timeout=3): + client.autostart_changed.emit(True) assert ffapp.queue_autostart_action.isChecked() From be7a3770acf8e4a8a4c6a7468db19e15845eebe3 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Fri, 7 Jun 2024 15:15:52 -0500 Subject: [PATCH 04/10] Added some icons to the plan actions and cleaned up application launching exceptions. --- src/firefly/application.py | 20 ++++++++++++++------ src/firefly/launcher.py | 2 +- src/firefly/main_window.py | 1 - src/firefly/queue_client.py | 6 +++++- src/haven/exceptions.py | 26 ++++++-------------------- 5 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/firefly/application.py b/src/firefly/application.py index 17788cdf..cc38d23f 100644 --- a/src/firefly/application.py +++ b/src/firefly/application.py @@ -265,13 +265,13 @@ def setup_window_actions(self): ) # Actions for executing plans plans = [ - # (plan_name, text, display file, shortcut) - ("count", "&Count", "count.py", "Ctrl+Shift+C"), - ("line_scan", "&Line scan", "line_scan.py", "Ctrl+Shift+L"), - ("xafs_scan", "&XAFS Scan", "xafs_scan.py", "Ctrl+Shift+X"), + # (plan_name, text, display file, shortcut, icon) + ("count", "&Count", "count.py", "Ctrl+Shift+C", "mdi.counter"), + ("line_scan", "&Line scan", "line_scan.py", "Ctrl+Shift+L", None), + ("xafs_scan", "&XAFS Scan", "xafs_scan.py", "Ctrl+Shift+X", None), ] self.plan_actions = [] - for plan_name, text, display_file, shortcut in plans: + for plan_name, text, display_file, shortcut, icon in plans: slot = partial( self.show_plan_window, name=plan_name, display_file=display_file ) @@ -283,6 +283,8 @@ def setup_window_actions(self): action.triggered.connect(slot) if shortcut is not None: action.setShortcut(QKeySequence(shortcut)) + if icon is not None: + action.setIcon(qta.icon(icon)) self.plan_actions.append(action) # Action for showing the run browser window self._setup_window_action( @@ -533,8 +535,14 @@ def prepare_queue_client(self, client=None, api=None): queueserver API. Used for testing. """ + # Load the API for controlling the queueserver. if api is None: - api = queueserver_api() + try: + api = queueserver_api() + except InvalidConfiguration: + log.error("Could not load queueserver API " + "configuration from iconfig.toml file.") + return # Create the client object if client is None: client = QueueClient(api=api) diff --git a/src/firefly/launcher.py b/src/firefly/launcher.py index 4dbc3de8..05a0ae5d 100644 --- a/src/firefly/launcher.py +++ b/src/firefly/launcher.py @@ -182,7 +182,7 @@ def main(default_fullscreen=False, default_display="status"): asyncio.set_event_loop(event_loop) # Define devices on the beamline (slow!) - app.setup_instrument() + app.setup_instrument(load_instrument=not pydm_args.no_instrument) # Get rid of the splash screen now that we're ready to go splash.close() diff --git a/src/firefly/main_window.py b/src/firefly/main_window.py index 214fc1ab..eb525722 100644 --- a/src/firefly/main_window.py +++ b/src/firefly/main_window.py @@ -120,7 +120,6 @@ def customize_ui(self): if hasattr(app, "show_logs_window_action"): self.ui.menuView.addAction(app.show_logs_window_action) # Setup menu - print(type(self.ui.menubar)) self.ui.menuSetup = QtWidgets.QMenu(self.ui.menubar) self.ui.menuSetup.setObjectName("menuSetup") self.ui.menuSetup.setTitle("Set&up") diff --git a/src/firefly/queue_client.py b/src/firefly/queue_client.py index e8b01c4d..4ba9ee32 100644 --- a/src/firefly/queue_client.py +++ b/src/firefly/queue_client.py @@ -9,12 +9,16 @@ from qtpy.QtCore import QObject, QTimer, Signal from haven import load_config +from haven.exceptions import UnknownDeviceConfiguration, InvalidConfiguration log = logging.getLogger() def queueserver_api(): - config = load_config()["queueserver"] + try: + config = load_config()["queueserver"] + except KeyError: + raise InvalidConfiguration("Could not load queueserver info from iconfig.toml file.") ctrl_addr = f"tcp://{config['control_host']}:{config['control_port']}" info_addr = f"tcp://{config['info_host']}:{config['info_port']}" api = REManagerAPI(zmq_control_addr=ctrl_addr, zmq_info_addr=info_addr) diff --git a/src/haven/exceptions.py b/src/haven/exceptions.py index f11fd642..b1a7baac 100644 --- a/src/haven/exceptions.py +++ b/src/haven/exceptions.py @@ -16,25 +16,7 @@ class GainOverflow(RuntimeError): ... - -# class ComponentNotFound(IndexError): -# """Registry looked for a component, but it wasn't registered.""" - -# ... - - -# class MultipleComponentsFound(IndexError): -# """Registry looked for a single component, but found more than one.""" - -# ... - - -# class InvalidComponentLabel(TypeError): -# """Registry looked for a component, but the label provided is not vlaid.""" - -# ... - - + class FileNotWritable(IOError): """Output file is available but does not have write intent.""" @@ -71,7 +53,11 @@ class IOCTimeout(RuntimeError): ... -class UnknownDeviceConfiguration(ValueError): +class InvalidConfiguration(ValueError): + """The configuration files for Haven are missing keys.""" + ... + +class UnknownDeviceConfiguration(InvalidConfiguration): """The configuration for a device does not match the known options.""" ... From 90ac005e1554081c28c33af6eb5e88beb49b1bb6 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Fri, 7 Jun 2024 15:26:13 -0500 Subject: [PATCH 05/10] Flake8, black, and isort. --- src/firefly/application.py | 8 +++++--- src/firefly/queue_client.py | 6 ++++-- src/haven/exceptions.py | 4 +++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/firefly/application.py b/src/firefly/application.py index cc38d23f..390528c3 100644 --- a/src/firefly/application.py +++ b/src/firefly/application.py @@ -21,7 +21,7 @@ from haven import load_config from haven import load_instrument as load_haven_instrument from haven import registry -from haven.exceptions import ComponentNotFound +from haven.exceptions import ComponentNotFound, InvalidConfiguration from haven.instrument.device import titelize from .main_window import FireflyMainWindow, PlanMainWindow @@ -540,8 +540,10 @@ def prepare_queue_client(self, client=None, api=None): try: api = queueserver_api() except InvalidConfiguration: - log.error("Could not load queueserver API " - "configuration from iconfig.toml file.") + log.error( + "Could not load queueserver API " + "configuration from iconfig.toml file." + ) return # Create the client object if client is None: diff --git a/src/firefly/queue_client.py b/src/firefly/queue_client.py index 4ba9ee32..0213b03d 100644 --- a/src/firefly/queue_client.py +++ b/src/firefly/queue_client.py @@ -9,7 +9,7 @@ from qtpy.QtCore import QObject, QTimer, Signal from haven import load_config -from haven.exceptions import UnknownDeviceConfiguration, InvalidConfiguration +from haven.exceptions import InvalidConfiguration log = logging.getLogger() @@ -18,7 +18,9 @@ def queueserver_api(): try: config = load_config()["queueserver"] except KeyError: - raise InvalidConfiguration("Could not load queueserver info from iconfig.toml file.") + raise InvalidConfiguration( + "Could not load queueserver info from iconfig.toml file." + ) ctrl_addr = f"tcp://{config['control_host']}:{config['control_port']}" info_addr = f"tcp://{config['info_host']}:{config['info_port']}" api = REManagerAPI(zmq_control_addr=ctrl_addr, zmq_info_addr=info_addr) diff --git a/src/haven/exceptions.py b/src/haven/exceptions.py index b1a7baac..0e05b443 100644 --- a/src/haven/exceptions.py +++ b/src/haven/exceptions.py @@ -16,7 +16,7 @@ class GainOverflow(RuntimeError): ... - + class FileNotWritable(IOError): """Output file is available but does not have write intent.""" @@ -55,8 +55,10 @@ class IOCTimeout(RuntimeError): class InvalidConfiguration(ValueError): """The configuration files for Haven are missing keys.""" + ... + class UnknownDeviceConfiguration(InvalidConfiguration): """The configuration for a device does not match the known options.""" From 2bcc2a7550b0021380e391a6ec6df7ad9d52760f Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 12 Jun 2024 22:52:16 -0500 Subject: [PATCH 06/10] More tooltips and shortcuts. --- src/firefly/application.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/firefly/application.py b/src/firefly/application.py index 390528c3..f8de821a 100644 --- a/src/firefly/application.py +++ b/src/firefly/application.py @@ -297,7 +297,7 @@ def setup_window_actions(self): action_name="launch_queuemonitor_action", text="Queue Monitor", slot=self.launch_queuemonitor, - shortcut="Ctrl+q", + shortcut="Ctrl+Q", ) # Action for showing the beamline scheduling window self._setup_window_action( @@ -363,7 +363,7 @@ def setup_runengine_actions(self): "pause_runengine_action", "Pause", "fa5s.stopwatch", - None, + "Ctrl+D", "Pause the current plan at the next checkpoint.", ), ( @@ -416,23 +416,34 @@ def setup_runengine_actions(self): setattr(self, name, action) # Actions that control how the queue operates actions = [ - # Attr, object name, text - ("queue_autostart_action", "queue_autostart_action", "&Autoplay"), - ("queue_stop_action", "queue_stop_action", "Stop Queue"), + # Attr, object name, text, tooltip + ( + "queue_autostart_action", + "queue_autostart_action", + "&Autoplay", + "If enabled, the queue will start when items are added.", + ), + ( + "queue_stop_action", + "queue_stop_action", + "Stop Queue", + "Instruct the queue to stop after the current item is done.", + ), ( "queue_open_environment_action", "queue_open_environment_action", "&Open Environment", + "If open (checked), the queue server is able to run plans.", ), ] - for attr, obj_name, text in actions: + for attr, obj_name, text, tooltip in actions: action = QAction() action.setObjectName(obj_name) action.setText(text) + action.setToolTip(tooltip) setattr(self, attr, action) # Customize some specific actions self.queue_autostart_action.setCheckable(True) - self.queue_autostart_action.setChecked(True) self.queue_stop_action.setCheckable(True) self.queue_stop_action.setToolTip( "Tell the queueserver to stop after this current plan is done." From 42759b509467c54b1d815a0be7dbab14f33f50e5 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Thu, 13 Jun 2024 09:55:13 -0500 Subject: [PATCH 07/10] Removed old FireflyApplication attribute xafs_scan_window. --- src/firefly/application.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/firefly/application.py b/src/firefly/application.py index f8de821a..780a39f5 100644 --- a/src/firefly/application.py +++ b/src/firefly/application.py @@ -44,7 +44,6 @@ class FireflyApplication(PyDMApplication): default_display = None - xafs_scan_window = None # For keeping track of ophyd devices used by the Firefly registry: Registry = None From fbe52ecbf651c83e4fadca3d5a51040e2248f584 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Thu, 13 Jun 2024 10:22:19 -0500 Subject: [PATCH 08/10] Firefly queue button reads the queue status directly to determine whether autostart is enabled. --- src/firefly/queue_button.py | 14 +++++++++----- src/firefly/tests/test_queue_button.py | 23 ++++++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/firefly/queue_button.py b/src/firefly/queue_button.py index 2a5f23ef..c440e8ad 100644 --- a/src/firefly/queue_button.py +++ b/src/firefly/queue_button.py @@ -1,11 +1,16 @@ """A QPushButton that responds to the state of the queue server.""" - +from strenum import StrEnum import qtawesome as qta from qtpy import QtGui, QtWidgets from firefly import FireflyApplication +class Colors(StrEnum): + ADD_TO_QUEUE = "rgb(0, 123, 255)" + RUN_QUEUE = "rgb(25, 135, 84)" + + class QueueButton(QtWidgets.QPushButton): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -22,11 +27,10 @@ def handle_queue_status_change(self, status: dict): # Should be disabled because the queue is closed self.setDisabled(True) # Coloration for the whether the item would get run immediately - app = FireflyApplication.instance() - if status["re_state"] == "idle" and app.queue_autostart_action.isChecked(): + if status["re_state"] == "idle" and status["queue_autostart_enabled"]: # Will play immediately self.setStyleSheet( - "background-color: rgb(25, 135, 84);\nborder-color: rgb(25, 135, 84);" + f"background-color: {Colors.RUN_QUEUE};\nborder-color: {Colors.RUN_QUEUE};" ) self.setIcon(qta.icon("fa5s.play")) self.setText("Run") @@ -34,7 +38,7 @@ def handle_queue_status_change(self, status: dict): elif status["worker_environment_exists"]: # Will be added to the queue self.setStyleSheet( - "background-color: rgb(0, 123, 255);\nborder-color: rgb(0, 123, 255);" + f"background-color: {Colors.ADD_TO_QUEUE};\nborder-color: {Colors.ADD_TO_QUEUE};" ) self.setIcon(qta.icon("fa5s.list")) self.setText("Add to Queue") diff --git a/src/firefly/tests/test_queue_button.py b/src/firefly/tests/test_queue_button.py index 725d2c97..2fb871a1 100644 --- a/src/firefly/tests/test_queue_button.py +++ b/src/firefly/tests/test_queue_button.py @@ -1,31 +1,44 @@ -from firefly.queue_button import QueueButton +from firefly.queue_button import QueueButton, Colors def test_queue_button_style(ffapp): - """Does the queue button change color/icon based.""" + """Does the queue button change color/icon based on the queue state.""" btn = QueueButton() # Initial style should be disabled and plain assert not btn.isEnabled() assert btn.styleSheet() == "" - # State when queue server is open and idle + # State when queue server is open and idle (no autostart) + queue_state = { + "worker_environment_exists": True, + "items_in_queue": 0, + "re_state": "idle", + "queue_autostart_enabled": False, + } + ffapp.queue_status_changed.emit(queue_state) + assert btn.isEnabled() + assert Colors.ADD_TO_QUEUE in btn.styleSheet() + assert btn.text() == "Add to Queue" + # State when queue server is open and idle (w/ autostart) queue_state = { "worker_environment_exists": True, "items_in_queue": 0, "re_state": "idle", + "queue_autostart_enabled": True, } ffapp.queue_status_changed.emit(queue_state) assert btn.isEnabled() - assert "rgb(25, 135, 84)" in btn.styleSheet() + assert Colors.RUN_QUEUE in btn.styleSheet() assert btn.text() == "Run" # State when queue server is open and idle queue_state = { "worker_environment_exists": True, "items_in_queue": 0, "re_state": "running", + "queue_autostart_enabled": True, } ffapp.queue_status_changed.emit(queue_state) assert btn.isEnabled() - assert "rgb(0, 123, 255)" in btn.styleSheet() + assert Colors.ADD_TO_QUEUE in btn.styleSheet() assert btn.text() == "Add to Queue" From 63179ed835078ee87722e130538d298e6121fd4e Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Thu, 13 Jun 2024 10:27:30 -0500 Subject: [PATCH 09/10] Plan windows no longer enable the run button automatically. The idea is that the QueueButton class will enable/disable these buttons based on the state of the queueserver. By default, they will remain disabled until the queueserver is ready. --- src/firefly/plans/line_scan.py | 2 +- src/firefly/plans/move_motor_window.py | 1 - src/firefly/plans/xafs_scan.py | 1 - src/firefly/tests/test_xafs_scan.py | 2 ++ 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/firefly/plans/line_scan.py b/src/firefly/plans/line_scan.py index e837071a..b7ec2c39 100644 --- a/src/firefly/plans/line_scan.py +++ b/src/firefly/plans/line_scan.py @@ -48,7 +48,7 @@ def customize_ui(self): self.ui.num_motor_spin_box.lineEdit().setReadOnly(True) self.ui.num_motor_spin_box.valueChanged.connect(self.update_regions) - self.ui.run_button.setEnabled(True) # for testing + # Connect signals for executing the plan self.ui.run_button.clicked.connect(self.queue_plan) # when selections of detectors changed update_total_time diff --git a/src/firefly/plans/move_motor_window.py b/src/firefly/plans/move_motor_window.py index 866cb75b..1ddbfbfd 100644 --- a/src/firefly/plans/move_motor_window.py +++ b/src/firefly/plans/move_motor_window.py @@ -41,7 +41,6 @@ def customize_ui(self): self.ui.num_motor_spin_box.lineEdit().setReadOnly(True) self.ui.num_motor_spin_box.valueChanged.connect(self.update_regions) - self.ui.run_button.setEnabled(True) # for testing self.ui.run_button.clicked.connect(self.queue_plan) def time_converter(self, total_seconds): diff --git a/src/firefly/plans/xafs_scan.py b/src/firefly/plans/xafs_scan.py index 4adc6aa0..452f28a1 100644 --- a/src/firefly/plans/xafs_scan.py +++ b/src/firefly/plans/xafs_scan.py @@ -257,7 +257,6 @@ def customize_ui(self): self.ui.pushButton.clicked.connect(self.reset_default_regions) # Button to actually execute the plan - self.ui.run_button.setEnabled(True) self.ui.run_button.clicked.connect(self.queue_plan) # connect checkboxes with all regions' check box diff --git a/src/firefly/tests/test_xafs_scan.py b/src/firefly/tests/test_xafs_scan.py index 07bd852d..56d35214 100644 --- a/src/firefly/tests/test_xafs_scan.py +++ b/src/firefly/tests/test_xafs_scan.py @@ -155,6 +155,7 @@ def check_item(item): return True # Click the run button and see if the plan is queued + display.ui.run_button.setEnabled(True) with qtbot.waitSignal( ffapp.queue_item_added, timeout=1000, check_params_cb=check_item ): @@ -262,6 +263,7 @@ def check_item(item): return True # Click the run button and see if the plan is queued + display.ui.run_button.setEnabled(True) with qtbot.waitSignal( ffapp.queue_item_added, timeout=1000, check_params_cb=check_item ): From d80d209684eff0c019c4f47ea963cf88c34d847a Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Thu, 13 Jun 2024 10:48:00 -0500 Subject: [PATCH 10/10] Black and isort --- src/firefly/queue_button.py | 3 ++- src/firefly/tests/test_queue_button.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/firefly/queue_button.py b/src/firefly/queue_button.py index c440e8ad..75beb914 100644 --- a/src/firefly/queue_button.py +++ b/src/firefly/queue_button.py @@ -1,7 +1,8 @@ """A QPushButton that responds to the state of the queue server.""" -from strenum import StrEnum + import qtawesome as qta from qtpy import QtGui, QtWidgets +from strenum import StrEnum from firefly import FireflyApplication diff --git a/src/firefly/tests/test_queue_button.py b/src/firefly/tests/test_queue_button.py index 2fb871a1..6eac47cd 100644 --- a/src/firefly/tests/test_queue_button.py +++ b/src/firefly/tests/test_queue_button.py @@ -1,4 +1,4 @@ -from firefly.queue_button import QueueButton, Colors +from firefly.queue_button import Colors, QueueButton def test_queue_button_style(ffapp):