.. _instrument_plugin_doc: Instrument Plugins ================== .. toctree:: :maxdepth: 3 :caption: Contents: Any new hardware has to be included in PyMoDAQ within a :term:`plugin`. A PyMoDAQ's plugin is a python package containing several added functionalities such as instruments objects. A instrument object is a class inheriting from either a ``DAQ_Move_base`` or a ``DAQ_Viewer_base`` class and implementing mandatory methods for easy and quick inclusion of the instrument within the PyMoDAQ control modules. Plugins are articulated given their type: Moves or Viewers and for the latter their main dimensionality: **0D**, **1D** or **2D**. It is recommended to start from the *template* `repository`__ that includes templates for all kind of instruments and also the generic structure to build and publish a given plugin. You will find below some information on the **how to** but comparison with existing plugins packages will be beneficial. .. note:: You'll find in this documentation a detailed tutorial on :ref:`plugin_development`. __ https://github.com/PyMoDAQ/pymodaq_plugins_template Installation ------------ The main and official list of plugins is located in the `pymodaq_plugin_manager`__ repository on github. This constitutes a list of (contributed) python package that can be installed using the :ref:`PluginManager` (or directly using pip). Other unofficial plugins may also be installed if they follow PyMoDAQ's plugin specifications but you are invited to let know other users of the plugins you develop in order to contribute to PyMoDAQ's development. PyMoDAQ is looking at startup for all installed packages that it can consider as its plugins. This includes by default the *pymodaq_plugins_mock* package of mock instruments installed on the *site_packages* location in your python distribution. __ https://github.com/PyMoDAQ/pymodaq_plugin_manager Contributions ------------- If you wish to develop a plugin specific to a new hardware or feature not present on the github repo (and I strongly encourage you to do so!!), you will have to follow the rules as below. Two cases are possible: either you want to add a new hardware from a manufacturer for which a repository already exists 1) (thorlabs, PI, Andor...) or not 2) #. You have to fork the existing repo #. you will use the `pymodaq_plugins_template`__ on github to create a new repo (see also the :ref:`new_plugin` tutorial) Once you've done that, you can clone the package locally and install it in developer using ``pip install -e .`` from the command line where you *cd* within the cloned package. This command will install the package but any change you apply on the local folder will be applied on the package. Then just add a new python file in the correct location. Once you're ready with a working plugin, have your repo hosted in the PyMoDAQ GitHub organization (you stay its official maintainer): a plugin owned by a single person is lost when this person moves on, in the organization someone can always take over. See :ref:`plugin_hosting` for the reason, the transfer procedure and how the plugin is then released on PyPI. .. note:: Starting with PyMoDAQ version 4.1.0 onwards, old github actions for publication and suite testing should be updated in the plugin packages. You can just use the one from the template repository __ https://github.com/PyMoDAQ/pymodaq_plugins_template All the packages published on pypi using the template and the naming convention will be available in the plugin manager. A very detailed tutorial has been published in this documentation: :ref:`plugin_development` and you can in the mean time look at this `video`__ __ https://youtu.be/9O6pqz89UT8 Naming convention ----------------- For an instrument plugin to be properly recognised by PyMoDAQ, the location and name of the underlying script must follow some rules and syntax. The `plugin template package`__ could be copied locally as a starting point: * The plugin package will be named pymodaq_plugins_xxxx (name: xxxx) * An actuator plugin (name: xxxx) will be a script whose name is daq_move_Xxxx (notice first X letter is capital) * The main instrument class within the script will be named DAQ_Move_Xxxx (notice the capital letters here as well and sorry if it is troublesome) * A detector plugin of dimensionality N (N=0, 1, 2 or N) (name: xxxx) will be a script whose name is daq_NDviewer_Xxxx (notice first X letter is capital, and replace N by 0, 1, 2 or leave it for higher dimensionality) * The main instrument class within the script will be named DAQ_NDViewer_Xxxx (notice the capital letters here as well) __ https://github.com/PyMoDAQ/pymodaq_plugins_template .. _plugin_acceptance_tests: Testing your plugin ------------------- A plugin contribution is accepted if it passes the acceptance checks shipped with PyMoDAQ. They need no hardware and are available at three levels, from the quickest to the deepest. Start with the first one, the others build on it: #. :ref:`the check_plugin command `: a report on your plugin package, from the command line, without pytest and without installing the plugin; #. :ref:`the same checks as tests ` in your plugin repository, to run them in your CI; #. :ref:`tests of the behaviour ` of your plugin class against a fake of its controller. .. _check_plugin_command: The check_plugin command ************************ ``check_plugin`` analyses the source of the plugin with *static rules* (nothing is imported, so it also works when the instrument SDK is not installed) and checks the plugin modules that can be imported. It is the fastest way to know whether your package is recognised by PyMoDAQ, and the one to run first and often. From the root of your plugin repository (the exit code is non zero when the checks fail): .. code-block:: bash check_plugin [package name, folder or instrument file] [--fail-on error|warning|todo] [--strict-imports] [-v] [--color auto|always|never] The report is colored in a terminal (``--color auto``, the default); set the ``NO_COLOR`` environment variable or use ``--color never`` to disable it, or ``FORCE_COLOR`` / ``--color always`` to force it (for instance in a CI log). The argument can also be a single instrument module of a plugin package (``daq_move_Xxxx.py`` or ``daq_NDviewer_Xxxx.py``): only this module is then checked, with its todos, not the package (*pyproject.toml*, entry points...). The argument is either the name of an installed plugin package, or a folder: the plugin repository or its package folder (the current folder by default). The plugin does not need to be installed, and the *pyproject.toml* can still have the template name, which is then reported; only the entry points cannot be checked before the installation. Every finding has a code, a severity, a location and a hint to fix it. The severities are: * ``error``: the plugin will not work or will not be seen by PyMoDAQ * ``warning``: deprecated or suspicious * ``todo``: the plugin is not finished (leftovers of the template) By default only the errors make the command fail, the others are listed (the todos are also counted). Use ``--fail-on warning`` or ``--fail-on todo`` (everything) to be stricter, for instance before a release or to review a contribution. A module that cannot be imported because of a missing third party module or SDK is reported but does not fail the command; add ``--strict-imports`` to make it fail, which is advised in the CI of your plugin as its dependencies are installed there. The findings are identified by these codes: ======== ================================================================================================= Code Meaning ======== ================================================================================================= PMQ101-3 project name does not follow the convention, still the template one, differs from the package folder PMQ104-6 template placeholders left in *pyproject.toml* (url, description, authors) PMQ107 no pymodaq dependency declared PMQ108 no entry points: PyMoDAQ will not find the plugin PMQ109 code found for a feature set to false in ``[features]`` (its entry points will not be generated) PMQ110 feature set to true but nothing found. PMQ111: README.rst or LICENSE missing PMQ112 python file that is not a valid module name (often a conflicted copy of a synchronisation tool): ignored PMQ201 TODO comment. PMQ202: ``NotImplementedError`` left. PMQ203: template placeholder name left PMQ204 example module of the template left. PMQ205: *config_template.toml* still has the template title PMQ302 deprecated or ignored class attribute (``_epsilon``, ``stage_names``, ``axis_names``) PMQ303 ``data_actuator_type`` is not ``DataActuatorType.DataActuator`` PMQ304-6 (module that cannot be imported) inconsistent axes/units, unknown unit, missing mandatory method PMQ307 import of the removed ``pymodaq.daq_utils`` PMQ308 no ``if __name__ == '__main__': main(__file__)`` block to run the plugin standalone PMQ309 ``ini_stage`` / ``ini_detector`` do not return ``(info, initialized)`` PMQ310 the plugin class has no docstring. PMQ311: *stop_motion* not overridden, the Stop of the actuator does nothing PMQ312 deprecated hook of an actuator (``check_position``, ``move_Abs``, ``move_Rel``, ``move_Home``) PMQ313 deprecated attribute ``current_position`` / ``target_position`` (use ``*_value`` instead) PMQ314 ``comon_parameters`` of the actuators used as a list: it is a function, the import fails PMQ315 invalid escape sequence in a string, such as ``"C:\Program Files"`` (use a raw string) PMQ316 settings group ``multiaxes`` / ``multi_status`` renamed ``controller`` / ``controller_status`` PMQ317 code that only works on Windows and runs on import (``windll``, ``winreg``, drive path); not reported in the modules used only by instruments declaring ``supported_platforms = ('win32',)`` PMQ318 deprecated ``ini_stage_init`` / ``ini_detector_init``; write ``if self.is_master: self.controller = AWrapper()`` / ``else: self.controller = controller`` PMQ319 unknown value in ``supported_platforms`` / ``supported_architectures`` (see :ref:`plugin_platforms`) PMQ320 the ``Operating System`` classifiers of the ``pyproject.toml`` do not match ``supported_platforms``; ``check_plugin --fix-classifiers`` rewrites them PMQ330-1 extension (``extensions`` folder): ``EXTENSION_NAME`` / ``CLASS_NAME`` missing, so the Dashboard does not list it; ``CLASS_NAME`` is not a class of the module PMQ332 the extension class does not derive from ``CustomExt`` (a ``CustomApp`` has no access to the Dashboard) PMQ333 import from a deprecated module of pymodaq (``pymodaq.utils.parameter``, ``pymodaq.utils.messenger``, ``pymodaq.utils.math_utils``...) in an extension PMQ334 deprecated hooks ``setup_docks`` / ``setup_menu`` of an extension (use ``setup_docks_and_widgets`` / ``setup_menus_and_toolbars``) PMQ335 the extension creates its own ``ExperimentManager`` or ``StateManager`` instead of using the ones of the Dashboard PMQ336 ``__init__`` of the extension does not take ``(parent, dashboard)``. PMQ337: it never calls ``self.setup_ui()`` PMQ338 a class deriving from ``CustomExt`` has no ``_quit_fun`` guard, or one that never returns ``True``: ``quit_fun`` then never closes the extension PMQ339 the extension class overrides ``quit_fun`` instead of ``_quit_fun`` (the guard called by ``quit_fun``) ======== ================================================================================================= The functions behind this command (*check_package_layout*, *check_move_class*, *check_viewer_class*) return the list of problems found and can be used from a script. To get a full report of an installed plugin package without pytest, use ``print(check_plugin_package('pymodaq_plugins_xxxx'))`` (the returned *PluginReport* also has *ok*, *failures*, *todos* and *to_dict()*; use ``check_plugin_package(..., fail_on='todo')`` and ``report.format(verbose=True)`` to see every todo). Lint your code too, at least with ``flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics`` (the gate used by the PyMoDAQ CI) or ``ruff``. Silencing a finding on purpose ****************************** A finding that is intended (a plugin written to test an error, a deprecated name kept on purpose...) can be silenced, like ``# noqa`` for flake8, by a comment on the line the finding reports (its line is shown in the report):: self.settings['multiaxes', 'axis'] # noqa: PMQ316 class DAQ_Move_Test(DAQ_Move_base): # noqa: PMQ303, PMQ310 Only the codes listed are silenced, the other findings of the line are still reported. .. _plugin_pytest_checks: The same checks as tests ************************ To run the same checks in your plugin repository, for instance in your CI, use a test module such as *tests/test_plugin.py*. The pytest mixin applying them is provided by ``pymodaq.utils.plugin_testing`` from PyMoDAQ 5.3.2 on (in 5.3.1 it was ``pymodaq_utils.plugin_testing``, still importable but deprecated). The checks themselves stay in ``pymodaq_utils``, so that the ``check_plugin`` command remains fast and does not start the whole PyMoDAQ initialization. The test module should skip itself with older versions, where the mixin does not exist: .. code-block:: python import pytest pytest.importorskip('pymodaq.utils.plugin_testing', reason='The plugin acceptance checks need PyMoDAQ >= 5.3.2') from pymodaq.utils.plugin_testing import PluginPackageChecks class TestMyPlugin(PluginPackageChecks): package_name = 'pymodaq_plugins_xxxx' # optional, otherwise found from the package folder This file replaces any generic test file inherited from the template; keep only tests that are specific to your instrument next to it. Every plugin module found in the package is checked individually (``pytest -v`` lists them). The tests apply the same rules and codes as ``check_plugin``, and in particular: * the package name, its *config* and *__version__*, and the ``pymodaq.plugins`` / ``pymodaq.instruments`` entry points (and the optional extension, model, scanner... ones, that must be loadable) * the naming convention of the modules and of the classes (see above), and that no module is wrongly named * the plugin modules can be imported *without the instrument vendor SDK* (guard these imports with *try/except*) * the plugin class derives from the right base class and overrides the mandatory methods (*ini_stage*, *get_actuator_value*, *close* for an actuator; *ini_detector*, *grab_data*, *stop*, *close* for a detector), see :ref:`plugin_contract` * for an actuator, ``_axis_names``, ``_controller_units`` and ``_epsilons`` are consistent and the units are known from `pint` * ``params`` is a valid list of dict producing a settings tree As with the command, only the errors fail the tests by default. Set the class attribute ``fail_on = 'warning'`` or ``fail_on = 'todo'`` (everything) in your test class to be stricter, for instance before a release or to review a contribution. A module that cannot be imported because of a missing third party module or SDK is skipped; set ``strict_imports = True`` to make it fail (the ``--strict-imports`` of the command), which is advised in the CI of your plugin. .. _plugin_behaviour_tests: Testing the behaviour of your plugin without hardware ----------------------------------------------------- The acceptance checks look at the structure of the plugin. To check that it also *behaves* (the initialization, the moves, the units of the positions, the data emitted by a grab, the release of the controller in ``close``), run the plugin class against a fake of its controller. Keep the communication with the instrument behind a small wrapper class in the *hardware* folder of the plugin package, write a fake of this wrapper with the same public methods (in memory, instantaneous, recording the calls it receives), and drive the plugin with the helpers of ``pymodaq.utils.plugin_testing`` (available from PyMoDAQ 5.3.2). They run in the calling thread, with no visible GUI, and fail with a ``SignalTimeout`` if the plugin never emits the expected signal: .. code-block:: python import pytest from pymodaq.utils.plugin_testing import make_actuator, move_abs_and_wait, make_detector, grab_and_wait, assert_units from pymodaq_plugins_xxxx.daq_move_plugins import daq_move_Xxxx from pymodaq_plugins_xxxx.daq_viewer_plugins.plugins_1D import daq_1Dviewer_Xxxx class FakeWrapper: position = 0. def move_at(self, value): self.position = value def get_position(self): return self.position def close(self): pass def test_move_abs(monkeypatch): # 'XxxxWrapper' is the name of the wrapper that the plugin normally imports to control the instrument # (as it appears in the plugin module): here it is replaced by the fake one monkeypatch.setattr(daq_move_Xxxx, 'XxxxWrapper', FakeWrapper) actuator = make_actuator(daq_move_Xxxx.DAQ_Move_Xxxx) assert actuator.initialized position = move_abs_and_wait(actuator, 2.5) # a float is in the axis unit of the actuator assert position.value() == pytest.approx(2.5, abs=actuator.epsilon) actuator.close() In this example ``XxxxWrapper`` stands for the wrapper of your instrument that ``daq_move_Xxxx`` imports and uses (``monkeypatch.setattr`` replaces that name in the plugin module, so use the exact name found there), and ``FakeWrapper`` is the fake that takes its place during the test. ``move_rel_and_wait``, ``move_home_and_wait`` and ``make_detector`` / ``grab_and_wait`` / ``assert_units`` work the same way, and a slave axis of a multi-axes controller is created with ``make_actuator(plugin_class, controller=master.controller)``. The helpers reproduce what the Dashboard does around the plugin: the target is expressed in the axis unit before reaching ``move_abs``, and the end of a move is signalled once ``poll_moving`` has run. After a ``move_home`` the position is not polled (as in PyMoDAQ): ``move_home_and_wait`` waits for the plugin to call ``self.move_done()`` and raises a ``SignalTimeout`` if it never does, see :ref:`move_home`. The tests of the helpers themselves (*tests/utils/plugin_harness_test.py* in the PyMoDAQ repository) contain complete fake actuators (single and multi-axes) and a fake detector to copy. .. note:: These tests check the plugin's side of its contract with PyMoDAQ (return types, units, signals, shared controller, ``close``) against a fake that encodes what you believe the driver does. They say nothing about the device itself: timing, error replies, real ranges and units. A passing run is not a hardware validation, say in a contribution what was tested on the real instrument and what only against a fake. .. _plugin_contract: The methods of a plugin ----------------------- These are the methods a plugin overrides. *Required* ones are checked by ``check_plugin`` and by the plugin tests: the others have a default in the base class that does nothing or raises ``NotImplementedError``, override them when your instrument can do it. The list is defined once, in ``pymodaq_utils.plugin_tools.contract``, and read by the checks, the tests and this table. .. include:: /developer_folder/plugin_contract_table.inc .. _plugin_platforms: Plugins working on some systems only ------------------------------------ A plugin for a Windows-only driver, or for the hardware of a Raspberry Pi, declares the systems it works on in the class of its instrument plugin, with two optional class attributes (``None``, the default of the base classes, means all the systems and all the architectures): .. code-block:: python class DAQ_Move_Xxxx(DAQ_Move_base): supported_platforms = ('win32',) # a Windows driver class DAQ_1DViewer_Yyyy(DAQ_Viewer_base): supported_platforms = ('linux',) # a Raspberry Pi board supported_architectures = ('aarch64', 'armv7l') On another system PyMoDAQ does not initialize the plugin and shows *This plugin only works on Windows (this computer: linux, x86_64)* in place of the usual information. ``check_plugin`` reads the declaration (it prints *Works on: Windows*) and checks it: * the ``Operating System`` classifiers of the ``pyproject.toml`` have to match it (PMQ320): run ``check_plugin --fix-classifiers`` to write them, instead of the default *OS Independent*. When a package holds several instruments the classifiers are those of all of them together. The architecture has no classifier: it is only used by PyMoDAQ and ``check_plugin``. To avoid running it, let the build compute them (next paragraph); * the values have to be known (PMQ319). The accepted values and the classifier they give are: .. include:: /developer_folder/plugin_platforms_table.inc The same lists are in ``pymodaq_utils.plugin_tools.platforms`` (``PLATFORMS`` and ``ARCHITECTURES``). ``sys.platform`` and ``platform.machine()`` give the values of the computer. When a plugin works on several systems, or on all but one, its module still has to be **importable on every system**, as PyMoDAQ lists all the plugins at startup (PMQ317 warns about Windows-only code run on import; it is not reported in the modules that are only used, directly or through other modules, by instruments declaring ``supported_platforms = ('win32',)``). Import the vendor SDK in a ``try/except`` (or under ``sys.platform``), and let ``ini_stage`` / ``ini_detector`` return ``(message, False)`` when the SDK is missing (see PMQ317). In the ``pyproject.toml``, restrict the dependencies that only exist for these systems with an environment marker, so that ``pip`` does not try to install them elsewhere:: dependencies = [ "pymodaq>=5.3.1", "RPi.GPIO; platform_machine == 'aarch64' or platform_machine == 'armv7l'", "pywin32; sys_platform == 'win32'", ] Computing the classifiers when building *************************************** Instead of keeping the ``Operating System`` classifiers up to date by hand, the build can write them. Declare the ``classifiers`` field as dynamic and move the other classifiers to ``[tool.pymodaq]``:: [project] dynamic = ["version", "urls", "entry-points", "classifiers"] [tool.pymodaq] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Science/Research", ] The metadata hook of the plugin (``hatch_build.py``, see the plugin template) then adds the Python classifiers (``Programming Language :: Python :: 3.11``...) of the versions supported by PyMoDAQ that ``requires-python`` accepts (``PYTHON_VERSIONS`` in ``pymodaq_utils.plugin_tools.platforms``), and the ``Operating System`` classifiers of the ``supported_platforms`` declared in the plugin (*OS Independent* if there is none), so that the published package is always consistent with the code. This needs ``pymodaq_utils>=5.3.6`` in the ``requires`` of the ``[build-system]``. ``check_plugin --fix-classifiers`` has nothing to write in that case. Adding an instrument to your plugin ----------------------------------- To avoid naming mistakes, a helper creates a new instrument module in your plugin package, from the root of your plugin repository: .. code-block:: bash python -m pymodaq_utils.plugin_tools.scaffold move Xxxx # daq_move_Xxxx.py with the class DAQ_Move_Xxxx python -m pymodaq_utils.plugin_tools.scaffold 1D Xxxx # daq_1Dviewer_Xxxx.py with the class DAQ_1DViewer_Xxxx (use ``--folder`` to give the *pymodaq_plugins_xxxx* package folder). The module derives from the right base class, declares the mandatory attributes and methods, and its ``TODO`` comments tell where to put your instrument code. The static rules above list what is left to do. .. _hardware_settings: .. _value_reached: Value Reached? -------------- Every time, you trigger an absolute or relative value change in your DAQ_Move, PyMoDAQ will wait before sending a `move_done` signal. The value change is considered as done if the current value reached the target value at a certain tolerance. This tolerance is a float number called epsilon within the code and in the UI. Here is a snippet of this calculation: .. code-block:: if not abs(self._current_value - self._target_value) < self.epsilon: # keep trying to reach the value .. note:: From PyMoDAQ >= 4.4.0, this condition include working units, the code snippet looks like: .. code-block:: if not (self._current_value - self._target_value).abs().value(self.axis_unit) < self.epsilon: # keep trying to reach the value where all quantities holds an information about their units, for instance `epsilon` could be expressed in *µm* (micron) (controller units) while _current_value (related to what is printed on the UI) could be expressed in *mm*. This feature is available naturally by using the PyMoDAQ DataManagement ecosystem with units compatibility. .. _move_home: Moving to the home position --------------------------- The *Find Home* button of a :py:class:`DAQ_Move` calls the ``move_home`` method of the plugin. Its purpose is to let the actuator look for a reference (a physical limit, a switch, an index of the encoder...) so that the controller knows this position is the *home* (zero) position. Contrary to ``move_abs`` and ``move_rel``, PyMoDAQ does not know the position to reach: there is no target, no ``epsilon`` check, no polling and no timeout after a ``move_home``. The plugin is therefore responsible for telling when the homing is over. Until it does, the UI stays in the *Moving* state and the displayed value is not refreshed. To do so, the plugin calls ``self.move_done()`` once the homing is finished. This method reads the position of the actuator using your ``get_actuator_value`` method, emits the ``move_done_signal`` with it (this updates the displayed value and the *Moving* state of the UI) and flags the move as done. The way to do it depends on the controller: * **blocking homing**: the controller method returns when the reference has been found. Call ``self.move_done()`` right after it: .. code-block:: python def move_home(self): self.controller.home() # returns when the reference has been found self.move_done() # reads get_actuator_value() and signals the end of the move * **asynchronous homing**: the controller starts the homing and returns immediately, and notifies the end of it with a callback (or an event, a status to poll...). Register a callback calling ``self.move_done()``. Note that the callback may be called from a thread of the driver, make sure the controller can be queried from there: .. code-block:: python def move_home(self): # the callback receives arguments from the driver, that must not be given to move_done self.controller.home(callback=lambda *args: self.move_done()) If the homing takes long, you can refresh the value displayed meanwhile with ``self.emit_value(position)``, see :ref:`DAQ_Move specific commands`. Hardware Settings ----------------- An important feature similar for all modules is the layout as a tree structure of all the hardware parameters. These settings will appear on the UI as a tree of parameters with a title and different types, see :numref:`figure_settings`. On the module side, they will be instantiated as a list of dictionaries and later exist in the object ``self.settings``. This object inherits from the ``Parameter`` object defined in `pyqtgraph`__. __ https://pyqtgraph.readthedocs.io/en/latest/api_reference/parametertree/parameter.html .. _figure_settings: .. figure:: /image/settings_example.PNG :alt: Settings example Typical hardware settings represented as a tree structure (here from the ``daq_2Dviewer_AndorCCD`` plugin) Here is an example of such a list of dictionaries corresponding to :numref:`figure_settings`: .. code-block:: python [{'title': 'Dll library:', 'name': 'andor_lib', 'type': 'browsepath', 'value': libpath}, {'title': 'Camera Settings:', 'name': 'camera_settings', 'type': 'group', 'expanded': True, 'children': [ {'title': 'Camera SN:', 'name': 'camera_serialnumber', 'type': 'int', 'value': 0, 'readonly': True}, {'title': 'Camera Model:', 'name': 'camera_model', 'type': 'str', 'value': '', 'readonly': True}, {'title': 'Readout Modes:', 'name': 'readout', 'type': 'list', 'values': ['FullVertBinning','Imaging'], 'value': 'FullVertBinning'}, {'title': 'Readout Settings:', 'name': 'readout_settings', 'type': 'group', 'children':[ {'title': 'single Track Settings:', 'name': 'st_settings', 'type': 'group', 'visible': False, 'children':[ {'title': 'Center pixel:', 'name': 'st_center', 'type': 'int', 'value': 1 , 'default':1, 'min':1}, {'title': 'Height:', 'name': 'st_height', 'type': 'int', 'value': 1 , 'default':1, 'min':1}, ]},]}]}] .. _parameter_tree: The list of available types of parameters (defined in ``pymodaq_gui.parameter.pymodaq_ptypes``) is: * ``group`` : "camera settings" on :numref:`figure_settings` is of type group * ``int`` : settable integer (SpinBox_Custom object) * ``float`` : settable float (SpinBox_Custom object) * ``str`` : a QLineEdit object (see Qt5 documentation) * ``list`` : "Readout Modes" :numref:`figure_settings` is a combo box * ``bool`` : checkable boolean * ``bool_push`` : a checkable boolean in the form of a QPushButton * ``led`` : non checkable boolean in the form of a green (True) of red (False) led * ``led_push`` : checkable boolean in the form of a green (True) of red (False) led * ``date_time`` : a QDateTime object (see Qt5 documentation) * ``date`` : a QDate object (see Qt5 documentation) * ``time`` : a QTime object (see Qt5 documentation) * ``slide`` : a combination of a slide and spinbox for floating point values (linear of log scale) * ``itemselect`` : an object to easily select one or more items among a few * ``browsepath``: a text area and a pushbutton to select a given path or file * ``text`` : a text area (for comments for instance) **Important**: the *name* key in the dictionaries must **not** contain any space, please use underscore if necessary! .. note:: For a live example of these Parameters and their widget, type in ``parameter_example`` in your shell or check the example folder Once the module is initialized, any modification on the UI hardware settings will be send to the plugin through the ``commit_settings`` method of the plugin class and illustrated below (still from the ``daq_2Dviewer_AndorCCD`` plugin). The ``param`` method argument is of the type ``Parameter`` (from ``pyqtgraph``): .. code-block:: python def commit_settings(self,param): """ | Activate parameters changes on the hardware from parameter's name. """ try: if param.name()=='set_point': self.controller.SetTemperature(param.value()) elif param.name() == 'readout' or param.name() in custom_parameter_tree.iter_children(self.settings.child('camera_settings', 'readout_settings')): self.update_read_mode() elif param.name()=='exposure': self.controller.SetExposureTime(self.settings.child('camera_settings','exposure').value()/1000) #temp should be in s (err, timings) = self.controller.GetAcquisitionTimings() self.settings.child('camera_settings','exposure').setValue(timings['exposure']*1000) elif param.name() == 'grating': index_grating = self.grating_list.index(param.value()) self.get_set_grating(index_grating) self.emit_status(ThreadCommand('show_splash', ["Setting wavelength"])) err = self.controller.SetWavelengthSR(0, self.settings.child('spectro_settings','spectro_wl').value()) self.emit_status(ThreadCommand('close_splash')) .. _data_emission: Emission of data ---------------- When data are ready (see :ref:`data_ready` to know about that), the plugin has to notify the viewer module in order to display data and eventually save them. For this PyMoDAQ use two types of signals (see pyqtsignal documentation for details): * ``dte_signal_temp`` * ``dte_signal`` where ``dte`` stands for DataToExport, see :ref:`datatoexport`. .. note:: So far (07/07/2023) instrument plugins would use signals below to emit a list of DataFromPlugins objects * ``data_grabed_signal_temp`` (old style, will be deprecated) * ``data_grabed_signal`` (old style, will be deprecated) It will be deprecated in versions > 4.1, as the object to use and emit are now DataToExport objects They both *emit* the same type of signal but will trigger different behaviour from the viewer module. The first is to be used to send temporary data to update the plotting but without triggering anything else (so that the DAQ_Scan still awaits for data completion before moving on). It is also used in the initialisation of the plugin in order to set the type and number of data viewers displayed by the viewer module. The second signal is to be used once data are fully ready to be send back to the user interface and further processed by DAQ_Scan or DAQ_Viewer instances. The code below is an example of emission of data: .. code-block:: python from pymodaq_data.data import Axis, DataToExport from pymodaq.utils.data import DataFromPlugins x_axis = Axis(label='Wavelength', units= "nm", data = vector_X) y_axis = Axis(data=vector_Y) self.dte_signal.emit(DataToExport('mydata', data=[ DataFromPlugins(name='Camera',data=[data2D_0, data2D_1,...], dim='Data2D', x_axis=x_axis,y_axis=y_axis), DataFromPlugins(name='Spectrum',data=[data1D_0, data1D_1,...], dim='Data1D', x_axis=x_axis, labels=['label0', 'label1', ...]), DataFromPlugins(name='Current',data=[data0D_0, data0D_1,...], dim='Data0D'), DataFromPlugins(name='Datacube',data=[dataND_0, dataND_1,...], dim='DataND', nav_indexes=(0,2), axes=[Axis(data=.., label='Xaxis', units= "µm", index=0)])) Such an emitted signal would trigger the initialization of 4 data viewers in the viewer module. One for each ``DataFromPlugins`` in the data attribute (which is a list of DataFromPlugins). The type of data viewer will be determined by the *dim* key value while its name will be set to the *name* parameter value, for more details on data objects, see :ref:`data_objects` .. note:: *New in version 4.1.0* *Deprecated in version 4.2.0, but still working* The behaviour of the `DAQ_Viewer` can be even more tailored using two extra boolean attributes in the DataWithAxes objects. * ``save``: will tell the DAQ_Viewer whether it should save the corresponding dwa (short for ``DataWithAwes``) * ``plot``: will tell the DAQ_Viewer whether it should plot the corresponding dwa *New in version 4.2.0* the `save` and `plot` extra-attributes have been replaced by: * ``do_save``: will tell the DAQ_Viewer whether it should save the corresponding dwa (short for ``DataWithAwes``) * ``do_plot``: will tell the DAQ_Viewer whether it should plot the corresponding dwa ``DataFromPlugins`` objects have these two extra attributes by default with values set to True .. _data_ready: Data ready? *********** One difficulty with these viewer plugins is to determine when data is ready to be read from the controller and then to be send to the user interface for plotting and saving. There are a few solutions: * **synchronous**: The simplest one. When the ``grab`` command has been send to the controller (let's say to its ``grab_sync`` method), the ``grab_sync`` method will hold and freeze the plugin until the data are ready. The Mock plugin work like this. * **asynchronous**: There are 2 ways of doing asynchronous *waiting*. The first is to poll the controller state to check if data are ready within a loop. This polling could be done with a while loop but if nothing more is done then the plugin will still be freezed, except if one process periodically the Qt queue event using ``QtWidgets.QApplication.processEvents()`` method. The polling can also be done with a timer event, firing periodically a check of the data state (ready or not). Finally, the nicest/hardest solution is to use callbacks (if the controller provides one) and link it to a ``emit_data`` method. Synchronous example: ******************** The code below illustrates the poll method using a loop: .. code-block:: python def poll_data(self): """ Poll the current data state """ sleep_ms=50 ind=0 data_ready = False while not self.controller.is_ready(): QThread.msleep(sleep_ms) ind+=1 if ind*sleep_ms>=self.settings.child(('timeout')).value(): self.emit_status(ThreadCommand('raise_timeout')) break QtWidgets.QApplication.processEvents() self.emit_data() Asynchronous example: ********************* The code below is derived from *daq_Andor_SDK2* (in *andor* hardware folder) and shows how to create a thread waiting for data ready and triggering the emission of data .. code-block:: python class DAQ_AndorSDK2(DAQ_Viewer_base): callback_signal = QtCore.Signal() #used to talk with the callback object ... def ini_camera(self): ... callback = AndorCallback(self.controller.WaitForAcquisition) # the callback is linked to the controller WaitForAcquisition method self.callback_thread = QtCore.QThread() #creation of a Qt5 thread callback.moveToThread(self.callback_thread) #callback object will live within this thread callback.data_sig.connect(self.emit_data) # when the wait for acquisition returns (with data taken), emit_data will be fired self.callback_signal.connect(callback.wait_for_acquisition) # self.callback_thread.callback = callback self.callback_thread.start() def grab(self,Naverage=1,**kwargs): ... self.callback_signal.emit() #trigger the wait_for_acquisition method def emit_data(self): """ Function used to emit data obtained by callback. """ ... self.dte_signal.emit( DataToExport('mydata', data=[DataFromPlugins('Camera', data=[np.squeeze(self.data.reshape((sizey, sizex)).astype(np.float))])]) class AndorCallback(QtCore.QObject): data_sig=QtCore.Signal() def __init__(self,wait_fn): super(AndorCallback, self).__init__() self.wait_fn = wait_fn def wait_for_acquisition(self): err = self.wait_fn() if err != 'DRV_NO_NEW_DATA': #will be returned if the main thread called CancelWait self.data_sig.emit() Documentation from Andor SDK concerning the WaitForAcquisition method of the dll: .. | *unsigned int WINAPI WaitForAcquisition(void)* | | ``WaitForAcquisition`` can be called after an acquisition is started using StartAcquisition to put the calling thread to sleep until an Acquisition Event occurs. | It will use less processor resources than continuously polling with the GetStatus function. If you wish to restart the calling thread without waiting for an Acquisition event, call the function CancelWait. .. _hardware_averaging: Hardware averaging ****************** By default, if averaging of data is needed the Viewer module will take care of it software wise. However, if the hardware controller provides an efficient method to do it (that will save time) then you should set the class field ``hardware_averaging`` to ``True``. .. code-block:: python class DAQ_NDViewer_Template(DAQ_Viewer_base): """ Template to be used in order to write your own viewer modules """ hardware_averaging = True #will use the accumulate acquisition mode if averaging #is True else averaging is done software wise .. _live_mode: Live Mode ********* By default, the live *Grab* mode is done software wise in the core code of the DAQ_Viewer. However, if one want to send data as fast as possible, the live mode is possible within a plugin. For this, the plugin class attribute, ``live_mode_available``, should be set to ``True``. .. code-block:: python class DAQ_2DViewer_MockCamera(DAQ_Viewer_base): live_mode_available = True The method ``grab_data`` will then receive a named boolean parameter (in ``kwargs``) called *live* that tells if one should grab or snap data. The MockCamera plugin illustrates this feature: .. code-block:: python def grab_data(self, Naverage=1, **kwargs): """Start a grab from the detector Parameters ---------- Naverage: int Number of hardware averaging (if hardware averaging is possible, self.hardware_averaging should be set to True in class preamble and you should code this implementation) kwargs: dict others optionals arguments """ if 'live' in kwargs: if kwargs['live']: self.live = True # self.live = False # don't want to use that for the moment if self.live: while self.live: data = self.average_data(Naverage) QThread.msleep(kwargs.get('wait_time', 100)) self.dte_signal.emit(data) QtWidgets.QApplication.processEvents() Hardware needed files --------------------- If you are using/referring to custom python wrappers/dlls... within your plugin and need a place where to copy them in PyMoDAQ, then use the ``\hardware`` folder of your plugin package. For instance, the ``daq_2Dviewer_AndorCCD`` plugin need various files stored in the ``andor`` folder (on github repository). I would therefore copy it as ``\pymodaq_plugins_andor\hardware\andor`` and call whatever module I need within (meaning there is a __init__.py file in the *andor* folder) as: .. code-block:: python #import controller wrapper from pymodaq_plugins.hardware.andor import daq_AndorSDK2 #this import the module DAQ_AndorSDK2 containing classes, methods... #and then use it as you see fit in your module Actuator plugin having multiple axis ------------------------------------ See also: :ref:`multiaxes_controller` When an actuator's controller can drive multiple axis (like a XY translation stage for instance), the plugin instrument class should defines two class attributes: * `is_multiaxis` should be set to True. This will trigger the display of the multiaxis section on the UI\n Deprecated in 4.4.1 not needed anymore as one read that info from the length of the axis_names variable * `axes_names` should be a list or dict describing the different actuator (axis) such a controller can drive .. note:: New in 4.4.1 Moreover two other attributes should then be transformed as list or dict (depending the definition of the `_axis_names` variable, namely: * `_controller_units`: can be a list or a dict of strings specifying the units to be used by each axis. In case of a list or dict, the type and length should be the same as `axis_names` In case all axes have the same working *units*, could be left as a str. PyMoDAQ handles the rest * `_epsilon`: deprecated, please use `_epsilons` * `_epsilons`: can be a list or dict of float specifying the epsilon to be used by each axis. In case of a list or dict, the type and length should be the same as `axis_names` In case all axes have the same *epsilon*, could be left as a float. PyMoDAQ handles the rest .. code-block:: class DAQ_Move_MockNamedAxes(DAQ_Move_base): _axis_names = ['Xaxis', 'Yaxis', 'Zaxis'] # or: _axis_names = {'Xaxis': 0, 'Yaxis': 1, 'Zaxis': 2} would produce such display on the UI (Fig. :numref:`multiaxes_xyz`): .. _multiaxes_xyz: .. figure:: /image/DAQ_Move/multiaxes_xyz.png :alt: Settings example Typical multiaxis settings represented as a combo box Both the list or the dictionary will produce the same output on the UI but their use will depend of the controller and underlying methods of its driver to act on a particular axis. In the drivers derived from C code, methods will have an argument describing a particular axis as an integer. It is however not possible to pass integers directly to the combobox of the UI who holds strings. To deal with that `pyqtgraph`, and therefore `pymodaq`, uses a dictionary mapping the names of the axis (to be printed in the UI) to objects (here integers) to be used with the drivers's method. A set of methods/properties have been introduced to quickly manipulate those and get either the current axis name of associated *value*. Case of a list of strings: .. code-block:: >>> self.axis_name 'Yaxis' >>> self.axis_names ['Xaxis', 'Yaxis', 'Zaxis'] >>> self.axis_value 'Yaxis' Case of a dictionary of strings/integers: .. code-block:: >>> self.axis_name 'Yaxis' >>> self.axis_names {'Xaxis': 0, 'Yaxis': 1, 'Zaxis': 2} >>> self.axis_value 1 Other properties will then reflect the current axis units and epsilon value: * `self.epsilon` * `self.axis_unit` Modifying the UI from the instrument plugin class ------------------------------------------------- The user interface control module and the instrument plugin class are not in the same thread, moreover, the plugin class is not aware of the UI object (``DAQ_Move`` or ``DAQ_Viewer``). The following shows, how the instrument plugin class ``DAQ_Move_MyPluginClass`` relates to ``DAQ_Move``: .. code-block:: python class DAQ_Move: def init_hardware(self...): hardware: DAQ_Move_Hardware hardware.moveToThread() class DAQ_Move_Hardware: hardware: DAQ_Move_Base class DAQ_Move_MyPluginClass(DAQ_Move_Base): """Plugin class defined in a plugins repository.""" Therefore, you'll find below ways to interact with the UI from the plugin class. The most generic way (valid for both control modules) is to use the ``emit_status`` method, defined in the parent class of the instrument plugin class. Such a method takes one argument, a ``ThreadCommand`` and will send this object to the ``thread_status`` method of the UI main class. .. note:: A :py:class:`ThreadCommand` is an object taking two arguments a string (the command) and a named attribute called attribute that can be any type. This :py:class:`ThreadCommand` is used everywhere in PyMoDAQ to communicate between threads. New in 5.0.1: two extra arguments (args and kwargs) can be used to store arguments that should be passed to an eventual method triggered from the ThreadCommand emission (see the command *update_ui* for an example). Control modules share some commands, see :py:meth:`thread_status` * **update_status**: call the update_status method with status attribute as a ``str``. To print info on the status bar * **close**: close the current thread and delete corresponding attribute on cascade. * **update_main_settings**: update the *main settings* in the UI settings tree. See below for an exemple. The attribute argument is a list containing: * the setting path as a tuple of strings (with the 'main_setting' parent setting) * the new value, limit or option of the setting * the type of change of the setting as a string, either: * 'value': to change the setting value * 'limits': to change the limits of the setting * 'options': any option of the setting * **update_settings**: update the *actuator's settings* in the UI settings tree. The attribute argument is a list containing: * the setting path as a tuple of strings (with the 'main_setting' parent setting) * the new value, limit or option of the setting * the type of change of the setting as a string, either: * *value*: to change the setting value * *limits*: to change the limits of the setting * *options*: any option of the setting * **raise_timeout**: call the raise_timeout method * **show_splash**: show the splash screen displaying info. The attribute argument is a string that will be displayed on the splash screen * **close_splash**: close the splash screen * **update_ui**: call a method of the control module current UI. The attribute argument is a string beeing the name of the method to call. The args and kwargs arguments are passed to this method Splash Screen and info ********************** You can therefore show info about initialization in a splash screen using (taken from the Mock 0DViewer plugin): .. code-block:: self.emit_status(ThreadCommand('show_splash', 'Starting initialization')) QtCore.QThread.msleep(500) self.ini_detector_init(old_controller=controller, new_controller='Mock controller') self.emit_status(ThreadCommand('show_splash', 'generating Mock Data')) QtCore.QThread.msleep(500) self.set_Mock_data() self.emit_status(ThreadCommand('update_main_settings', [['wait_time'], self.settings['wait_time'], 'value'])) self.emit_status(ThreadCommand('show_splash', 'Displaying initial data')) QtCore.QThread.msleep(500) # initialize viewers with the future type of data self.dte_signal_temp.emit(DataToExport('Mock0D', data=[ DataFromPlugins(name='Mock1', data=[np.array([0])], dim='Data0D', labels=['Mock1', 'label2'])])) self.emit_status(ThreadCommand('close_splash')) Modifying the UI settings ************************* if you want to modify the settings tree of the UI (the *Main Settings* part or the hardware part, you can do so within the plugin directly) using: .. code-block:: self.emit_status(ThreadCommand('update_main_settings', [['wait_time'], 10, 'value'])) or .. code-block:: self.emit_status(ThreadCommand('update_settings', [['wait_time'], 10, 'value'])) The attribute of the ThreadCommand is a bit complex here ``[['wait_time'], 10, 'value']``. It is a list of three variables: * a list of string defining a path in the main_settings tree hierarchy * the new value, limit or option of the setting (here an integer) * a string specifying the type of modification, either: * *value*: the object should therefore be the new value of the modified parameter * *limits*: the object should be a sequence listing the limits of the parameter (depends on the type of parameter) * *options*: the object is a dictionary defining the options to modify * *childAdded*: the object is a dictionary generated using SaveState of a given Parameter .. _daq_move_commands: DAQ_Move specific commands ************************** Specifics commands for the :py:class:`DAQ_Move` are listed in: :py:meth:`thread_status` and explained a bit below * **ini_stage**: obtains info from the initialization. The attribute argument should be a dict containing a *initialized* key and associated boolean value and an *info* key and associated string value * **get_actuator_value**: update the UI current displayed value. The attribute argument is a DataActuator whose value and unit will be displayed on the UI. * **move_done**: update the UI current value and emits the move_done signal * **outofbounds**: emits the bounds_signal signal with a True argument * **set_allowed_values**: used to change the behaviour of the spinbox controlling absolute values, see :py:meth:`set_abs_spinbox_properties` * *stop*: stop the motion * *units*: update the UI units. The attribute argument should be a string specifying the new unit. You can directly modify the printed current actuator's value using the ``emit_value(DataActuator(data=12.4, units='m'))`` method which is a shortcut of ``emit_status(ThreadCommand('get_actuator_value', DataActuator(data=12.4, units='m')))``. In that case the printed value would show ``12.4 m``. You can also modify some SpinBox of the UI (the ones used to specify the absolute values) using the *set_allowed_values* command. In that case the attribute argument of the ThreadCommand should be a dictionary, see :py:meth:`set_abs_spinbox_properties`. DAQ_Viewer specific commands **************************** Specifics commands for the :py:class:`DAQ_Viewer` are listed in: :py:meth:`thread_status` and explained a bit below * ini_detector: update the status with "detector initialized" value and init state if attribute not null. * grab : emit grab_status(True) * grab_stopped: emit grab_status(False) * init_lcd: display a LCD panel. the attribute argument should be a dictionnary directly passed as named arguments to the LCD object * lcd: display on the LCD panel. the attribute argument should be a dict mapping the argument of the LCD setvalues method: * values: list of numpy arrays of shape (1,) * show_graph: boolean, optional tell if graph should be displayed * stop: stop the grab The interesting bit is the possibility to display a :py:class:`LCD widget` to display some numerical values (could be 0D Data also emitted using the ``dte_signal`` but could also be any values). You should first init the LCD screen using the command: ``init_lcd`` with an attribute being a dictionary with keys either: * digits: an integer specifying the number of digits to display, default is 3 * Nvals: the number of numerical values to be displayed * labels: the name/label of each value * show_graph: boolean, optional. set the graph visibility, default is True For instance, in the 0D Mock viewer plugin: .. code-block:: if not self.lcd_init: self.emit_status(ThreadCommand('init_lcd', dict(labels=['dat0', 'data1'], Nvals=2, digits=6))) QtWidgets.QApplication.processEvents() self.lcd_init = True self.emit_status(ThreadCommand('lcd', dict(values=data_tot, show_graph=False))) .. _plugin_roi_select: Reacting to the viewers: ROI select and crosshair ------------------------------------------------- A detector plugin can use what the user selects on the data viewers of its DAQ_Viewer (see :ref:`viewers_usage`): the *ROI select* and the *crosshair*. Each time the user moves them, the DAQ_Viewer calls one of these methods of :py:class:`DAQ_Viewer_base`, which do nothing by default and can be overridden in your plugin: .. code-block:: python def roi_select(self, roi_info: RoiInfo, ind_viewer: int = 0): ... def crosshair(self, crosshair_info: Iterable[float], ind_viewer: int = 0): ... * ``roi_select`` is called each time the user releases the ROI select of a Viewer1D or Viewer2D after moving or resizing it (the ROI select has to be shown, from the viewer toolbar or from the preferences); * ``crosshair`` is called each time the crosshair is moved, with its coordinates ``(x, y)`` (only ``x`` is meaningful for a Viewer1D); * ``ind_viewer`` is the index of the viewer: the DAQ_Viewer creates one viewer per ``DataFromPlugins`` of the emitted ``DataToExport``, in the same order; * positions and sizes are given in the units of the viewer axes, i.e. the axes of the emitted data (in pixels if your data has no axes, or plain index axes). Both methods are called in the thread of the plugin, like ``grab_data`` or ``commit_settings``: there is no need to protect the stored information with a lock. In a live acquisition loop, they are called when the loop processes the Qt events (``QtWidgets.QApplication.processEvents()``). RoiInfo ******* The ROI select is described by a :py:class:`RoiInfo` object: * ``origin``: the position of the corner of the selection, as a ``Point``. For a Viewer2D, the coordinates are in the numpy order of the image dimensions: ``(y, x)``, i.e. (rows, columns); * ``size``: the size of the selection, in the same order: ``(height, width)`` for a Viewer2D, ``(width,)`` for a Viewer1D; * ``centered``: ``False`` for the ROI select (``origin`` is the corner). ``center_origin()`` and ``uncenter_origin()`` move ``origin`` to the center of the selection and back; * ``roi_class``: the type of the selection (rectangle for a Viewer2D, linear region for a Viewer1D). Its ``to_slices`` method returns the slices corresponding to the selection, ready to be used on your data: one slice for a Viewer1D, two for a Viewer2D (rows, then columns). ``to_slices()`` returns integer bounds, ``to_slices(False)`` keeps float bounds. ``RoiInfo.from_slices`` does the opposite. As the selection is in the units of the axes, use the value-based slicer ``vsig`` of :ref:`DataWithAxes ` to crop your data: it converts the axes values into indexes, whatever the offset and scaling of your axes. The index-based slicer ``isig`` with ``to_slices()`` is only correct when the axes are plain indexes. .. code-block:: python dwa_cropped = dwa.vsig[roi_info.to_slices(False)] Example: cropping the emitted data ********************************** The plugin stores the last selection and crops the data before emitting them. A setting lets the user enable the cropping: .. code-block:: python from pymodaq_gui.plotting.items.roi import RoiInfo from pymodaq.utils.data import DataToExport class DAQ_2DViewer_MyCamera(DAQ_Viewer_base): params = comon_parameters + [ {'title': 'Crop to ROI select:', 'name': 'crop', 'type': 'bool', 'value': False}, ] def ini_attributes(self): self.controller = None self.roi_select_info: RoiInfo = None def roi_select(self, roi_info: RoiInfo, ind_viewer: int = 0): self.roi_select_info = roi_info def crop(self, dte: DataToExport) -> DataToExport: """Crop all the data of the DataToExport to the ROI select, if enabled and defined""" if not self.settings['crop'] or self.roi_select_info is None: return dte slices = self.roi_select_info.to_slices(False) return DataToExport(dte.name, data=[dwa.vsig[slices] for dwa in dte]) def grab_data(self, Naverage=1, **kwargs): dte = self.get_data_from_camera() # your own method building the DataToExport self.dte_signal.emit(self.crop(dte)) Make sure every emission goes through the cropping, in particular in a live loop (see :ref:`live_mode`). The *RoiStuff* plugin of the ``pymodaq_plugins_mockexamples`` package gives a complete example. Example: hardware ROI ********************* Many cameras can read only a part of their sensor, which increases the frame rate. In that case, ``roi_select`` can send the selection to the camera, then update the axes of the emitted data. If the axes of your data are the pixels of the sensor (with an offset and a scaling equal to the current hardware ROI start and binning), the integer slices of ``to_slices()`` directly give the pixels to read: .. code-block:: python def roi_select(self, roi_info: RoiInfo, ind_viewer: int = 0): rows, cols = roi_info.to_slices() self.controller.set_roi(x=cols.start, width=cols.stop - cols.start, y=rows.start, height=rows.stop - rows.start) # your camera API # then update the x and y axes of the emitted data (offset and size) The pylablib camera plugins (*camera_base_pylablib* in ``pymodaq_plugins_utils``) implement this pattern: the selection is copied into a *ROI* setting (*Update ROI from Viewer*), applied to the camera with *Apply ROI*, and the axes are recomputed from the camera ROI and binning. Example: crosshair ****************** .. code-block:: python def crosshair(self, crosshair_info: Iterable[float], ind_viewer: int = 0): x, y = crosshair_info # in the units of the viewer axes self.controller.point_to(x, y) # e.g. move a scanning mirror to the selected position