diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..79d185ed08 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,67 @@ +group: deprecated-2017Q4 + +language: python + +sudo: required + +services: + - docker + +python: + - "3.5" + +env: + global: + # Doctr deploy key for sardana-org/sardana-doc + - secure: "p/0UgVZzPKJQqcvQ/97qMgo9kPCE0cZ6vI+308YEJ2o9xj4a3FsfHCZ/vWtjdsrp1sQbtKVDesx+xmK4CLDzQeC2+Xskv8OZDjaG2jYkHcVosZEM3EGW8rLVKzoDWLr6cTy2wexLgjHPCsmrjukPs49/i5p+WU0no64YoLlZdp9TT+gvWSQJLIk6R4eqt4FHMszPybLv0pvb1SEiCzimlX1WM1pBrE0LHgchd2ZBYSUWTTwe+Koi4HCS4Bads8j20K2e3fFKcmR2u9DfmU+7Mf5HRJsj1LYJgBUF76lUG2/fZfpoDe8sWi+eUewTa3zNM4bhRLpV+pmG0ypplM4pIcdvwiHV03nGSGu6XK6OGQ/Mgsw0fmud4JR4f5g9DgEfERlyJKI4A9mPZQ327OmEwOOl33x2AFJAL05Qvm0yXCkf1dwgYXnZl44SQbAczY1NHFL90t6xbHtmTitJrE2Xb+4BLzMe3OOZj6j/0QeiXA4z1FnZr1s8UoAsm68iW194IuFg1RRG9FTISFWaBew5wzwvAJak0DxkpG0k43VkHiVC7sPHqr5CxXMXO/MuaptK2ti6iLK9xBAEUpO9HluOkeJq5WDIIxBiBS9tPi0i3vIpq87RjHkdw5n7pdIqnuJ1nXUjpWsuUyV3fLkY12fFxSbZgqmNhIE5/o9c5VP/69Y=" + + matrix: + # - TEST="flake8" + - TEST="testsuite" DOCKER_IMG=reszelaz/sardana-test + - TEST="doc" DOCKER_IMG=reszelaz/sardana-test + + +before_install: + # install flake8 to perform python code style check in the script part + # install it using pip in order to get the newest version + - if [ $TEST == "flake8" ]; then sudo apt-get update -qq ; fi + - if [ $TEST == "flake8" ]; then sudo apt-get install -qq python3-pip; fi + - if [ $TEST == "flake8" ]; then sudo pip3 install flake8; fi + +install: + # run reszelaz/sardana-test docker container (Debian8 with sardana-deps) + - if [ $TEST != "flake8" ]; then docker pull $DOCKER_IMG; fi + - if [ $TEST != "flake8" ]; then docker run -d --name=sardana-test -h sardana-test --volume=`pwd`:/sardana $DOCKER_IMG; fi + + # wait approx. 10 s (supervisor starts mysql and Tango DB) + - if [ $TEST == "testsuite" ]; then sleep 10; fi + + # install sardana in order to create the launcher scripts for servers + - if [ $TEST == "testsuite" ]; then docker exec sardana-test bash -c "cd /sardana && python3 setup.py install"; fi + + # start Pool and MacroServer necessary for macro tests + - if [ $TEST == "testsuite" ]; then docker exec sardana-test supervisorctl start Pool; fi + - if [ $TEST == "testsuite" ]; then docker exec sardana-test supervisorctl start MacroServer; fi + +script: + # make the script fail if a line fails + - set -e + # run flake8 check on all python files in the project + - if [ $TEST == "flake8" ]; then ci/flake8_diff.sh; fi + # run the full testsuite + - if [ $TEST == "testsuite" ]; then docker exec sardana-test xvfb-run -s '-screen 0 1920x1080x24' /bin/bash -c "pytest /usr/local/lib/python3.5/dist-packages/sardana-*.egg/sardana"; fi + # build docs + - if [ $TEST == "doc" ]; then docker exec -t sardana-test /bin/bash -c "cd /sardana ; sphinx-build -W doc/source/ build/sphinx/html" ; fi + - if [ $TEST == "doc" ]; then docker exec -t sardana-test /bin/bash -c "touch /sardana/build/sphinx/html/.nojekyll" ; fi + +deploy: + - provider: pages + local_dir: build/sphinx/html + repo: sardana-org/sardana-doc + skip_cleanup: true + github_token: $GITHUB_TOKEN # Set in the settings page of your repository, as a secure variable + keep_history: true + fqdn: sardana-controls.org # Set custom domain + on: + branch: develop + condition: "$TEST == doc" diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c271e292..f29e1099aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,43 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This file follows the formats and conventions from [keepachangelog.com] +## [Unreleased] + +### Added + +* *scan information* and *scan point* forms to the *showscan online* widget (#1386) +* `ScanPlotWidget`, `ScanPlotWindow`, `ScanInfoForm`, `ScanPointForm` and `ScanWindow` + widget classes for easier composition of custom GUIs involving online scan plotting (#1386) +* Include trigger/gate elements in the per-measurement preparation (#1432, #1443, #1468) + * Add `PrepareOne()` to TriggerGate controller. + * Call TriggerGate controller preparation methods in the _acquision action_ +* Add `ScanUser` environment variable (#1355) +* Allow to programmatically disable *deterministic scan* optimization (#1426, #1427) +* Initial delay in position domain to the synchronization description + in *ct* like continuous scans (#1428) +* Avoid double printing of user units in PMTV: read widget and units widget (#1424) +* Document how to properly deal with exceptions in macros in order to not interfer + with macro stopping/aborting (#1461) +* Documentation on how to start Tango servers on fixed IP - ORBendPoint (#1470) +* Documentation example on how to more efficiently access Tango with PyTango + in macros/controllers (#1456) + +### Fixed + +* Execute per measurement preparation in `mesh` scan macro (#1437) +* Continously read value references in hardware synchronized acquisition + instead of reading only at the end (#1442, #1448) +* Avoid problems when defining different, e.g. shape, standard attributes, + e.g. pseudo counter's value, in controllers (#1440, #1446) +* Problems with macro id's when `sequencer` executes from _plain text_ files (#1215, #1216) +* `sequencer` loading of plain text sequences in spock syntax with macro functions (#1422) +* Allow running Spock without Qt bindings (#1462, #1463) +* Recorders tests helpers (#1439) +* Disable flake8 job in travis CI (#1455) +* `createMacro()` and `prepareMacro()` docstring (#1460, #1444) +* Make write of MeasurementGroup (Taurus extension) integration time more robust (#1473) +* String formatting when rising exceptions in pseudomotors (#1469) + ## [3.0.3] 2020-09-18 ### Added diff --git a/doc/source/_static/showscan-online-infopanels.png b/doc/source/_static/showscan-online-infopanels.png new file mode 100644 index 0000000000..8fb460471c Binary files /dev/null and b/doc/source/_static/showscan-online-infopanels.png differ diff --git a/doc/source/devel/api/api_controller.rst b/doc/source/devel/api/api_controller.rst index f77e55bff9..be81875ddf 100644 --- a/doc/source/devel/api/api_controller.rst +++ b/doc/source/devel/api/api_controller.rst @@ -13,6 +13,7 @@ Controller API reference * :class:`ZeroDController` - 0D controller API * :class:`PseudoMotorController` - PseudoMotor controller API * :class:`PseudoCounterController` - PseudoCounter controller API + * :class:`TriggerGateController` - Trigger/Gate controller API * :class:`IORegisterController` - IORegister controller API .. _sardana-controller-data-type: diff --git a/doc/source/devel/api/api_macro.rst b/doc/source/devel/api/api_macro.rst index 3ac6de1219..810ad1bff1 100644 --- a/doc/source/devel/api/api_macro.rst +++ b/doc/source/devel/api/api_macro.rst @@ -42,3 +42,23 @@ imacro decorator :members: :undoc-members: +StopException +------------- + +.. autoclass:: StopException + :members: + :undoc-members: + +AbortException +-------------- + +.. autoclass:: AbortException + :members: + :undoc-members: + +InterruptException +------------------ + +.. autoclass:: InterruptException + :members: + :undoc-members: diff --git a/doc/source/devel/api/sardana/macroserver/scan.rst b/doc/source/devel/api/sardana/macroserver/scan.rst index 2a969b062e..0663819c11 100644 --- a/doc/source/devel/api/sardana/macroserver/scan.rst +++ b/doc/source/devel/api/sardana/macroserver/scan.rst @@ -26,8 +26,8 @@ GScan :show-inheritance: :members: -GScan ------ +Scan +---- .. inheritance-diagram:: SScan :parts: 1 diff --git a/doc/source/devel/api/sardana/pool/controller.rst b/doc/source/devel/api/sardana/pool/controller.rst index 4d0f9c79df..9377423f70 100644 --- a/doc/source/devel/api/sardana/pool/controller.rst +++ b/doc/source/devel/api/sardana/pool/controller.rst @@ -44,6 +44,7 @@ * :class:`OneDController` * :class:`TwoDController` * :class:`PseudoCounterController` + * :class:`TriggerGateController` * :class:`IORegisterController` @@ -216,6 +217,17 @@ Pseudo Counter Controller API :undoc-members: +Trigger/Gate Controller API +--------------------------- + +.. inheritance-diagram:: TriggerGateController + :parts: 1 + +.. autoclass:: TriggerGateController + :show-inheritance: + :members: + :undoc-members: + IO Register Controller API ---------------------------- diff --git a/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst b/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst index c1516526f8..0b89a2e1c1 100644 --- a/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst +++ b/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst @@ -89,6 +89,18 @@ The state should be a member of :obj:`~sardana.sardanadefs.State` (For backward compatibility reasons, it is also supported to return one of :class:`PyTango.DevState`). The status could be any string. +.. _sardana-TriggerGateController-howto-prepare: + +Prepare for measurement +~~~~~~~~~~~~~~~~~~~~~~~ + +To prepare a trigger for a measurement you can use the +:meth:`~sardana.pool.controller.TriggerGateController.PrepareOne` method which +receives as an argument the number of starts of the whole measurement. +This information may be used to prepare the hardware for generating +multiple events (triggers or gates) in a complex measurement +e.g. :ref:`sardana-macros-scanframework-determscan`. + .. _sardana-TriggerGateController-howto-load: Load synchronization description diff --git a/doc/source/devel/howto_macros/macros_general.rst b/doc/source/devel/howto_macros/macros_general.rst index f8e1cb08c7..07cda863ed 100644 --- a/doc/source/devel/howto_macros/macros_general.rst +++ b/doc/source/devel/howto_macros/macros_general.rst @@ -13,6 +13,10 @@ Writing macros This chapter provides the necessary information to write macros in sardana. The complete macro :term:`API` can be found :ref:`here `. +.. contents:: Table of contents + :depth: 3 + :backlinks: entry + What is a macro --------------- @@ -866,6 +870,55 @@ of user's interruption you must override the withing the :meth:`~sardana.macroserver.macro.Macro.on_stop` or :meth:`~sardana.macroserver.macro.Macro.on_abort`. +.. _sardana-macro-exception-handling: + +Handling exceptions +------------------- + +Please refer to the +`Python Errors and Exceptions `_ +documentation on how to deal with exceptions in your macro code. + +.. important:: + :ref:`sardana-macro-handling-macro-stop-and-abort` is internally implemented + using Python exceptions. So, your ``except`` clause can not simply catch any + exception type without re-raising it - this would ignore the macro stop/abort + request done in the ``try ... except`` block. If you still would like to + use the broad catching, you need to catch and raise the stop/abort exception + first: + + .. code-block:: python + :emphasize-lines: 7 + + import time + + from sardana.macroserver.macro import macro, StopException + + @macro() + def exception_macro(self): + self.output("Starting stoppable process") + try: + for i in range(10): + self.output("In iteration: {}".format(i)) + time.sleep(1) + except StopException: + raise + except Exception: + self.warning("Exception, but we continue") + self.output("After 'try ... except' block") + + If you do not program lines 12-13 and you stop your macro within + the ``try ... except`` block then the macro will continue and print the + output from line 16. + + You may choose to catch and re-raise: + `~sardana.macroserver.macro.StopException`, + `~sardana.macroserver.macro.AbortException` or + `~sardana.macroserver.macro.InterruptException`. The last one will + take care of stopping and aborting at the same time. + + + .. _sardana-macro-adding-hooks-support: Adding hooks support @@ -965,6 +1018,36 @@ simplified usage you should use Taurus. If you strive for very optimal access to Tango and don't need these benefits then most probably PyTango will work better for you. +.. hint:: + If you go for PyTango and wonder if creating a new `tango.DeviceProxy` + in frequent macro executions is inefficient from the I/O point of view you + should not worry about it cause Tango (more precisely CORBA) is taking + care about recycling the connection during a period of 120 s (default). + + If you still would like to optimize your code in order to avoid creation + of a new `tango.DeviceProxy` you may consider using the + `functools.lru_cache` as a singleton cache mechanism:: + + import functools + import tango + from sardana.macroserver.macro import macro + + Device = functools.lru_cache(maxsize=1024)(tango.DeviceProxy) + + @macro() + def switch_states(self): + """Switch TangoTest device state""" + proxy = Device('sys/tg_test/1') + proxy.SwitchStates() + + Here you don't need to worry about the opened connection to the + Tango device server in case you don't execute the macro for a while. + Again, Tango (more precisely CORBA) will take care about it. + See more details about the CORBA scavanger thread in: + `Tango client threading `_ + and `CORBA idle connection shutdown `_. + + .. _sardana-macro-using-external-libraries: Using external python libraries diff --git a/doc/source/devel/howto_macros/scan_framework.rst b/doc/source/devel/howto_macros/scan_framework.rst index 23a2a5db7d..43567fac30 100644 --- a/doc/source/devel/howto_macros/scan_framework.rst +++ b/doc/source/devel/howto_macros/scan_framework.rst @@ -195,6 +195,7 @@ the most basic features of a continuous scan:: :: (with more elaborated waypoint generator), see the code of :class:`~sardana.macroserver.macros.scan.meshc` +.. _sardana-macros-scanframework-determscan: Deterministic scans ------------------- diff --git a/doc/source/users/configuration/server.rst b/doc/source/users/configuration/server.rst index f7cf79fe0e..6859fd8f6f 100644 --- a/doc/source/users/configuration/server.rst +++ b/doc/source/users/configuration/server.rst @@ -7,25 +7,30 @@ Sardana system can :ref:`run as one or many Tango device servers`. Tango device servers listens on a TCP port for the CORBA requests. Usually it is fine to use the randomly assigned port (default behavior) but sometimes -it may be necessary to use a fixed port number. For example, when the server -needs to be accessed from another isolated network and we want to open -connections only for the given ports. +it may be necessary to use a fixed port number or even IP address. +For example, when the server needs to be accessed from another isolated +network and we want to open connections only for the given ports or IPs. -There are three possibilities to assign the port explicitly (the order -indicates the precedence): +There are three possibilities to assign the IP and/or port in format of the +ORBendPoint explicitly (the order indicates the precedence): + +.. note:: + The ORBendPoint is in the following format: ``giop:tcp::`` + and both IP and port are optional, so you could only fix the IP, + only fix the port, fix both of them or none of them. - using OS environment variable ``ORBendPoint`` e.g. .. code-block:: bash - $ export ORBendPoint=28366 - $ Pool demo1 -ORBendPoint 28366 + $ export ORBendPoint=giop:tcp:192.168.0.100:28366 + $ Pool demo1 - using Tango device server command line argument ``-ORBendPoint`` .. code-block:: bash - $ Pool demo1 -ORBendPoint 28366 + $ Pool demo1 -ORBendPoint giop:tcp:192.168.0.100:28366 - using Tango DB free property with object name: ``ORBendPoint`` and property name: ``/``) @@ -34,7 +39,7 @@ indicates the precedence): import tango db = tango.Database() - db.put_property("ORBendPoint", {"Pool/demo1": 28366}) + db.put_property("ORBendPoint", {"Pool/demo1": "giop:tcp:192.168.0.100:28366"}) .. note:: diff --git a/doc/source/users/environment_variable_catalog.rst b/doc/source/users/environment_variable_catalog.rst index e66214594d..80432ce2a3 100644 --- a/doc/source/users/environment_variable_catalog.rst +++ b/doc/source/users/environment_variable_catalog.rst @@ -308,7 +308,7 @@ ScanRecorder Its value may be either of type string or of list of strings. If ScanRecorder variable is defined, it explicitly indicates which recorder -class should be used and for which file defined by ScanFile (based on the +class should be used and for which file defined by ScanFile (based on the order). Example 1: @@ -381,6 +381,17 @@ For example:: 'min': 2.9802322387695312e-08, 'minpos': 0.0}}} +.. _scanuser: + +ScanUser +~~~~~~~~ +*Not mandatory, set by user* + +Its value is of type string. Its value is delivered to the recorders which +may use it, for example, as a user contact information. If not set, the OS +user executing the Sardana server (which executes the scan) will be passed to +the recorders instead. + .. _sharedmemory: SharedMemory diff --git a/doc/source/users/taurus/showscan.rst b/doc/source/users/taurus/showscan.rst index df040ed9e4..e2f208ae70 100644 --- a/doc/source/users/taurus/showscan.rst +++ b/doc/source/users/taurus/showscan.rst @@ -48,6 +48,15 @@ plot per curve or group curves by the selected x axis Showscan online plotting three physical counters against the motor's position on separate plots. +Finally, the *scan point* and the *scan information* panels are available +and offer online updates on the channel values of the current scan point +and some general scan information e.g. scan file, start and end time, etc. +respectively. + +.. figure:: /_static/showscan-online-infopanels.png + + Showscan online plotting with separate plots and information panels. + ---------------- Showscan offline ---------------- diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py index 728ef8a85a..afb91c0d3a 100644 --- a/src/sardana/macroserver/macro.py +++ b/src/sardana/macroserver/macro.py @@ -33,7 +33,8 @@ __all__ = ["OverloadPrint", "PauseEvent", "Hookable", "ExecMacroHook", "MacroFinder", "Macro", "macro", "iMacro", "imacro", "MacroFunc", "Type", "Table", "List", "ViewOption", - "LibraryError", "Optional"] + "LibraryError", "Optional", "StopException", "AbortException", + "InterruptException"] __docformat__ = 'restructuredtext' @@ -60,7 +61,7 @@ from sardana.macroserver.msparameter import Type, ParamType, Optional from sardana.macroserver.msexception import StopException, AbortException, \ ReleaseException, MacroWrongParameterType, UnknownEnv, UnknownMacro, \ - LibraryError + LibraryError, InterruptException from sardana.macroserver.msoptions import ViewOption from sardana.taurus.core.tango.sardana.pool import PoolElement @@ -387,13 +388,24 @@ def my_macro1(self): def where_moveable(self, moveable): self.output("Moveable %s is at %s", moveable.getName(), moveable.getPosition())""" + param_def = [] + result_def = [] + env = () + hints = {} + interactive = False + def __init__(self, param_def=None, result_def=None, env=None, hints=None, interactive=None): - self.param_def = param_def - self.result_def = result_def - self.env = env - self.hints = hints - self.interactive = interactive + if param_def is not None: + self.param_def = param_def + if result_def is not None: + self.result_def = result_def + if env is not None: + self.env = env + if hints is not None: + self.hints = hints + if interactive is not None: + self.interactive = interactive def __call__(self, fn): fn.macro_data = {} @@ -526,6 +538,8 @@ def __init__(self, *args, **kwargs): self._id = kwargs.get('id') self._desc = "Macro '%s'" % self._macro_line self._macro_status = {'id': self._id, + 'name': self._name, + 'macro_line': self._macro_line, 'range': (0.0, 100.0), 'state': 'start', 'step': 0.0} @@ -1121,35 +1135,35 @@ def createMacro(self, *pars): Several different parameter formats are supported:: # several parameters: - self.execMacro('ascan', 'th', '0', '100', '10', '1.0') - self.execMacro('mv', [[motor.getName(), '0']]) - self.execMacro('mv', motor.getName(), '0') # backwards compatibility - see note - self.execMacro('ascan', 'th', 0, 100, 10, 1.0) - self.execMacro('mv', [[motor.getName(), 0]]) - self.execMacro('mv', motor.getName(), 0) # backwards compatibility - see note + self.createMacro('ascan', 'th', '0', '100', '10', '1.0') + self.createMacro('mv', [[motor.getName(), '0']]) + self.createMacro('mv', motor.getName(), '0') # backwards compatibility - see note + self.createMacro('ascan', 'th', 0, 100, 10, 1.0) + self.createMacro('mv', [[motor.getName(), 0]]) + self.createMacro('mv', motor.getName(), 0) # backwards compatibility - see note th = self.getObj('th') - self.execMacro('ascan', th, 0, 100, 10, 1.0) - self.execMacro('mv', [[th, 0]]) - self.execMacro('mv', th, 0) # backwards compatibility - see note + self.createMacro('ascan', th, 0, 100, 10, 1.0) + self.createMacro('mv', [[th, 0]]) + self.createMacro('mv', th, 0) # backwards compatibility - see note # a sequence of parameters: - self.execMacro(['ascan', 'th', '0', '100', '10', '1.0') - self.execMacro(['mv', [[motor.getName(), '0']]]) - self.execMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note - self.execMacro(('ascan', 'th', 0, 100, 10, 1.0)) - self.execMacro(['mv', [[motor.getName(), 0]]]) - self.execMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note + self.createMacro(['ascan', 'th', '0', '100', '10', '1.0']) + self.createMacro(['mv', [[motor.getName(), '0']]]) + self.createMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note + self.createMacro(('ascan', 'th', 0, 100, 10, 1.0)) + self.createMacro(['mv', [[motor.getName(), 0]]]) + self.createMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note th = self.getObj('th') - self.execMacro(['ascan', th, 0, 100, 10, 1.0]) - self.execMacro(['mv', [[th, 0]]]) - self.execMacro(['mv', th, 0]) # backwards compatibility - see note + self.createMacro(['ascan', th, 0, 100, 10, 1.0]) + self.createMacro(['mv', [[th, 0]]]) + self.createMacro(['mv', th, 0]) # backwards compatibility - see note # a space separated string of parameters (this is not compatible # with multiple or nested repeat parameters, furthermore the repeat # parameter must be the last one): - self.execMacro('ascan th 0 100 10 1.0') - self.execMacro('mv %s 0' % motor.getName()) + self.createMacro('ascan th 0 100 10 1.0') + self.createMacro('mv %s 0' % motor.getName()) .. note:: From Sardana 2.0 the repeat parameter values must be passed as lists of items. An item of a repeat parameter containing more @@ -1192,34 +1206,34 @@ def prepareMacro(self, *args, **kwargs): Several different parameter formats are supported:: # several parameters: - self.execMacro('ascan', 'th', '0', '100', '10', '1.0') - self.execMacro('mv', [[motor.getName(), '0']]) - self.execMacro('mv', motor.getName(), '0') # backwards compatibility - see note - self.execMacro('ascan', 'th', 0, 100, 10, 1.0) - self.execMacro('mv', [[motor.getName(), 0]]) - self.execMacro('mv', motor.getName(), 0) # backwards compatibility - see note + self.prepareMacro('ascan', 'th', '0', '100', '10', '1.0') + self.prepareMacro('mv', [[motor.getName(), '0']]) + self.prepareMacro('mv', motor.getName(), '0') # backwards compatibility - see note + self.prepareMacro('ascan', 'th', 0, 100, 10, 1.0) + self.prepareMacro('mv', [[motor.getName(), 0]]) + self.prepareMacro('mv', motor.getName(), 0) # backwards compatibility - see note th = self.getObj('th') - self.execMacro('ascan', th, 0, 100, 10, 1.0) - self.execMacro('mv', [[th, 0]]) - self.execMacro('mv', th, 0) # backwards compatibility - see note + self.prepareMacro('ascan', th, 0, 100, 10, 1.0) + self.prepareMacro('mv', [[th, 0]]) + self.prepareMacro('mv', th, 0) # backwards compatibility - see note # a sequence of parameters: - self.execMacro(['ascan', 'th', '0', '100', '10', '1.0']) - self.execMacro(['mv', [[motor.getName(), '0']]]) - self.execMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note - self.execMacro(('ascan', 'th', 0, 100, 10, 1.0)) - self.execMacro(['mv', [[motor.getName(), 0]]]) - self.execMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note + self.prepareMacro(['ascan', 'th', '0', '100', '10', '1.0']) + self.prepareMacro(['mv', [[motor.getName(), '0']]]) + self.prepareMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note + self.prepareMacro(('ascan', 'th', 0, 100, 10, 1.0)) + self.prepareMacro(['mv', [[motor.getName(), 0]]]) + self.prepareMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note th = self.getObj('th') - self.execMacro(['ascan', th, 0, 100, 10, 1.0]) - self.execMacro(['mv', [[th, 0]]]) - self.execMacro(['mv', th, 0]) # backwards compatibility - see note + self.prepareMacro(['ascan', th, 0, 100, 10, 1.0]) + self.prepareMacro(['mv', [[th, 0]]]) + self.prepareMacro(['mv', th, 0]) # backwards compatibility - see note # a space separated string of parameters (this is not compatible # with multiple or nested repeat parameters, furthermore the repeat # parameter must be the last one): - self.execMacro('ascan th 0 100 10 1.0') - self.execMacro('mv %s 0' % motor.getName()) + self.prepareMacro('ascan th 0 100 10 1.0') + self.prepareMacro('mv %s 0' % motor.getName()) .. note:: From Sardana 2.0 the repeat parameter values must be passed as lists of items. An item of a repeat parameter containing more @@ -2322,6 +2336,12 @@ def exec_(self): # make sure a 0.0 progress is sent yield macro_status + # Avoid repeating same information on subsequent events. If, in the + # future, clients that connect in the middle of macro execution need + # this information, just simply remove the lines below + del macro_status['name'] + del macro_status['macro_line'] + # allow any macro to be paused at the beginning of its execution self.pausePoint() diff --git a/src/sardana/macroserver/macros/scan.py b/src/sardana/macroserver/macros/scan.py index 57e9aa22fa..0f48477711 100644 --- a/src/sardana/macroserver/macros/scan.py +++ b/src/sardana/macroserver/macros/scan.py @@ -709,6 +709,7 @@ def prepare(self, m1, m1_start_pos, m1_final_pos, m1_nr_interv, self.starts = numpy.array([m1_start_pos, m2_start_pos], dtype='d') self.finals = numpy.array([m1_final_pos, m2_final_pos], dtype='d') self.nr_intervs = numpy.array([m1_nr_interv, m2_nr_interv], dtype='i') + self.nb_points = (m1_nr_interv + 1) * (m2_nr_interv + 1) self.integ_time = integ_time self.bidirectional_mode = bidirectional diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py index eacd779395..f0662ad2d1 100644 --- a/src/sardana/macroserver/scan/gscan.py +++ b/src/sardana/macroserver/scan/gscan.py @@ -639,11 +639,16 @@ def _setupEnvironment(self, additional_env): serialno = 1 self.macro.setEnv("ScanID", serialno) + try: + user = self.macro.getEnv("ScanUser") + except UnknownEnv: + user = USER_NAME + env = ScanDataEnvironment( {'serialno': serialno, # TODO: this should be got from # self.measurement_group.getChannelsInfo() - 'user': USER_NAME, + 'user': user, 'title': self.macro.getCommand()}) # Initialize the data_desc list (and add the point number column) @@ -1058,24 +1063,52 @@ def do_restore(self): class SScan(GScan): """Step scan""" + def __init__(self, macro, generator=None, moveables=[], env={}, + constraints=[], extrainfodesc=[]): + GScan.__init__(self, macro, generator=generator, moveables=moveables, + env=env, constraints=constraints, + extrainfodesc=extrainfodesc) + self._deterministic_scan = None + + @property + def deterministic_scan(self): + """Check if the scan is a deterministic scan. + + Scan is considered as deterministic scan if + the `~sardana.macroserver.macro.Macro` specialization owning + the scan object contains ``nb_points`` and ``integ_time`` attributes. + + Scan flow depends on this property (some optimizations are applied). + These can be disabled by setting this property to `False`. + """ + if self._deterministic_scan is None: + macro = self.macro + if hasattr(macro, "nb_points") and hasattr(macro, "integ_time"): + self._deterministic_scan = True + else: + self._deterministic_scan = False + return self._deterministic_scan + + @deterministic_scan.setter + def deterministic_scan(self, value): + self._deterministic_scan = value + def scan_loop(self): lstep = None macro = self.macro scream = False - self._deterministic_scan = False if hasattr(macro, "nb_points"): nb_points = float(macro.nb_points) - if hasattr(macro, "integ_time"): - integ_time = macro.integ_time - self.measurement_group.putIntegrationTime(integ_time) - self.measurement_group.setNbStarts(nb_points) - self.measurement_group.prepare() - self._deterministic_scan = True scream = True else: yield 0.0 + if self.deterministic_scan: + self.measurement_group.putIntegrationTime(macro.integ_time) + self.measurement_group.setNbStarts(macro.nb_points) + self.measurement_group.prepare() + self._sum_motion_time = 0 self._sum_acq_time = 0 @@ -1157,7 +1190,7 @@ def stepUp(self, n, step, lstep): # Acquire data self.debug("[START] acquisition") try: - if self._deterministic_scan: + if self.deterministic_scan: state, data_line = mg.count_raw() else: state, data_line = mg.count(integ_time) @@ -2366,8 +2399,10 @@ def _go_through_waypoints(self): initial_position = start total_time = abs(total_position) / path.max_vel delay_time = path.max_vel_time + delay_position = start - path.initial_user_pos synch = [ - {SynchParam.Delay: {SynchDomain.Time: delay_time}, + {SynchParam.Delay: {SynchDomain.Time: delay_time, + SynchDomain.Position: delay_position}, SynchParam.Initial: {SynchDomain.Position: initial_position}, SynchParam.Active: {SynchDomain.Position: active_position, SynchDomain.Time: active_time}, diff --git a/src/sardana/macroserver/scan/test/helper.py b/src/sardana/macroserver/scan/test/helper.py index d3bdefb8f7..b4fa2007bc 100644 --- a/src/sardana/macroserver/scan/test/helper.py +++ b/src/sardana/macroserver/scan/test/helper.py @@ -12,7 +12,7 @@ import time -from datetime import date +import datetime import threading import numpy import os @@ -47,7 +47,7 @@ def run(self): if skip: continue time.sleep(t) - _dict = dict(data=v, index=idx, label=self.name) + _dict = dict(value=v, index=idx, label=self.name) self.scan_data.addData(_dict) def get_obj(self): @@ -69,7 +69,7 @@ def createScanDataEnvironment(columns, scanDir='/tmp/', env['ScanFile'] = scanFile env['total_scan_intervals'] = -1.0 - today = date.today() + today = datetime.datetime.fromtimestamp(time.time()) env['datetime'] = today env['starttime'] = today env['endtime'] = today diff --git a/src/sardana/macroserver/test/res/macros/testmacros.py b/src/sardana/macroserver/test/res/macros/testmacros.py index 638315865b..450db21879 100644 --- a/src/sardana/macroserver/test/res/macros/testmacros.py +++ b/src/sardana/macroserver/test/res/macros/testmacros.py @@ -88,7 +88,6 @@ def run(self, *args): params = (99, 1., 2.) expected_params = (99, [1., 2.]) - self.runMacro(macro) macro, pars = self.createMacro('pt6_base', *params) self.runMacro(macro) result = macro.data diff --git a/src/sardana/pool/controller.py b/src/sardana/pool/controller.py index fc24d05030..3574399f03 100644 --- a/src/sardana/pool/controller.py +++ b/src/sardana/pool/controller.py @@ -912,6 +912,19 @@ class TriggerGateController(Controller, Synchronizer, Stopable, Startable): def __init__(self, inst, props, *args, **kwargs): Controller.__init__(self, inst, props, *args, **kwargs) + # TODO: Implement a Preparable interface and move this method + # and the Loadable.PrepareOne() there. + def PrepareOne(self, axis, nb_starts): + """**Controller API**. Override if necessary. + Called to prepare the trigger/gate axis with the measurement + parameters. + Default implementation does nothing. + + :param int axis: axis + :param int nb_starts: number of starts + """ + pass + class ZeroDController(Controller, Readable, Stopable): """Base class for a 0D controller. Inherit from this class to diff --git a/src/sardana/pool/poolacquisition.py b/src/sardana/pool/poolacquisition.py index 224fb6ce1f..457b78fc97 100644 --- a/src/sardana/pool/poolacquisition.py +++ b/src/sardana/pool/poolacquisition.py @@ -499,6 +499,9 @@ def prepare(self, config, acq_mode, value, synch_description=None, config.changed = False + # Call synchronizer controllers prepare method + self._prepare_synch_ctrls(ctrls_synch, nb_starts) + # Call hardware and software start controllers prepare method ctrls = ctrls_hw + ctrls_sw_start self._prepare_ctrls(ctrls, value, repetitions, latency, @@ -510,6 +513,7 @@ def prepare(self, config, acq_mode, value, synch_description=None, self._prepare_ctrls(ctrls_sw, value, repetitions, latency, nb_starts) + @staticmethod def _prepare_ctrls(ctrls, value, repetitions, latency, nb_starts): for ctrl in ctrls: @@ -518,6 +522,14 @@ def _prepare_ctrls(ctrls, value, repetitions, latency, nb_starts): pool_ctrl.ctrl.PrepareOne(axis, value, repetitions, latency, nb_starts) + @staticmethod + def _prepare_synch_ctrls(ctrls, nb_starts): + for ctrl in ctrls: + for chn in ctrl.get_channels(): + axis = chn.axis + pool_ctrl = ctrl.element + pool_ctrl.ctrl.PrepareOne(axis, nb_starts) + def is_running(self): """Checks if acquisition is running. @@ -812,6 +824,30 @@ def _raw_read_ctrl_value_ref(self, ret, pool_ctrl): finally: self._value_info.finish_one() + def _process_value_buffer(self, acquirable, value, final=False): + final_str = "final " if final else "" + if is_value_error(value): + self.error("Loop %sread value error for %s" % (final_str, + acquirable.name)) + msg = "Details: " + "".join( + traceback.format_exception(*value.exc_info)) + self.debug(msg) + acquirable.put_value(value, propagate=2) + else: + acquirable.extend_value_buffer(value, propagate=2) + + def _process_value_ref_buffer(self, acquirable, value_ref, final=False): + final_str = "final " if final else "" + if is_value_error(value_ref): + self.error("Loop read ref %svalue error for %s" % + (final_str, acquirable.name)) + msg = "Details: " + "".join( + traceback.format_exception(*value_ref.exc_info)) + self.debug(msg) + acquirable.put_value_ref(value_ref, propagate=2) + else: + acquirable.extend_value_ref_buffer(value_ref, propagate=2) + def in_acquisition(self, states): """Determines if we are in acquisition or if the acquisition has ended based on the current unit trigger modes and states returned by the @@ -1056,15 +1092,10 @@ def action_loop(self): if not i % nb_states_per_value: self.read_value(ret=values) for acquirable, value in list(values.items()): - if is_value_error(value): - self.error("Loop read value error for %s" % - acquirable.name) - msg = "Details: " + "".join( - traceback.format_exception(*value.exc_info)) - self.debug(msg) - acquirable.put_value(value) - else: - acquirable.extend_value_buffer(value) + self._process_value_buffer(acquirable, value) + self.read_value_ref(ret=value_refs) + for acquirable, value_ref in list(value_refs.items()): + self._process_value_ref_buffer(acquirable, value_ref) time.sleep(nap) i += 1 @@ -1076,24 +1107,11 @@ def action_loop(self): for acquirable, state_info in list(states.items()): if acquirable in values: value = values[acquirable] - if is_value_error(value): - self.error("Loop final read value error for: %s" % - acquirable.name) - msg = "Details: " + "".join( - traceback.format_exception(*value.exc_info)) - self.debug(msg) - acquirable.put_value(value) - else: - acquirable.extend_value_buffer(value, propagate=2) + self._process_value_buffer(acquirable, value, final=True) if acquirable in value_refs: value_ref = value_refs[acquirable] - if is_value_error(value_ref): - self.error("Loop final read value ref error for: %s" % - acquirable.name) - msg = "Details: " + "".join( - traceback.format_exception(*value_ref.exc_info)) - self.debug(msg) - acquirable.extend_value_ref_buffer(value_ref, propagate=2) + self._process_value_ref_buffer(acquirable, value_ref, + final=True) state_info = acquirable._from_ctrl_state_info(state_info) set_state_info = functools.partial(acquirable.set_state_info, state_info, diff --git a/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py b/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py index f2795057b6..bd746ed8ac 100644 --- a/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py +++ b/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py @@ -72,6 +72,9 @@ def StateOne(self, axis): print(e) return sta, status + def PrepareOne(self, axis, nb_starts): + self._log.debug('PrepareOne(%d): entering...' % axis) + def PreStartAll(self): pass diff --git a/src/sardana/pool/poolpseudomotor.py b/src/sardana/pool/poolpseudomotor.py index 7819b4cdea..e63fb214ab 100644 --- a/src/sardana/pool/poolpseudomotor.py +++ b/src/sardana/pool/poolpseudomotor.py @@ -135,7 +135,7 @@ def get_physical_write_positions(self): # because of a cold start pos_attr.update(propagate=0) if pos_attr.in_error(): - raise PoolException("Cannot get '%' position" % pos_attr.obj.name, + raise PoolException("Cannot get '%s' position" % pos_attr.obj.name, exc_info=pos_attr.exc_info) value = pos_attr.value ret.append(value) @@ -149,7 +149,7 @@ def get_physical_positions(self): if not pos_attr.has_value(): pos_attr.update(propagate=0) if pos_attr.in_error(): - raise PoolException("Cannot get '%' position" % pos_attr.obj.name, + raise PoolException("Cannot get '%s' position" % pos_attr.obj.name, exc_info=pos_attr.exc_info) ret.append(pos_attr.value) return ret diff --git a/src/sardana/pool/test/test_acquisition.py b/src/sardana/pool/test/test_acquisition.py index cf0c1139e8..7b02dd8370 100644 --- a/src/sardana/pool/test/test_acquisition.py +++ b/src/sardana/pool/test/test_acquisition.py @@ -27,7 +27,7 @@ import numpy -from unittest import TestCase +from unittest import TestCase, mock from taurus.test import insertTest from sardana.sardanautils import is_number, is_pure_str @@ -567,6 +567,14 @@ def setUp(self): self.data_listener = AttributeListener(dtype=object, attr_name="valuebuffer") + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "ReadOne", + wraps=ctrl.ReadOne) as mock_ReadOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_ReadOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -592,6 +600,14 @@ def _prepare(self, integ_time, repetitions, latency_time, nb_starts): axis = self.channel.axis self.channel_ctrl.set_axis_par(axis, "value_ref_enabled", True) + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "RefOne", + wraps=ctrl.RefOne) as mock_RefOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_RefOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -610,6 +626,14 @@ def setUp(self): self.data_listener = AttributeListener(dtype=object, attr_name="valuebuffer") + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "ReadOne", + wraps=ctrl.ReadOne) as mock_ReadOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_ReadOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -628,6 +652,14 @@ def setUp(self): self.data_listener = AttributeListener(dtype=object, attr_name="valuebuffer") + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "ReadOne", + wraps=ctrl.ReadOne) as mock_ReadOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_ReadOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -652,6 +684,14 @@ def _prepare(self, integ_time, repetitions, latency_time, nb_starts): axis = self.channel.axis self.channel_ctrl.set_axis_par(axis, "value_ref_enabled", True) + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "RefOne", + wraps=ctrl.RefOne) as mock_RefOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_RefOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) diff --git a/src/sardana/spock/inputhandler.py b/src/sardana/spock/inputhandler.py index 7b9b6f93c3..73add6ef0c 100644 --- a/src/sardana/spock/inputhandler.py +++ b/src/sardana/spock/inputhandler.py @@ -25,18 +25,10 @@ """Spock submodule. It contains an input handler""" -__all__ = ['SpockInputHandler', 'InputHandler'] +__all__ = ['SpockInputHandler'] __docformat__ = 'restructuredtext' -import sys -from multiprocessing import Process, Pipe - -from taurus.core import TaurusManager -from taurus.core.util.singleton import Singleton -from taurus.external.qt import Qt, compat -from taurus.qt.qtgui.dialog import TaurusMessageBox, TaurusInputDialog - from sardana.taurus.core.tango.sardana.macroserver import BaseInputHandler from sardana.spock import genutils @@ -67,103 +59,3 @@ def input(self, input_data=None): def input_timeout(self, input_data): print("SpockInputHandler input timeout") - - -class MessageHandler(Qt.QObject): - - messageArrived = Qt.pyqtSignal(compat.PY_OBJECT) - - def __init__(self, conn, parent=None): - Qt.QObject.__init__(self, parent) - self._conn = conn - self._dialog = None - self.messageArrived.connect(self.on_message) - - def handle_message(self, input_data): - self.messageArrived.emit(input_data) - - def on_message(self, input_data): - msg_type = input_data['type'] - if msg_type == 'input': - if 'macro_name' in input_data and 'title' not in input_data: - input_data['title'] = input_data['macro_name'] - self._dialog = dialog = TaurusInputDialog(input_data=input_data) - dialog.activateWindow() - dialog.exec_() - ok = dialog.result() - value = dialog.value() - ret = dict(input=None, cancel=False) - if ok: - ret['input'] = value - else: - ret['cancel'] = True - self._conn.send(ret) - elif msg_type == 'timeout': - dialog = self._dialog - if dialog: - dialog.close() - - -class InputHandler(Singleton, BaseInputHandler): - - def __init__(self): - # don't call super __init__ on purpose - pass - - def init(self, *args, **kwargs): - self._conn, child_conn = Pipe() - self._proc = proc = Process(target=self.safe_run, - name="SpockInputHandler", args=(child_conn,)) - proc.daemon = True - proc.start() - - def input(self, input_data=None): - # parent process - data_type = input_data.get('data_type', 'String') - if isinstance(data_type, str): - ms = genutils.get_macro_server() - interfaces = ms.getInterfaces() - if data_type in interfaces: - input_data['data_type'] = [ - elem.name for elem in list(interfaces[data_type].values())] - self._conn.send(input_data) - ret = self._conn.recv() - return ret - - def input_timeout(self, input_data): - # parent process - self._conn.send(input_data) - - def safe_run(self, conn): - # child process - try: - return self.run(conn) - except Exception as e: - msgbox = TaurusMessageBox(*sys.exc_info()) - conn.send((e, False)) - msgbox.exec_() - - def run(self, conn): - # child process - self._conn = conn - app = Qt.QApplication.instance() - if app is None: - app = Qt.QApplication(['spock']) - app.setQuitOnLastWindowClosed(False) - self._msg_handler = MessageHandler(conn) - TaurusManager().addJob(self.run_forever, None) - app.exec_() - conn.close() - print("Quit input handler") - - def run_forever(self): - # child process - message, conn = True, self._conn - while message: - message = conn.recv() - if not message: - continue - self._msg_handler.handle_message(message) - app = Qt.QApplication.instance() - if app: - app.quit() diff --git a/src/sardana/spock/ipython_01_00/genutils.py b/src/sardana/spock/ipython_01_00/genutils.py index 1c26d28af8..c92ca164df 100644 --- a/src/sardana/spock/ipython_01_00/genutils.py +++ b/src/sardana/spock/ipython_01_00/genutils.py @@ -82,7 +82,10 @@ from taurus.core.util.codecs import CodecFactory # make sure Qt is properly initialized -from taurus.external.qt import Qt +try: + from taurus.external.qt import Qt +except ImportError: + pass from sardana.spock import exception from sardana.spock import colors @@ -110,7 +113,11 @@ def get_gui_mode(): - return 'qt' + try: + import taurus.external.qt.Qt + return 'qt' + except ImportError: + return None def get_pylab_mode(): @@ -1159,7 +1166,8 @@ def out_prompt_tokens(self): term_app = config.TerminalIPythonApp term_app.display_banner = True term_app.gui = gui_mode - term_app.pylab = 'qt' + if gui_mode == 'qt': + term_app.pylab = 'qt' term_app.pylab_import_all = False #term_app.nosep = False #term_app.classic = True @@ -1280,8 +1288,16 @@ def mainloop(app=None, user_ns=None): def prepare_input_handler(): # initialize input handler as soon as possible - import sardana.spock.inputhandler - _ = sardana.spock.inputhandler.InputHandler() + + from sardana import sardanacustomsettings + + if getattr(sardanacustomsettings, "SPOCK_INPUT_HANDLER", "CLI") == "Qt": + + try: + import sardana.spock.qtinputhandler + _ = sardana.spock.qtinputhandler.InputHandler() + except ImportError: + raise Exception("Cannot use Spock Qt input handler!") def prepare_cmdline(argv=None): diff --git a/src/sardana/spock/magic.py b/src/sardana/spock/magic.py index 0f4b1c6126..b6e2ea1f30 100644 --- a/src/sardana/spock/magic.py +++ b/src/sardana/spock/magic.py @@ -40,6 +40,15 @@ def expconf(self, parameter_s=''): """Launches a GUI for configuring the environment variables for the experiments (scans)""" + + try: + from taurus.external.qt import Qt + except ImportError: + print("Qt binding is not available. ExpConf cannot work without it." + "(hint: maybe you want to use experiment configuration macros? " + "https://sardana-controls.org/users/standard_macro_catalog.html#experiment-configuration-macros)") + return + try: from sardana.taurus.qt.qtgui.extra_sardana import ExpDescriptionEditor except: @@ -81,6 +90,13 @@ def showscan(self, parameter_s=''): Where *online* means plot the scan as it runs and *offline* means - extract the scan data from the file - works only with HDF5 files. """ + + try: + from taurus.external.qt import Qt + except ImportError: + print("Qt binding is not available. Showscan cannot work without it.") + return + params = parameter_s.split() door = get_door() scan_nb = None @@ -121,6 +137,12 @@ def showscan(self, parameter_s=''): def spsplot(self, parameter_s=''): + try: + from taurus.external.qt import Qt + except ImportError: + print("Qt binding is not available. SPSplot cannot work without it.") + return + get_door().plot() diff --git a/src/sardana/spock/qtinputhandler.py b/src/sardana/spock/qtinputhandler.py new file mode 100644 index 0000000000..c1dea5d8eb --- /dev/null +++ b/src/sardana/spock/qtinputhandler.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +############################################################################## +## +# This file is part of Sardana +## +# http://www.sardana-controls.org/ +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Sardana is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Sardana is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Sardana. If not, see . +## +############################################################################## + +"""Spock submodule. It contains an input handler""" + +__all__ = ['InputHandler'] + +__docformat__ = 'restructuredtext' + +import sys +from multiprocessing import Process, Pipe + +from taurus.core import TaurusManager +from taurus.core.util.singleton import Singleton +from taurus.external.qt import Qt, compat +from taurus.qt.qtgui.dialog import TaurusMessageBox, TaurusInputDialog + +from sardana.taurus.core.tango.sardana.macroserver import BaseInputHandler + +from sardana.spock import genutils + + +class MessageHandler(Qt.QObject): + + messageArrived = Qt.pyqtSignal(compat.PY_OBJECT) + + def __init__(self, conn, parent=None): + Qt.QObject.__init__(self, parent) + self._conn = conn + self._dialog = None + self.messageArrived.connect(self.on_message) + + def handle_message(self, input_data): + self.messageArrived.emit(input_data) + + def on_message(self, input_data): + msg_type = input_data['type'] + if msg_type == 'input': + if 'macro_name' in input_data and 'title' not in input_data: + input_data['title'] = input_data['macro_name'] + self._dialog = dialog = TaurusInputDialog(input_data=input_data) + dialog.activateWindow() + dialog.exec_() + ok = dialog.result() + value = dialog.value() + ret = dict(input=None, cancel=False) + if ok: + ret['input'] = value + else: + ret['cancel'] = True + self._conn.send(ret) + elif msg_type == 'timeout': + dialog = self._dialog + if dialog: + dialog.close() + + +class InputHandler(Singleton, BaseInputHandler): + + def __init__(self): + # don't call super __init__ on purpose + pass + + def init(self, *args, **kwargs): + self._conn, child_conn = Pipe() + self._proc = proc = Process(target=self.safe_run, + name="SpockInputHandler", args=(child_conn,)) + proc.daemon = True + proc.start() + + def input(self, input_data=None): + # parent process + data_type = input_data.get('data_type', 'String') + if isinstance(data_type, str): + ms = genutils.get_macro_server() + interfaces = ms.getInterfaces() + if data_type in interfaces: + input_data['data_type'] = [ + elem.name for elem in list(interfaces[data_type].values())] + self._conn.send(input_data) + ret = self._conn.recv() + return ret + + def input_timeout(self, input_data): + # parent process + self._conn.send(input_data) + + def safe_run(self, conn): + # child process + try: + return self.run(conn) + except Exception as e: + msgbox = TaurusMessageBox(*sys.exc_info()) + conn.send((e, False)) + msgbox.exec_() + + def run(self, conn): + # child process + self._conn = conn + app = Qt.QApplication.instance() + if app is None: + app = Qt.QApplication(['spock']) + app.setQuitOnLastWindowClosed(False) + self._msg_handler = MessageHandler(conn) + TaurusManager().addJob(self.run_forever, None) + app.exec_() + conn.close() + print("Quit input handler") + + def run_forever(self): + # child process + message, conn = True, self._conn + while message: + message = conn.recv() + if not message: + continue + self._msg_handler.handle_message(message) + app = Qt.QApplication.instance() + if app: + app.quit() diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py index adbcf9853a..78c511c8c5 100644 --- a/src/sardana/spock/spockms.py +++ b/src/sardana/spock/spockms.py @@ -38,7 +38,6 @@ from sardana.sardanautils import is_pure_str, is_non_str_seq from sardana.spock import genutils from sardana.util.parser import ParamParser -from sardana.spock.inputhandler import SpockInputHandler, InputHandler from sardana import sardanacustomsettings CHANGE_EVTS = TaurusEventType.Change, TaurusEventType.Periodic @@ -290,7 +289,9 @@ def __init__(self, name, **kw): self.call__init__(BaseDoor, name, **kw) def create_input_handler(self): - return SpockInputHandler(self) + from sardana.spock.inputhandler import SpockInputHandler + + return SpockInputHandler() def get_color_mode(self): return genutils.get_color_mode() @@ -513,6 +514,11 @@ def processRecordData(self, data): and data['type'] == 'function'): func_name = data['func_name'] if func_name.startswith("pyplot."): + try: + from taurus.external.qt import Qt + except ImportError: + print("Qt binding is not available. Macro plotting cannot work without it.") + return func_name = self.MathFrontend + "." + func_name args = data['args'] kwargs = data['kwargs'] @@ -554,9 +560,6 @@ def _processRecordData(self, data): return BaseDoor._processRecordData(self, data) -from taurus.external.qt import Qt - - class QSpockDoor(SpockBaseDoor): def __init__(self, name, **kw): @@ -575,6 +578,9 @@ def recordDataReceived(self, s, t, v): return res def create_input_handler(self): + from sardana.spock.inputhandler import SpockInputHandler + from sardana.spock.qtinputhandler import InputHandler + inputhandler = getattr(sardanacustomsettings, 'SPOCK_INPUT_HANDLER', "CLI") diff --git a/src/sardana/tango/pool/PoolDevice.py b/src/sardana/tango/pool/PoolDevice.py index ca4ce9b944..13060ffd4f 100644 --- a/src/sardana/tango/pool/PoolDevice.py +++ b/src/sardana/tango/pool/PoolDevice.py @@ -33,6 +33,7 @@ __docformat__ = 'restructuredtext' import time +from copy import deepcopy from PyTango import Util, DevVoid, DevLong64, DevBoolean, DevString,\ DevDouble, DevEncoded, DevVarStringArray, DispLevel, DevState, SCALAR, \ @@ -690,7 +691,9 @@ def _get_dynamic_attributes(self): attr_name_lower = attr_name.lower() if attr_name_lower in std_attrs_lower: data_info = DataInfo.toDataInfo(attr_name, attr_info) - tg_info = dev_class.standard_attr_list[attr_name] + # copy in order to leave the class attributes untouched + # the downstream code can append MaxDimSize to the attr. info + tg_info = deepcopy(dev_class.standard_attr_list[attr_name]) std_attrs[attr_name] = attr_name, tg_info, data_info else: data_info = DataInfo.toDataInfo(attr_name, attr_info) diff --git a/src/sardana/taurus/core/tango/sardana/macro.py b/src/sardana/taurus/core/tango/sardana/macro.py index d77feb4f91..4c497d121d 100644 --- a/src/sardana/taurus/core/tango/sardana/macro.py +++ b/src/sardana/taurus/core/tango/sardana/macro.py @@ -32,6 +32,7 @@ import os import copy +import uuid import types import tempfile @@ -833,7 +834,6 @@ def fromList(self, params): class MacroNode(BranchNode): """Class to represent macro element.""" - count = 0 def __init__(self, parent=None, name=None, params_def=None, macro_info=None): @@ -867,7 +867,7 @@ def id(self): """ Getter of macro's id property - :return: (int) + :return: (str) .. seealso: :meth:`MacroNode.setId`, assignId """ @@ -878,7 +878,7 @@ def setId(self, id): """ Setter of macro's id property - :param id: (int) new macro's id + :param id: (str) new macro's id See Also: id, assignId """ @@ -890,16 +890,13 @@ def assignId(self): If macro didn't have an assigned id it assigns it and return macro's id. - :return: (int) + :return: (str) See Also: id, setId """ - id = self.id() - if id is not None: - return id - MacroNode.count += 1 - self.setId(MacroNode.count) - return MacroNode.count + id_ = str(uuid.uuid1()) + self.setId(id_) + return id_ def name(self): return self._name @@ -1160,7 +1157,7 @@ def toXml(self, withId=True): if withId: id_ = self.id() if id_ is not None: - macroElement.set("id", str(self.id())) + macroElement.set("id", self.id()) for hookPlace in self.hookPlaces(): hookElement = etree.SubElement(macroElement, "hookPlace") hookElement.text = hookPlace diff --git a/src/sardana/taurus/core/tango/sardana/pool.py b/src/sardana/taurus/core/tango/sardana/pool.py index 91c2bfa124..8f476ad29e 100644 --- a/src/sardana/taurus/core/tango/sardana/pool.py +++ b/src/sardana/taurus/core/tango/sardana/pool.py @@ -2460,8 +2460,8 @@ def setIntegrationTime(self, ctime): def putIntegrationTime(self, ctime): if self._last_integ_time == ctime: return - self._last_integ_time = ctime self.getIntegrationTimeObj().write(ctime) + self._last_integ_time = ctime def getAcquisitionModeObj(self): return self._getAttrEG('AcquisitionMode') diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py index e1a4193a57..659cb60c6c 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py @@ -937,7 +937,6 @@ def onMacroStatusUpdated(self, data): "range"], data["step"], data["id"] if id is None: return - id = int(id) if id != self.macroId(): return macroName = macro.name diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py index 45c6feab7c..0a49fd9353 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py @@ -727,7 +727,6 @@ def onMacroStatusUpdated(self, data): "range"], data["step"], data["id"] if id is None: return - id = int(id) if not id in self.macroIds(): return macroName = macro.name diff --git a/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py b/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py index a31d5db83c..b2ec6f8b48 100644 --- a/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py +++ b/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py @@ -595,6 +595,7 @@ def __init__(self, parent=None, designMode=False): self.layout().addLayout(limits_layout, 0, 0) self.lbl_read = TaurusLabel() + self.lbl_read.setFgRole('rvalue.magnitude') self.lbl_read.setBgRole('quality') self.lbl_read.setSizePolicy(Qt.QSizePolicy( Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Fixed)) diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py index 102eab1aae..44d7fbeaa7 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py @@ -23,21 +23,228 @@ ## ############################################################################## -"""This module contains a taurus ShowScanOnline widget.""" +""" +This module contains a taurus ShowScanWidget, ShowScanWindow and ShowScanOnline +widgets. +""" -__all__ = ["ShowScanOnline"] +__all__ = [ + "ScanInfoForm", "ScanPointForm", "ScanPlotWidget", + "ScanPlotWindow", "ScanWindow", "ShowScanOnline" +] import click +import pkg_resources +from taurus.external.qt import Qt, uic +from taurus.qt.qtgui.base import TaurusBaseWidget from taurus.qt.qtgui.taurusgui import TaurusGui -from sardana.taurus.qt.qtgui.macrolistener import (DynamicPlotManager, - assertPlotAvailability) +from sardana.taurus.qt.qtgui.macrolistener import ( + MultiPlotWidget, PlotManager, DynamicPlotManager, assertPlotAvailability +) + + +def set_text(label, field=None, data=None, default='---'): + if field is None and data is None: + value = default + elif field is None: + value = data + elif data is None: + value = field + else: + value = data.get(field, default) + if isinstance(value, (tuple, list)): + value = ', '.join(value) + elif isinstance(value, float): + value = '{:8.4f}'.format(value) + else: + value = str(value) + if len(value) > 60: + value = '...{}'.format(value[-57:]) + label.setText(value) + + +def resize_form(form, new_size): + layout = form.layout() + curr_size = layout.rowCount() + nb = new_size - curr_size + while nb > 0: + layout.addRow(Qt.QLabel(), Qt.QLabel()) + nb -= 1 + while nb < 0: + layout.removeRow(layout.rowCount() - 1) + nb += 1 + + +def fill_form(form, fields, offset=0): + resize_form(form, len(fields) + offset) + layout = form.layout() + result = [] + for row, field in enumerate(fields): + label, value = field + w_item = layout.itemAt(row + offset, Qt.QFormLayout.LabelRole) + w_label = w_item.widget() + set_text(w_label, label) + w_item = layout.itemAt(row + offset, Qt.QFormLayout.FieldRole) + w_field = w_item.widget() + set_text(w_field, value) + result.append((w_label, w_field)) + return result + + +def load_scan_info_form(widget): + ui_name = pkg_resources.resource_filename(__package__ + '.ui', + 'ScanInfoForm.ui') + uic.loadUi(ui_name, baseinstance=widget) + return widget + + +class ScanInfoForm(Qt.QWidget, TaurusBaseWidget): + + def __init__(self, parent=None): + super().__init__(parent) + load_scan_info_form(self) + + def setModel(self, doorname): + super().setModel(doorname) + if not doorname: + return + door = self.getModelObj() + door.recordDataUpdated.connect(self.onRecordDataUpdated) + + def onRecordDataUpdated(self, record_data): + data = record_data[1] + handler = self.event_handler.get(data.get("type")) + handler and handler(self, data['data']) + + def onStart(self, meta): + set_text(self.title_value, 'title', meta) + set_text(self.scan_nb_value, 'serialno', meta) + set_text(self.start_value, 'starttime', meta) + set_text(self.end_value, 'endtime', meta) + set_text(self.status_value, 'Running') + + directory = meta.get('scandir', '') + self.directory_groupbox.setEnabled(True if directory else False) + self.directory_groupbox.setTitle('Directory: {}'.format(directory)) + files = meta.get('scanfile', ()) + if isinstance(files, str): + files = files, + elif files is None: + files = () + files = [('File:', filename) for filename in files] + fill_form(self.directory_groupbox, files) + + def onEnd(self, meta): + set_text(self.end_value, 'endtime', meta) + set_text(self.status_value, 'Finished') + + event_handler = { + "data_desc": onStart, + "record_end": onEnd + } + + +def load_scan_point_form(widget): + ui_name = pkg_resources.resource_filename(__package__ + '.ui', + 'ScanPointForm.ui') + uic.loadUi(ui_name, baseinstance=widget) + return widget + + +class ScanPointForm(Qt.QWidget, TaurusBaseWidget): + + def __init__(self, parent=None): + super().__init__(parent) + load_scan_point_form(self) + self._in_scan = False + + def setModel(self, doorname): + super().setModel(doorname) + if not doorname: + return + door = self.getModelObj() + door.recordDataUpdated.connect(self.onRecordDataUpdated) + + def onRecordDataUpdated(self, record_data): + data = record_data[1] + handler = self.event_handler.get(data.get("type")) + handler and handler(self, data['data']) + + def onStart(self, meta): + set_text(self.scan_nb_value, 'serialno', meta) + cols = meta['column_desc'] + col_labels = [(c['label']+':', '') for c in cols] + fields = fill_form(self, col_labels, 1) + self.fields = {col['name']: field for col, field in zip(cols, fields)} + self._in_scan = True + + def onPoint(self, point): + if self._in_scan: + for name, value in point.items(): + set_text(self.fields[name][1], value) + + def onEnd(self, meta): + self._in_scan = False + + event_handler = { + "data_desc": onStart, + "record_data": onPoint, + "record_end": onEnd + } + + +class ScanPlotWidget(MultiPlotWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.manager = PlotManager(self) + self.setModel = self.manager.setModel + self.setGroupMode = self.manager.setGroupMode + + +class ScanPlotWindow(Qt.QMainWindow): + + def __init__(self, parent=None): + super().__init__() + plot_widget = ScanPlotWidget(parent=self) + self.setCentralWidget(plot_widget) + self.plotWidget = self.centralWidget + self.setModel = plot_widget.setModel + self.setGroupMode = plot_widget.setGroupMode + sbar = self.statusBar() + sbar.showMessage("Ready!") + plot_widget.manager.newShortMessage.connect(sbar.showMessage) + + +def load_scan_window(widget): + ui_name = pkg_resources.resource_filename(__package__ + '.ui', + 'ScanWindow.ui') + uic.loadUi(ui_name, baseinstance=widget) + return widget + + +class ScanWindow(Qt.QMainWindow): + + def __init__(self, parent=None): + super().__init__() + load_scan_window(self) + sbar = self.statusBar() + sbar.showMessage("Ready!") + self.plot_widget.manager.newShortMessage.connect(sbar.showMessage) + + def setModel(self, model): + self.plot_widget.setModel(model) + self.info_form.setModel(model) + self.point_form.setModel(model) + class ShowScanOnline(DynamicPlotManager): def __init__(self, parent): - DynamicPlotManager.__init__(self, parent) + DynamicPlotManager.__init__(self, parent=parent) + Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') def onExpConfChanged(self, expconf): DynamicPlotManager.onExpConfChanged(self, expconf) @@ -106,12 +313,10 @@ def main(group, taurus_log_level, door): assertPlotAvailability() - gui = TaurusGuiLite() - - widget = ShowScanOnline(gui) + widget = ScanWindow() + widget.plot_widget.setGroupMode(group) widget.setModel(door) - widget.setGroupMode(group) - gui.show() + widget.show() return app.exec_() diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui new file mode 100644 index 0000000000..3784123163 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui @@ -0,0 +1,141 @@ + + + Tiago Coutinho + scan_info_form + + + + 0 + 0 + 300 + 169 + + + + Form + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 3 + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Scan #: + + + + + + + --- + + + + + + + Title: + + + + + + + --- + + + + + + + Start: + + + + + + + --- + + + + + + + End: + + + + + + + --- + + + + + + + Status: + + + + + + + --- + + + + + + + Directory:--- + + + + 6 + + + 3 + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + + + + + diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui new file mode 100644 index 0000000000..45056f10f3 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui @@ -0,0 +1,52 @@ + + + Form + + + + 0 + 0 + 400 + 291 + + + + Form + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 3 + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Scan #: + + + + + + + + + + + diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui new file mode 100644 index 0000000000..9c011166d6 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui @@ -0,0 +1,150 @@ + + + MainWindow + + + + 0 + 0 + 891 + 600 + + + + + + 3 + + + 0 + + + 6 + + + 6 + + + 6 + + + + + + + + + + Scan point + + + 2 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 250 + 0 + + + + QFrame::NoFrame + + + 0 + + + true + + + + + 0 + 0 + 250 + 240 + + + + + + + + + + + Scan information + + + 2 + + + + + 250 + 0 + + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 232 + 276 + + + + + + + + + + + + ScanPointForm + QWidget +
sardana.taurus.qt.qtgui.extra_sardana.showscanonline
+ 1 +
+ + ScanInfoForm + QWidget +
sardana.taurus.qt.qtgui.extra_sardana.showscanonline
+ 1 +
+ + ScanPlotWidget + QWidget +
sardana.taurus.qt.qtgui.extra_sardana.showscanonline
+ 1 +
+
+ + +
diff --git a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py index f06fbbe6a3..1a2e7a07c2 100644 --- a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py +++ b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py @@ -56,7 +56,10 @@ from sardana.taurus.core.tango.sardana import PlotType -__all__ = ['MacroBroker', 'DynamicPlotManager', 'assertPlotAvailability'] +__all__ = [ + 'MultiPlotWidget', 'MacroBroker', 'PlotManager', 'DynamicPlotManager', + 'assertPlotAvailability' +] __docformat__ = 'restructuredtext' @@ -91,6 +94,7 @@ class MultiPlotWidget(Qt.QWidget): def __init__(self, parent=None): super().__init__(parent) layout = Qt.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) self.win = pyqtgraph.GraphicsLayoutWidget() layout.addWidget(self.win) self._plots = {} @@ -177,16 +181,15 @@ def _end_update(self): self._timer = None -class DynamicPlotManager(Qt.QObject, TaurusBaseComponent): - '''This is a manager of plots related to the execution of macros. +class PlotManager(Qt.QObject, TaurusBaseComponent): + ''' + This is a manager of plots related to the execution of macros. It dynamically creates/removes plots according to the configuration made by an ExperimentConfiguration widget. Currently it supports only 1D scan trends (2D scans are only half-baked) - To use it simply instantiate it and pass it a door name as a model. You may - want to call :meth:`onExpConfChanged` to update the configuration being - used. + To use it simply instantiate it and pass it a door name as a model. ''' plots_available = pyqtgraph is not None @@ -196,16 +199,13 @@ class DynamicPlotManager(Qt.QObject, TaurusBaseComponent): Single = 'single' # each curve has its own plot XAxis = 'x-axis' # group curves with same X-Axis - def __init__(self, parent=None): + def __init__(self, plot=None, parent=None): Qt.QObject.__init__(self, parent) TaurusBaseComponent.__init__(self, self.__class__.__name__) self._group_mode = self.XAxis - Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') - self._plot = MultiPlotWidget() - self.createPanel( - self._plot, 'Scan plot', registerconfig=False, permanent=False) + self.plot = plot or MultiPlotWidget() def setGroupMode(self, group): assert group in (self.Single, self.XAxis) @@ -329,7 +329,7 @@ def prepare(self, data_desc): raise NotImplementedError nb_points = data.get('total_scan_intervals', 2**16 - 1) + 1 - self._plot.prepare(plots, nb_points=nb_points) + self.plot.prepare(plots, nb_points=nb_points) # build status message serialno = 'Scan #{}'.format(data.get('serialno', '?')) @@ -350,18 +350,35 @@ def prepare(self, data_desc): def newPoint(self, point): data = point['data'] - self._plot.onNewPoint(data) + self.plot.onNewPoint(data) point_nb = 'Point #{}'.format(data['point_nb']) msg = self.message_template.format(progress=point_nb) self.newShortMessage.emit(msg) def end(self, end_data): data = end_data['data'] - self._plot.onEnd(data) + self.plot.onEnd(data) progress = 'Ended {}'.format(data['endtime']) msg = self.message_template.format(progress=progress) self.newShortMessage.emit(msg) + +class DynamicPlotManager(PlotManager): + '''This is a manager of plots related to the execution of macros. + It dynamically creates/removes plots according to the configuration made by + an ExperimentConfiguration widget. + + Currently it supports only 1D scan trends (2D scans are only half-baked) + + To use it simply instantiate it and pass it a door name as a model. + ''' + + def __init__(self, *args, **kwargs): + PlotManager.__init__(self, *args, **kwargs) + self.__panels = {} + self.createPanel( + self.plot, 'Scan plot', registerconfig=False, permanent=False) + def createPanel(self, widget, name, **kwargs): '''Creates a "panel" from a widget. In this basic implementation this means that the widgets is shown as a non-modal top window @@ -414,12 +431,13 @@ class MacroBroker(DynamicPlotManager): def __init__(self, parent): '''Passing the parent object (the main window) is mandatory''' - DynamicPlotManager.__init__(self, parent) + DynamicPlotManager.__init__(self, parent=parent) self._createPermanentPanels() # connect the broker to shared data Qt.qApp.SDM.connectReader("doorName", self.setModel) + Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') def setModel(self, doorname): ''' Reimplemented from :class:`DynamicPlotManager`.'''