Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/Testbase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
python-version: ${{ inputs.python }}
- name: Install package
run: |
sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 libxcb-cursor0
python -m pip install --upgrade pip
export QT_DEBUG_PLUGINS=1
pip install flake8 pytest pytest-cov pytest-qt pytest-xdist pytest-xvfb setuptools wheel numpy h5py ${{ inputs.qt5 }} toml
Expand All @@ -41,5 +41,5 @@ jobs:
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=src/pymodaq/resources/QtDesigner_Ressources,docs
- name: Test with pytest
run: |
pytest -n auto
pytest

6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ PID Models
Extensions
==========

* **Move and Grab** (*extensions/move_and_grab.py*): the smallest useful Dashboard extension, to start from when
writing yours. It moves an actuator of the Dashboard's experiment to a target, grabs a detector, shows the data and can
save them. It shows how to use the ``modules_manager``, how to wait for the instruments with callbacks instead of
blocking the GUI, units on the positions and a clean shutdown. It works with the mock instruments of PyMoDAQ. See
"How a Dashboard extension works" in the PyMoDAQ documentation.


Installation instructions
=========================
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[features] # defines the plugin features contained into this plugin
instruments = true # true if plugin contains instrument classes (else false, notice the lowercase for toml files)
extensions = false # true if plugins contains dashboard extensions
extensions = true # true if plugins contains dashboard extensions
models = true # true if plugins contains pid models
h5exporters = false # true if plugin contains custom h5 file exporters
scanners = false # true if plugin contains custom scan layout (daq_scan extensions)
Expand Down
151 changes: 151 additions & 0 deletions src/pymodaq_plugins_mockexamples/extensions/move_and_grab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
"""Reference Dashboard extension: move an actuator, grab a detector, show the data and save them.

It is the smallest useful ``CustomExt`` and shows what an extension driving instruments has to get right:

* it uses the actuators and detectors of the Dashboard through ``self.modules_manager`` and never creates control
modules, nor its own experiment or state managers
* it never blocks the GUI thread: the move and the grab are started with a callback, the next step is done in the callback
when the control module signals that it is finished (no polling, no sleep)
* the position is a ``DataActuator`` with units (the ones of the actuator), the data come back as a ``DataToExport``
* the extension can be closed cleanly: callbacks are forgotten, the file is closed, and quitting is refused while busy

Read the page "How a Dashboard extension works" of the PyMoDAQ documentation for the structure.
"""
from qtpy import QtWidgets

from pymodaq_utils.logger import set_logger, get_module_name

from pymodaq_data import DataToExport
from pymodaq_data.h5modules.data_saving import DataToExportSaver

from pymodaq_gui import utils as gutils
from pymodaq_gui.plotting.data_viewers import ViewerDispatcher

from pymodaq.control_modules.enums import MoveType
from pymodaq.utils.custom_ext import CustomExt
from pymodaq.utils.data import DataActuator
from pymodaq.utils.managers.modules import ModuleType

logger = set_logger(get_module_name(__file__))

EXTENSION_NAME = 'Move and Grab' # displayed in the Extensions menu of the Dashboard
CLASS_NAME = 'MoveAndGrab' # the name of the class below


class MoveAndGrab(CustomExt):

params = [
{'title': 'Actuator:', 'name': 'actuator', 'type': 'list', 'limits': []},
{'title': 'Target:', 'name': 'target', 'type': 'float', 'value': 1.}, # in the units of the actuator
{'title': 'Detector:', 'name': 'detector', 'type': 'list', 'limits': []},
{'title': 'Save the data:', 'name': 'save', 'type': 'bool', 'value': False},
{'title': 'Status:', 'name': 'status', 'type': 'str', 'value': 'Idle', 'readonly': True},
]

def __init__(self, parent: gutils.DockArea, dashboard):
super().__init__(parent, dashboard) # also creates self.modules_manager for this extension
self._busy = False
self._positions: DataToExport = None
self.setup_ui()

def setup_docks_and_widgets(self):
self.docks['settings'] = gutils.Dock('Settings')
self.dockarea.addDock(self.docks['settings'])
self.docks['settings'].addWidget(self.settings_tree)

self.viewer = ViewerDispatcher(self.dockarea, title='Data') # shows each data of a DataToExport, in docks

def setup_menus_and_toolbars(self, menubar: QtWidgets.QMenuBar = None):
self.create_dashboard_toolbar(add_break=False) # show/hide the Dashboard, experiment and state

def setup_actions(self):
self.add_action('move_and_grab', 'Move and grab', 'camera',
tip='Move the selected actuator to the target, then grab the selected detector')

def connect_things(self):
self.connect_action('move_and_grab', self.move_and_grab)
self.modules_manager.timeout_signal.connect(self._on_timeout)

def do_things_after_experiment_set(self, experiment_name: str, show_dashboard: bool = None):
"""The instruments just changed: refresh what depends on them (the base class updates the modules_manager)"""
super().do_things_after_experiment_set(experiment_name, show_dashboard)
self.settings.child('actuator').setLimits(self.modules_manager.actuators_name)
self.settings.child('detector').setLimits(self.modules_manager.detectors_name)

def move_and_grab(self):
if self._busy:
return
actuator = self.modules_manager.get_mod_from_name(self.settings['actuator'], ModuleType.Actuator)
detector = self.modules_manager.get_mod_from_name(self.settings['detector'], ModuleType.Detector)
if actuator is None or detector is None:
self.update_status('Select an actuator and a detector (set an experiment in the Dashboard first)')
return
self._set_busy(True, f'Moving {actuator.title}')
self.modules_manager.selected_actuators_name = [actuator.title]
self.modules_manager.selected_detectors_name = [detector.title]
# the target has units: those of the actuator, so that no conversion is needed
target = DataToExport('target', data=[DataActuator(actuator.title, data=self.settings['target'],
units=actuator.units)])
self.modules_manager.move_actuators_with_callback(target, mode=MoveType.ABS, callback=self._on_move_done)

def _on_move_done(self, positions: DataToExport):
"""Slot called, in the GUI thread, once the actuator reached its target"""
self.modules_manager.forget_callback(self._on_move_done, ModuleType.Actuator)
self._positions = positions
self.set_status('Grabbing')
self.modules_manager.grab_data_with_callback(callback=self._on_data)

def _on_data(self, data: DataToExport):
"""Slot called, in the GUI thread, once the detector sent its data"""
self.modules_manager.forget_callback(self._on_data, ModuleType.Detector)
self.viewer.show_data(data)
if self.settings['save']:
self._save(data)
self._set_busy(False, 'Done')

def _save(self, data: DataToExport):
h5saver = self.h5_manager.h5saver # opens the file, or creates a new one, if needed
node = h5saver.get_set_group('/RawData', 'MoveAndGrab')
DataToExportSaver(h5saver).add_data(node, DataToExport('moveandgrab', data=[*self._positions, *data]))
h5saver.flush()

def _on_timeout(self, missing_modules: list):
"""A module did not answer: do not stay blocked"""
self.modules_manager.forget_callback(self._on_move_done, ModuleType.Control)
self.modules_manager.forget_callback(self._on_data, ModuleType.Control)
self._set_busy(False, f'Timeout: {", ".join(missing_modules)}')

def _set_busy(self, busy: bool, status: str):
self._busy = busy
self.set_action_enabled('move_and_grab', not busy)
self.set_status(status)

def set_status(self, status: str):
self.settings.child('status').setValue(status)

def _quit_fun(self) -> bool:
"""Return True to let the extension quit; False to refuse, here while a move or a grab is going on"""
if self._busy:
self.update_status('Wait for the end of the move and grab before quitting')
return False
self.modules_manager.timeout_signal.disconnect(self._on_timeout)
self.h5_manager.close_file()
return True


def main():
"""Run the extension on its own (the Dashboard stays hidden), see ``-x EXPERIMENT_NAME -s STATE_NAME``"""
import sys
from pymodaq_gui.qt_utils import mkQApp
from pymodaq.dashboard import load_dashboard_with_arguments
from pymodaq.utils.gui_utils.loader_utils import create_extension

app = mkQApp('Move and Grab')
win, dashboard, _ = load_dashboard_with_arguments(show_dashboard=False, load_extension=False)
win.mainwindow.setVisible(False)
win_ext, ext = create_extension(dashboard, MoveAndGrab, show_extension=True)
sys.exit(app.exec())


if __name__ == '__main__':
main()
10 changes: 5 additions & 5 deletions tests/test_exporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
from pathlib import Path
import pytest

from pymodaq.utils.h5modules.saving import H5SaverLowLevel
from pymodaq.utils import data as data_mod
from pymodaq.utils.h5modules.data_saving import DataSaverLoader, AxisSaverLoader
from pymodaq_data.h5modules.saving import H5SaverLowLevel
from pymodaq_data import data as data_mod
from pymodaq_data.h5modules.data_saving import DataSaverLoader, AxisSaverLoader

from pymodaq.utils.h5modules import exporter as h5export
from pymodaq.utils.h5modules.utils import register_exporter, register_exporters
from pymodaq_data.h5modules import exporter as h5export
from pymodaq_data.h5modules.utils import register_exporter, register_exporters


def test_register_exporter_mock_examples():
Expand Down
94 changes: 94 additions & 0 deletions tests/test_move_and_grab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Tests of the reference extension: it moves a mock actuator, grabs a mock detector, shows and saves the data"""
import pytest
import qt_themes
from pytest import fixture
from qtpy import QtWidgets

pytest.importorskip('pymodaq_plugins_mock', reason='The mock instruments of PyMoDAQ are needed')

from pymodaq_utils.config import GlobalConfig

from pymodaq.control_modules.enums import DAQTypesEnum
from pymodaq.dashboard import create_load_dashboard
from pymodaq.utils.custom_ext import CustomExt
from pymodaq.utils.gui_utils.loader_utils import create_extension

from pymodaq_plugins_mockexamples.extensions import move_and_grab
from pymodaq_plugins_mockexamples.extensions.move_and_grab import MoveAndGrab


def test_is_a_dashboard_extension():
assert move_and_grab.EXTENSION_NAME
assert move_and_grab.CLASS_NAME == MoveAndGrab.__name__
assert issubclass(MoveAndGrab, CustomExt)


@fixture
def extension(qtbot):
"""The extension, running in a Dashboard with an initialized mock actuator and a mock 0D detector"""
config = GlobalConfig()
qt_themes.set_theme(theme=config('gui', 'style', 'theme')[0], style=config('gui', 'style', 'style')[0])
shared_ui, dashboard = create_load_dashboard(show_dashboard=False)
qtbot.addWidget(shared_ui.parent)
qtbot.addWidget(dashboard.tree)

creator = dashboard.module_creator
actuator = creator.create_actuator('Move', 'Mock', None)
creator.set_actuator_type(actuator, 'Mock')
creator.add_actuator(actuator)
detector = creator.create_detector('Det', DAQTypesEnum.DAQ0D)
creator.set_detector_type(detector, DAQTypesEnum.DAQ0D, 'Mock')
creator.add_detector(detector)
for module in (actuator, detector):
module.init_hardware_ui(True)
qtbot.waitUntil(lambda: actuator.initialized_state and detector.initialized_state, timeout=10000)
dashboard.modules_manager.actuators_all = [actuator]
dashboard.modules_manager.detectors_all = [detector]

win_ext, ext = create_extension(dashboard, MoveAndGrab, show_extension=False)
ext.do_things_after_experiment_set('mock experiment', False) # what the Dashboard does once an experiment is set
yield ext, actuator
ext.quit_fun()
QtWidgets.QApplication.processEvents()
dashboard.quit_fun()


def test_lists_the_instruments_of_the_dashboard(extension):
ext, _ = extension
assert ext.settings['actuator'] == 'Move'
assert ext.settings['detector'] == 'Det'


def test_move_and_grab(extension, qtbot):
ext, actuator = extension
ext.settings['target'] = 5.
ext.move_and_grab()
assert ext._busy and not ext.is_action_enabled('move_and_grab') # the GUI is not blocked, the action is disabled
qtbot.waitUntil(lambda: not ext._busy, timeout=15000)

assert ext.settings['status'] == 'Done'
position = ext._positions.get_data_from_name('Move')
assert position.units == actuator.units # the target and the positions have the units of the actuator
assert float(position.data[0][0]) == pytest.approx(5., abs=actuator.epsilon) # reached within the precision
assert len(ext.viewer.viewers) == 1 # the data of the detector is displayed


def test_cannot_quit_while_busy(extension, qtbot):
ext, _ = extension
ext.move_and_grab()
assert ext._quit_fun() is False
qtbot.waitUntil(lambda: not ext._busy, timeout=15000)
assert ext._quit_fun() is True
ext.modules_manager.timeout_signal.connect(ext._on_timeout) # reconnected for the quit of the fixture


def test_save(extension, qtbot, tmp_path):
ext, _ = extension
ext.h5_manager.get_h5saver().settings['base_path'] = str(tmp_path) # not in the user's data folder
ext.settings['save'] = True
ext.move_and_grab()
qtbot.waitUntil(lambda: not ext._busy, timeout=15000)

h5saver = ext.h5_manager.h5saver
group = h5saver.get_node('/RawData/MoveAndGrab')
assert len(h5saver.get_children(group)) > 0 # the position and the data have been saved
Loading