diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 49c6dfb671..3c6abc7edb 100755 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -3,7 +3,7 @@ commit = True message = Bump version {current_version} to {new_version} tag = False tag_name = {new_version} -current_version = 2.5.0 +current_version = 2.7.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))? serialize = {major}.{minor}.{patch}-{release} diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bbed99897..1699c5275c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,119 @@ This file follows the formats and conventions from [keepachangelog.com] ## [Unreleased] +## [2.7.0] 2019-03-11 + +### Added + +* Possibility to directly acquire an experimental channel (without the need to define + a measurement group) (#185, #997) + * `IntegrationTime` (Tango) and `integration_time` (core) attributes to all experimental + channels + * `Timer` (Tango) and `timer` (core) attribute to all timerable experimental channels + * `default_timer` class attribute to all timerable controllers (plugins) to let them + announce the default timer axis +* `newfile` macro for setting `ScanDir`, `ScanFile` and `ScanID` env variables (#777) + +### Fixed + +* `expconf` warns only about the following environment variables changes: `ScanFile`, + `ScanDir`, `ActiveMntGrp`, `PreScanSnapshot` and `DataCompressionRank` (#1040) +* MeasurementGroup's Moveable attribute when set to "None" in Tango is used as None + in the core (#1001) + +## [2.6.1] 2019-02-04 + +This is a special release for meeting the deadline of debian buster freeze (debian 10). + +### Fixed +- String parameter editor in macroexecutor and sequencer (#1030, #1031) +- Documentation on differences between `Hookable.hooks` and `Hookable.appendHook` + (#962, #1013) + +## [2.6.0] 2019-01-31 + +This is a special release for meeting the deadline of debian buster freeze (debian 10). + +### Added +- New acquisition and synchronization concepts (SEP18, #773): + - Preparation of measurement group for a group of acquisitions is mandatory + (`Prepare` Tango command and `prepare` core method; `NbStarts` Tango + attribute and `nb_starts` core attribute; `count`, `count_raw` and + `count_continuous` methods in Taurus extension) + - Preparation of timerable controllers is optional (`PrepareOne` method) + - `SoftwareStart` and `HardwareStart` options in `AcqSynch` enumeration and + `Start` in `AcqSynchType` enumeration (the second one is available in + the `expconf` as synchronization option) + - `start` and `end` events in software synchronizer + - `PoolAcquisitionSoftwareStart` acquisition action + - `SoftwareStart` and `HardwareStart` synchronization in + `DummyCounterTimerController` +- Support to Qt5 for Sardana-Taurus widgets and Sardana-Taurus extensions (#1006, + #1009) +- Possibility to define macros with optional parameters. These must be the last + ones in the definition (#285, #876, #943, #941, #955) +- Possibility to pass values of repeat parameters with just one member without + the need to encapsulate them in square brackets (spock syntax) or list + (macro API) (#781, #983) +- Possibility to change data format (shape) of of pseudo counter values (#986) +- Check scan range agains motor limits wheneve possible (#46, #963) +- Workaround for API_DeviceTimedOut errors on MeasurementGroup Start. Call Stop + in case this error occured (#764). +- Optional measurement group parameter to `ct` and `uct` macros (#940, #473) +- Support to "PETRA3 P23 6C" and "PETRA3 P23 4C" diffractometers by means + of new controller classes and necessary adaptation to macros (#923, #921) +- Top LICENSE file that applies to the whole project (#938) +- Document remote connection to MacroServer Python process (RConsolePort Tango + property) (#984) +- sardana.taurus.qt.qtgui.macrolistener (moved from taurus.qt.qtgui.taurusgui) +- Documentation on differences between `Hookable.hooks` and `Hookable.appendHook` + (#962, #1013) + +### Fixed +- Do not read 1D and 2D experimental channels during software acquisition loop + (#967) +- Make `expconf` react on events of environment, measurement groups and their + configurations. An event offers an option to reload the whole experiment + configuration or keep the local changes. `expconf` started with + `--auto-update` option will automatically reload the whole experiment + configuration (#806, #882, #988, #1028, #1033) +- Reload macro library overriding another library (#927, #946) +- Avoid final padding in timescan when it was stopped by user (#869, #935) +- Moveables limits check in continuous scans when moveables position attribute + has unit configured and Taurus 4 is used (quantities) (#989, #990) +- Hook places advertised by continuous scans so the `allowHooks` hint and the + code are coherent (#936) +- Macro/controller module description when module does not have a docstring + (#945) +- Make `wu` macro respect view options (#1000, #1002) +- Make cleanup (remove configuration) if spock profile creation was interrupted + or failed (#791, #793) +- Spock considers passing supernumerary parameters as errors (#438, #781) +- MacroServer starts without the Qt library installed (#781, #907, #908) +- Make `Description` an optional part of controller's properties definition (#976) +- Correcting bug in hkl macros introduced when extending macros for new + diffractometer types: angle order was switched + +### Changed +- MacroButton stops macros instead of aborting them (#931, #943) +- Spock syntax and advanced spock syntax are considered as one in documentaion + (#781) +- Move pre-scan and post-scan hooks out of `scan_loop` method (#920, #922, + #933) +- Logstash handler from python-logstash to python-logstash-async (#895) +- Move `ParamParser` to `sardana.util.parser` (#781, #907, #908) +- SpockCommandWidget.returnPressed method renamed to onReturnPressed +- SpockCommandWidget.textChanged method renamed to onTextChanged + +### Deprecated +- Measurement group start without prior preparation (SEP18, #773) +- Loadable controller's API: `LoadOne(axis, value, repeats)` + in favor of `LoadOne(axis, value, repeats, latency)` (SEP18, #773) +- Unused class `sardana.taurus.qt.qtgui.extra_macroexecutor.dooroutput.DoorAttrListener` + +### Removed +- Support to Qt < 4.7.4 (#1006, #1009) + ## [2.5.0] 2018-08-10 ### Added @@ -79,6 +192,11 @@ This file follows the formats and conventions from [keepachangelog.com] - `Controller.getUsedAxis` (Taurus device extension) in favor of `Controller.getUsedAxes` (#609) +### Removed +- Signal `modelChanged()` from ParamBase class to use the call to + method onModelChanged directly instead + + ## [2.4.0] 2018-03-14 ### Added @@ -471,7 +589,10 @@ Main improvements since sardana 1.5.0 (aka Jan15): [keepachangelog.com]: http://keepachangelog.com -[Unreleased]: https://github.com/sardana-org/sardana/compare/2.5.0...HEAD +[Unreleased]: https://github.com/sardana-org/sardana/compare/2.7.0...HEAD +[2.7.0]: https://github.com/sardana-org/sardana/compare/2.6.1...2.7.0 +[2.6.1]: https://github.com/sardana-org/sardana/compare/2.6.0...2.6.1 +[2.6.0]: https://github.com/sardana-org/sardana/compare/2.5.0...2.6.0 [2.5.0]: https://github.com/sardana-org/sardana/compare/2.4.0...2.5.0 [2.4.0]: https://github.com/sardana-org/sardana/compare/2.3.2...2.4.0 [2.3.2]: https://github.com/sardana-org/sardana/compare/2.3.1...2.3.2 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..85dc2b3f4e --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +Sardana is Free Software by the CELLS / ALBA Synchrotron, Bellaterra, Spain + +SECTION 1: GENERAL LICENSE FOR SARDANA SOURCE CODE +================================================= + +The files in Sardana, except for the cases described in SECTION 2 +are distributed under 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. +See + +SECTION 2: EXCEPTIONS +===================== + +Some files (e.g., those authored by 3rd parties or the documentation +sources) are distributed under Free Software / documentation licenses +that may differ from the the general one defined in SECTION 1. + +The following is a list of these exceptions: + +2.1: Explicit copyright info in header/metadata: +------------------------------------------------ + +If a file contains an explicit license or other copyright information in +its header or metadata which differs from the one defined in SECTION 1, +such license/copyright info mentioned in the header/metadata prevails. + +2.2: Documentation: +------------------- + +The .py scripts in the doc directory are treated as per SECTION 1, +and the rest of its files are distributed under a Creative Commons +Attribution 3.0 License +See diff --git a/doc/how_to_release.md b/doc/how_to_release.md index 62703082dc..cc84e0ce1f 100644 --- a/doc/how_to_release.md +++ b/doc/how_to_release.md @@ -1,4 +1,4 @@ -# How to release (draft) +# How to release This is a guide for sardana release managers: it details the steps for making an official release, including a checklist of stuff that should be manually @@ -34,6 +34,7 @@ tested. 3. The version numbers used in the man pages of the Sardana scripts are bumped (you may use `taurus/doc/makeman` script executing it from the doc directory e.g. `sardana/doc`) and committing the changes. + There is a known [problem with the spock version number](https://github.com/sardana-org/sardana/issues/518). 4. In the code use version number instead of milestone in deprecation warnings (if any) e.g. replace *Jul18* with *2.5.0*. 5. Create a PR to merge the `release-XXX` against the **`master`** branch @@ -63,7 +64,7 @@ tested. * Ilustration: sardana or official logo (use png) * Summary: short summary of the news (do not include the whole changelog here..) * Categories: Release - 2. After submitting click on Modify this content text of the area <> and provide detailes of the release e.g. changelog. + 2. After submitting click on Modify this content text of the area \<\\> and provide detailes of the release e.g. changelog. 12. Notify mailing lists (sardana-users@lists.sourceforge.net, sardana-devel@lists.sourceforge.net, info@tango-controls.org) ## Manual test checklist @@ -111,8 +112,7 @@ Hint: this list can be used as a template to be copy-pasted on a release manual attribute. - [ ] Add the `sys/tg_test/1/double_scalar` attribute to the measurement group. -- [ ] Open online plot. -- [ ] Set JsonRecorder to true. In spock do `senv JsonRecorder True` +- [ ] Open online plot (This should ask to enable JsonRecorder, set it to true. Otherwise enable it in spock: `senv JsonRecorder True`). - [ ] Run step scan - [ ] Verify that records appear in spock output. - [ ] Verify that records were stored in scan files. diff --git a/doc/man/MacroServer.1 b/doc/man/MacroServer.1 index 0bfce5ccc0..4999b65801 100644 --- a/doc/man/MacroServer.1 +++ b/doc/man/MacroServer.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH MACROSERVER "1" "August 2018" "MacroServer 2.5.0" "User Commands" +.TH MACROSERVER "1" "February 2019" "MacroServer 2.7.0" "User Commands" .SH NAME -MacroServer \- manual page for MacroServer 2.5.0 +MacroServer \- manual page for MacroServer 2.7.0 .SH SYNOPSIS .B usage: \fI\,MacroServer instance_name \/\fR[\fI\,options\/\fR] diff --git a/doc/man/Pool.1 b/doc/man/Pool.1 index fae5daa19f..9ff11a4bc1 100644 --- a/doc/man/Pool.1 +++ b/doc/man/Pool.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH POOL "1" "August 2018" "Pool 2.5.0" "User Commands" +.TH POOL "1" "February 2019" "Pool 2.7.0" "User Commands" .SH NAME -Pool \- manual page for Pool 2.5.0 +Pool \- manual page for Pool 2.7.0 .SH SYNOPSIS .B usage: \fI\,Pool instance_name \/\fR[\fI\,options\/\fR] diff --git a/doc/man/Sardana.1 b/doc/man/Sardana.1 index 9fa4ea1d9d..b8f64980f3 100644 --- a/doc/man/Sardana.1 +++ b/doc/man/Sardana.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH SARDANA "1" "August 2018" "Sardana 2.5.0" "User Commands" +.TH SARDANA "1" "February 2019" "Sardana 2.7.0" "User Commands" .SH NAME -Sardana \- manual page for Sardana 2.5.0 +Sardana \- manual page for Sardana 2.7.0 .SH SYNOPSIS .B usage: \fI\,Sardana instance_name \/\fR[\fI\,options\/\fR] diff --git a/doc/man/diffractometeralignment.1 b/doc/man/diffractometeralignment.1 index 764cef0d7f..d42d20178f 100644 --- a/doc/man/diffractometeralignment.1 +++ b/doc/man/diffractometeralignment.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH DIFFRACTOMETERALIGNMENT "1" "August 2018" "diffractometeralignment 2.5.0" "User Commands" +.TH DIFFRACTOMETERALIGNMENT "1" "February 2019" "diffractometeralignment 2.7.0" "User Commands" .SH NAME -diffractometeralignment \- manual page for diffractometeralignment 2.5.0 +diffractometeralignment \- manual page for diffractometeralignment 2.7.0 .SH SYNOPSIS .B diffractometeralignment \fI\, \/\fR[\fI\,door_name\/\fR] diff --git a/doc/man/hklscan.1 b/doc/man/hklscan.1 index d1399936db..7fab5d20d1 100644 --- a/doc/man/hklscan.1 +++ b/doc/man/hklscan.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH HKLSCAN "1" "August 2018" "hklscan 2.5.0" "User Commands" +.TH HKLSCAN "1" "February 2019" "hklscan 2.7.0" "User Commands" .SH NAME -hklscan \- manual page for hklscan 2.5.0 +hklscan \- manual page for hklscan 2.7.0 .SH SYNOPSIS .B hklscan \fI\, \/\fR[\fI\,door_name\/\fR] diff --git a/doc/man/macroexecutor.1 b/doc/man/macroexecutor.1 index 4e0a7522eb..08995d5bb6 100644 --- a/doc/man/macroexecutor.1 +++ b/doc/man/macroexecutor.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH MACROEXECUTOR "1" "August 2018" "macroexecutor 2.5.0" "User Commands" +.TH MACROEXECUTOR "1" "February 2019" "macroexecutor 2.7.0" "User Commands" .SH NAME -macroexecutor \- manual page for macroexecutor 2.5.0 +macroexecutor \- manual page for macroexecutor 2.7.0 .SH SYNOPSIS .B macroexecutor [\fI\,options\/\fR] diff --git a/doc/man/sardanatestsuite.1 b/doc/man/sardanatestsuite.1 index 773d0832e8..0a92987164 100644 --- a/doc/man/sardanatestsuite.1 +++ b/doc/man/sardanatestsuite.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH SARDANATESTSUITE "1" "August 2018" "sardanatestsuite 2.5.0" "User Commands" +.TH SARDANATESTSUITE "1" "February 2019" "sardanatestsuite 2.7.0" "User Commands" .SH NAME -sardanatestsuite \- manual page for sardanatestsuite 2.5.0 +sardanatestsuite \- manual page for sardanatestsuite 2.7.0 .SH DESCRIPTION usage: sardanatestsuite [\-h] [\-e EXCLUDE_PATTERN] [\-\-version] .PP diff --git a/doc/man/sequencer.1 b/doc/man/sequencer.1 index c8c454c47e..1d8adc66b3 100644 --- a/doc/man/sequencer.1 +++ b/doc/man/sequencer.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH SEQUENCER "1" "August 2018" "sequencer 2.5.0" "User Commands" +.TH SEQUENCER "1" "February 2019" "sequencer 2.7.0" "User Commands" .SH NAME -sequencer \- manual page for sequencer 2.5.0 +sequencer \- manual page for sequencer 2.7.0 .SH SYNOPSIS .B sequencer [\fI\,options\/\fR] diff --git a/doc/man/spock.1 b/doc/man/spock.1 index 5fec32848d..677f7b8f62 100644 --- a/doc/man/spock.1 +++ b/doc/man/spock.1 @@ -1,7 +1,7 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH SPOCK "1" "August 2018" "spock 2.5.0" "User Commands" +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.4. +.TH SPOCK "1" "February 2019" "spock 2.7.0" "User Commands" .SH NAME -spock \- manual page for spock 2.5.0 +spock \- manual page for spock 2.7.0 .SH DESCRIPTION ========= .IP @@ -248,14 +248,14 @@ The IPython profile to use. \fB\-\-pylab=\fR (InteractiveShellApp.pylab) .IP Default: None -Choices: [u'auto', u'agg', u'gtk', u'gtk3', u'inline', u'ipympl', u'nbagg', u'notebook', u'osx', u'pdf', u'ps', u'qt', u'qt4', u'qt5', u'svg', u'tk', u'widget', u'wx'] +Choices: [u'auto', u'gtk', u'gtk3', u'inline', u'nbagg', u'notebook', u'osx', u'qt', u'qt4', u'qt5', u'tk', u'wx'] Pre\-load matplotlib and numpy for interactive use, selecting a particular matplotlib backend and loop integration. .PP \fB\-\-matplotlib=\fR (InteractiveShellApp.matplotlib) .IP Default: None -Choices: [u'auto', u'agg', u'gtk', u'gtk3', u'inline', u'ipympl', u'nbagg', u'notebook', u'osx', u'pdf', u'ps', u'qt', u'qt4', u'qt5', u'svg', u'tk', u'widget', u'wx'] +Choices: [u'auto', u'gtk', u'gtk3', u'inline', u'nbagg', u'notebook', u'osx', u'qt', u'qt4', u'qt5', u'tk', u'wx'] Configure matplotlib for interactive use with the default matplotlib backend. .PP diff --git a/doc/man/ubmatrix.1 b/doc/man/ubmatrix.1 index 0e0aafaa6f..caaf39b711 100644 --- a/doc/man/ubmatrix.1 +++ b/doc/man/ubmatrix.1 @@ -1,7 +1,7 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.3. -.TH UBMATRIX "1" "August 2018" "ubmatrix 2.5.0" "User Commands" +.TH UBMATRIX "1" "February 2019" "ubmatrix 2.7.0" "User Commands" .SH NAME -ubmatrix \- manual page for ubmatrix 2.5.0 +ubmatrix \- manual page for ubmatrix 2.7.0 .SH SYNOPSIS .B ubmatrix \fI\,\/\fR diff --git a/doc/source/_static/macrobutton.png b/doc/source/_static/macrobutton.png old mode 100755 new mode 100644 diff --git a/doc/source/_static/macrobutton_abort.png b/doc/source/_static/macrobutton_abort.png old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/api_1D.rst b/doc/source/devel/api/api_1D.rst index ef2628e39b..5d78caff37 100644 --- a/doc/source/devel/api/api_1D.rst +++ b/doc/source/devel/api/api_1D.rst @@ -18,10 +18,24 @@ The other attributes are: data source Unique identifier for the 1D data (value attribute) +timer + name of the timer channel (proceeding from the same controller) to be used + when the channel is acquired independently + + special values: + + * __default - controller's default timer + * __self - the same channel acts like a timer + * None - independent acquisition is disabled + +integration time + integration time (in seconds) to be used when the channel is acquired + independently + The available operations are: -start acquisition(integration time) - starts to acquire the 1D with the given integration time +start acquisition + starts to acquire the 1D :meth:`~PoolCounterTimer.start_acquisition` diff --git a/doc/source/devel/api/api_2D.rst b/doc/source/devel/api/api_2D.rst index c343a0ebc2..9b6641eb69 100644 --- a/doc/source/devel/api/api_2D.rst +++ b/doc/source/devel/api/api_2D.rst @@ -18,10 +18,24 @@ The other attributes are: data source Unique identifier for the 2D data (value attribute) +timer + name of the timer channel (proceeding from the same controller) to be used + when the channel is acquired independently + + special values: + + * __default - controller's default timer + * __self - the same channel acts like a timer + * None - independent acquisition is disabled + +integration time + integration time (in seconds) to be used when the channel is acquired + independently + The available operations are: -start acquisition(integration time) - starts to acquire the 2D with the given integration time +start acquisition + starts to acquire the 2Ds :meth:`~PoolCounterTimer.start_acquisition` diff --git a/doc/source/devel/api/api_countertimer.rst b/doc/source/devel/api/api_countertimer.rst index 4e25289bcf..3bcec1ad20 100644 --- a/doc/source/devel/api/api_countertimer.rst +++ b/doc/source/devel/api/api_countertimer.rst @@ -13,12 +13,28 @@ A counter/timer has a ``state``, and a ``value`` attributes. The state indicates at any time if the counter/timer is stopped, in alarm or moving. The value, indicates the current counter/timer value. +The other attributes are: + +timer + name of the timer channel (proceeding from the same controller) to be used + when the channel is acquired independently + + special values: + + * __default - controller's default timer + * __self - the same channel acts like a timer + * None - independent acquisition is disabled + +integration time + integration time (in seconds) to be used when the channel is acquired + independently + The available operations are: -start acquisition(integration time) - starts to acquire the counter/timer with the given integration time +start acquisition + starts to acquire the counter/timer - :meth:`~PoolCounterTimer.start_acquisition` + :meth:`~sardana.pool.poolbasechannel.PoolTimerableChannel.start_acquisition` stop stops the counter/timer acquisition in an orderly fashion diff --git a/doc/source/devel/api/api_measurementgroup.rst b/doc/source/devel/api/api_measurementgroup.rst index 0085f726db..f1355afbb1 100644 --- a/doc/source/devel/api/api_measurementgroup.rst +++ b/doc/source/devel/api/api_measurementgroup.rst @@ -6,6 +6,12 @@ Measurement group API reference ================================ +.. important:: + Measurement group :term:`API` was extended in SEP18_ but this is still + not documented in this chapter. Please check the said SEP for more + information about the additional :term:`API` or eventual changes. + + The measurement group is a group element. It aggregates other elements like experimental channels (counter/timer, 0D, 1D and 2D or external attribute e.g. Tango_) and trigger/gates. The measurement group role is to execute acquisitions @@ -92,4 +98,5 @@ start acquisition() .. :class:`~sardana.pool.poolmeasurementgroup.PoolMeasurementGroup` .. the measurement group class :term:`API` -.. _Tango: http://www.tango-controls.org \ No newline at end of file +.. _Tango: http://www.tango-controls.org +.. _SEP18: http://www.sardana-controls.org/sep/?SEP18.md \ No newline at end of file diff --git a/doc/source/devel/api/sardana/macroserver/macros/communication.rst b/doc/source/devel/api/sardana/macroserver/macros/communication.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/demo.rst b/doc/source/devel/api/sardana/macroserver/macros/demo.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/env.rst b/doc/source/devel/api/sardana/macroserver/macros/env.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/expert.rst b/doc/source/devel/api/sardana/macroserver/macros/expert.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/hkl.rst b/doc/source/devel/api/sardana/macroserver/macros/hkl.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/ioregister.rst b/doc/source/devel/api/sardana/macroserver/macros/ioregister.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/lists.rst b/doc/source/devel/api/sardana/macroserver/macros/lists.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/mca.rst b/doc/source/devel/api/sardana/macroserver/macros/mca.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/scan.rst b/doc/source/devel/api/sardana/macroserver/macros/scan.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/sequence.rst b/doc/source/devel/api/sardana/macroserver/macros/sequence.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/macroserver/macros/standard.rst b/doc/source/devel/api/sardana/macroserver/macros/standard.rst old mode 100755 new mode 100644 diff --git a/doc/source/devel/api/sardana/pool.rst b/doc/source/devel/api/sardana/pool.rst index 6465378d7f..e286d2cd6b 100644 --- a/doc/source/devel/api/sardana/pool.rst +++ b/doc/source/devel/api/sardana/pool.rst @@ -38,6 +38,7 @@ poolonedexpchannel poolpseudocounter poolpseudomotor + poolsynchronization pooltwodexpchannel poolutil poolzerodexpchannel diff --git a/doc/source/devel/api/sardana/pool/pooldefs.rst b/doc/source/devel/api/sardana/pool/pooldefs.rst index 93f3a85a5d..8f810f4b3b 100644 --- a/doc/source/devel/api/sardana/pool/pooldefs.rst +++ b/doc/source/devel/api/sardana/pool/pooldefs.rst @@ -9,17 +9,18 @@ .. autodata:: sardana.pool.pooldefs.ControllerAPI -.. rubric:: Classes +.. rubric:: Enumerations .. hlist:: :columns: 3 * :class:`~AcqSynch` + * :data:`~AcqSynchType` * :class:`~SynchParam` * :class:`~SynchDomain` AcqSynch ----------- +-------- .. inheritance-diagram:: AcqSynch :parts: 1 @@ -30,6 +31,12 @@ AcqSynch :undoc-members: +AcqSynchType +------------ + +.. autodata:: AcqSynchType + + SynchParam ---------- diff --git a/doc/source/devel/api/sardana/pool/poolmeasurementgroup.rst b/doc/source/devel/api/sardana/pool/poolmeasurementgroup.rst index 7f5c2ed0b1..4a40633c65 100644 --- a/doc/source/devel/api/sardana/pool/poolmeasurementgroup.rst +++ b/doc/source/devel/api/sardana/pool/poolmeasurementgroup.rst @@ -5,22 +5,113 @@ .. automodule:: sardana.pool.poolmeasurementgroup +.. rubric:: Functions + +.. hlist:: + :columns: 3 + + * :func:`build_measurement_configuration` + .. rubric:: Classes .. hlist:: :columns: 3 * :class:`PoolMeasurementGroup` + * :class:`ConfigurationItem` + * :class:`ControllerConfiguration` + * :class:`TimerableControllerConfiguration` + * :class:`ExternalControllerConfiguration` + * :class:`ChannelConfiguration` + * :class:`SynchronizerConfiguration` + * :class:`MeasurementConfiguration` +.. autofunction:: build_measurement_configuration -PoolInstrument --------------- +PoolMeasurementGroup +-------------------- .. inheritance-diagram:: PoolMeasurementGroup :parts: 1 - + .. autoclass:: PoolMeasurementGroup :show-inheritance: :members: :undoc-members: +ConfigurationItem +----------------- + +.. inheritance-diagram:: ConfigurationItem + :parts: 1 + +.. autoclass:: ConfigurationItem + :show-inheritance: + :members: + :undoc-members: + +ControllerConfiguration +----------------------- + +.. inheritance-diagram:: ControllerConfiguration + :parts: 1 + +.. autoclass:: ControllerConfiguration + :show-inheritance: + :members: + :undoc-members: + +TimerableControllerConfiguration +-------------------------------- + +.. inheritance-diagram:: TimerableControllerConfiguration + :parts: 1 + +.. autoclass:: TimerableControllerConfiguration + :show-inheritance: + :members: + :undoc-members: + +ExternalControllerConfiguration +------------------------------- + +.. inheritance-diagram:: ExternalControllerConfiguration + :parts: 1 + +.. autoclass:: ExternalControllerConfiguration + :show-inheritance: + :members: + :undoc-members: + +ChannelConfiguration +-------------------- + +.. inheritance-diagram:: ChannelConfiguration + :parts: 1 + +.. autoclass:: ChannelConfiguration + :show-inheritance: + :members: + :undoc-members: + +SynchronizerConfiguration +------------------------- + +.. inheritance-diagram:: SynchronizerConfiguration + :parts: 1 + +.. autoclass:: SynchronizerConfiguration + :show-inheritance: + :members: + :undoc-members: + +MeasurementConfiguration +------------------------ + +.. inheritance-diagram:: MeasurementConfiguration + :parts: 1 + +.. autoclass:: MeasurementConfiguration + :show-inheritance: + :members: + :undoc-members: diff --git a/doc/source/devel/api/sardana/pool/poolsynchronization.rst b/doc/source/devel/api/sardana/pool/poolsynchronization.rst new file mode 100644 index 0000000000..35e600de2f --- /dev/null +++ b/doc/source/devel/api/sardana/pool/poolsynchronization.rst @@ -0,0 +1,36 @@ +.. currentmodule:: sardana.pool.poolsynchronization + +:mod:`~sardana.pool.poolsynchronization` +======================================== + +.. automodule:: sardana.pool.poolsynchronization + +.. rubric:: Classes + +.. hlist:: + :columns: 3 + + * :class:`SynchronizationDescription` + * :class:`PoolSynchronization` + +PoolSynchronization +------------------- + +.. inheritance-diagram:: PoolSynchronization + :parts: 1 + +.. autoclass:: PoolSynchronization + :show-inheritance: + :members: + :undoc-members: + +SynchronizationDescription +-------------------------- + +.. inheritance-diagram:: SynchronizationDescription + :parts: 1 + +.. autoclass:: SynchronizationDescription + :show-inheritance: + :members: + :undoc-members: diff --git a/doc/source/devel/howto_controllers/howto_controller.rst b/doc/source/devel/howto_controllers/howto_controller.rst index 499a288e1c..83b499fe8d 100644 --- a/doc/source/devel/howto_controllers/howto_controller.rst +++ b/doc/source/devel/howto_controllers/howto_controller.rst @@ -404,6 +404,33 @@ spock), sardana assignes the default value has no default value, if it is not specified by the user, sardana will complain and fail to create and instance of SpringfieldMotorController. +.. _sardana-controller-howto-change-default-interface: + +Changing default interface +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Elements instantiated from your controller will have a default interface +corresponding to the controller's type. For example a moveable will have a +*position* attribute or an experimental channel will have a *value* +attribute. However this default interface can be changed if necessary. + +For example, the default type of a moveable's *position* attribute ``float`` +can be changed to ``long`` if the given axis only allows discrete positions. +To do that simple override the +:class:`~sardana.pool.controller.Controller.GetAxisAttributes` where you can +apply the necessary changes. + +Here is an example of how to change motor's *position* attribute to ``long``: + +.. code-block:: python + + def GetAxisAttributes(self, axis): + axis_attrs = MotorController.GetAxisAttributes(self, axis) + axis_attrs = dict(axis_attrs) + axis_attrs['Position']['type'] = float + return axis_attrs + + .. _sardana-controller-howto-error-handling: Error handling diff --git a/doc/source/devel/howto_controllers/howto_countertimercontroller.rst b/doc/source/devel/howto_controllers/howto_countertimercontroller.rst index 8914262808..b435b55a1e 100644 --- a/doc/source/devel/howto_controllers/howto_countertimercontroller.rst +++ b/doc/source/devel/howto_controllers/howto_countertimercontroller.rst @@ -6,6 +6,12 @@ How to write a counter/timer controller ======================================= +.. important:: + Counter/timer controller :term:`API` was extended in SEP18_ but this is + still not documented in this chapter. Please check the said SEP for more + information about the additional :term:`API` or eventual changes. + + The basics ---------- @@ -225,6 +231,9 @@ this configuration (axis number) via the controller parameter ``timer`` and ``monitor``. The currently used acquisition mode is set via the controller parameter ``acquisition_mode``. +Controller may announce its default timer axis with the +:obj:`~sardana.pool.controller.Loadable.default_timer` class attribute. + .. _sardana-countertimercontroller-howto-advanced: Advanced topics @@ -512,3 +521,4 @@ and that there are no gaps in between them. .. _numpy: http://numpy.scipy.org/ .. _SPEC: http://www.certif.com/ .. _EPICS: http://www.aps.anl.gov/epics/ +.. _SEP18: http://www.sardana-controls.org/sep/?SEP18.md diff --git a/doc/source/devel/howto_controllers/howto_pseudocountercontroller.rst b/doc/source/devel/howto_controllers/howto_pseudocountercontroller.rst index 8a19f91cf4..3e04d3e419 100644 --- a/doc/source/devel/howto_controllers/howto_pseudocountercontroller.rst +++ b/doc/source/devel/howto_controllers/howto_pseudocountercontroller.rst @@ -37,6 +37,13 @@ These names are used when creating the controller instance and their order is important when writing the controller itself. Each controller will define its own roles. +.. note:: + + It is possible to omit the + :obj:`~sardana.pool.controller.PseudoCounterController.pseudo_counter_roles` + definition if the controller provides only one axis. The controller class + name will be assumed as the pseudo counter role. + The constructor does nothing apart of calling the parent class constructor but could be used to implement any necessary initialization. @@ -66,6 +73,31 @@ negative. The value close to the zero indicates the beam centered in the middle. Similarly behaves the horizontal pseudo counter. The total pseudo counter is the mean value of all the four sensors and indicates the beam intensity. +Changing default interface +-------------------------- + +Pseudo counters instantiated from your controller will have a default +interface, which among others, comprises the *value* attribute. This attribute +is feed with the result of the +:meth:`~sardana.pool.controller.PseudoCounterController.calc` method and by +default it expects values of ``float`` type and scalar shape. You can easily +:ref:`change the default interface `. +This way you could program a pseudo counter to obtain an image :term:`ROI` +of a :ref:`2D experimental channel `. + +Here is an example of how to change *value* attribute's shape to an image +and specify its maximum dimension of 1024 x 1024 pixels: + +.. code-block:: python + + def GetAxisAttributes(self, axis): + axis_attrs = PseudoCounterController.GetAxisAttributes(self, axis) + axis_attrs = dict(axis_attrs) + axis_attrs['Value'][Type] = ((float, ), ) + axis_attrs['Value'][MaxDimSize] = (1024, 1024) + return axis_attrs + + Including external variables in the calculation ----------------------------------------------- diff --git a/doc/source/devel/howto_controllers/index.rst b/doc/source/devel/howto_controllers/index.rst index 034f023086..34e093ebfb 100644 --- a/doc/source/devel/howto_controllers/index.rst +++ b/doc/source/devel/howto_controllers/index.rst @@ -6,7 +6,12 @@ Writing controllers =================== -This chapter provides the necessary information to write controllers in sardana. +This chapter provides the necessary information to write controllers in +sardana. + +Before writing a new controller you should check the `controller plugin +register `_. +There's a high chance that somebody already wrote the plugin for your hardware. An overview of the pool controller concept can be found :ref:`here `. diff --git a/doc/source/devel/howto_macros/macros_general.rst b/doc/source/devel/howto_macros/macros_general.rst index 963bb83bd1..e747771556 100644 --- a/doc/source/devel/howto_macros/macros_general.rst +++ b/doc/source/devel/howto_macros/macros_general.rst @@ -45,7 +45,7 @@ a macro class you can benefit from all advantages of object-oriented programming. This means that, in theory: - it would reduce the amount of code you need to write - - reduce the complexity of your code y by dividing it into small, + - reduce the complexity of your code by dividing it into small, reasonably independent and re-usable components, that talk to each other using only well-defined interfaces - Improvement of productivity by using easily adaptable pre-defined @@ -298,7 +298,11 @@ being a composed of four elements: - parameter name - parameter type - - parameter default value (None means no default value) + - parameter default value: + - ``None`` means no default value + - ``Optional`` means that + :ref:`the parameter value is optional ` + - parameter description Here is a list of the most common allowed parameter types: @@ -321,6 +325,44 @@ The complete list of types distributed with sardana is made up by these five simple types: ``Integer``, ``Float``, ``Boolean``, ``String``, ``Any``, plus all available sardana interfaces (:obj:`~sardana.sardanadefs.Interface`) +.. _sardana-macro-optional-parameters: + +Optional parameters +~~~~~~~~~~~~~~~~~~~ + +A special parameter default value is the ``Optional`` keyword. A parameter +whose default value is set to ``Optional`` behaves just as one with a default +value, except that if the user does not provide a value explicitly, the +handling of its value is deferred to the run method (which gets ``None`` as +the parameter value). This allows for more complex handling of the value +(e.g. interactive prompting to the user, system introspection, reading from +files, etc.) + +So, here is an example how to define and use the optional parameter:: + + from sardana.macroserver.macro import Macro, Type, Optional + + class count(Macro): + + param_def = [ + ['itime', Type.Float, 1, 'integration time'], + ['mntgrp', Type.MeasurementGroup, Optional, 'MntGrp to use'] + ] + + def run(self, itime, mntgrp): + bkp_active_mntgrp = None + try: + if mntgrp is not None: + bkp_active_mntgrp = self.getEnv('ActiveMntGrp') + mntgrp_name = mntgrp.name + self.setEnv('ActiveMntGrp', mntgrp_name) + self.info('Use "{0}" measurement group'.format(mntgrp_name)) + self.ct(itime) + finally: + if bkp_active_mntgrp is not None: + self.setEnv('ActiveMntGrp', bkp_active_mntgrp) + + .. _sardana-macro-repeat-parameters: Repeat parameters @@ -669,7 +711,7 @@ parameters with different *flavors*: Accessing macro data -~~~~~~~~~~~~~~~~~~~~ +-------------------- Sometimes it is desirable to access data generated by the macro we just called. For these cases, the Macro :term:`API` provides a pair of low level methods @@ -778,7 +820,7 @@ prepare HelloWorld to run only after year 1989: def run(self): print "Hello, World!" -.. _sardana-macro-using-external-libraries: +.. _sardana-macro-handling-macro-stop-and-abort: Handling macro stop and abort ----------------------------- @@ -825,6 +867,64 @@ 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-adding-hooks-support: + +Adding hooks support +-------------------- + +Your macros may accept to :ref:`attach an arbitrary ` +code, a simple Python callable or even another macro, that will be executed +at given places. In Sardana this code are called *hooks*, and the places are +called *hook places*. + +In order to allow attaching hooks to your macro you must :ref:`write you +macro as a class ` while at the same time +inheriting from the :class:`~sardana.macroserver.macro.Hookable` class. + +The hook places can be defined in the ``hints`` class member dictionary with +the ``allowsHooks`` key and a tuple of strings with the hook places +identifiers:: + + class hookable_macro(Macro, Hookable): + """A macro that accepts and executes hooks.""" + + hints = {"allowsHooks": ("hook-place", "another-hook-place")} + + def run(self): + for hook in self.getHooks("hook-place"): + hook() + self.info("In between hook places") + for hook in self.getHooks("another-hook-place"): + hook() + +Hooks can be programmatically attached to a macro before its execution either +using the :attr:`~sardana.macroserver.macro.Hookable.hooks` property or +using the :meth:`~sardana.macroserver.macro.Hookable.appendHook` method:: + + def hook_function(): + pass + + class wrapping_macro(Macro): + """A wrapping macro that attaches hooks to a hookable macro + and executes it.""" + + def run(self): + hookable_macro, _ = self.createMacro("hookable_macro") + hook_macro = ExecMacroHook(self, "mv", [["mot01", 1]]) + hookable_macro.hooks = [(hook_macro, ["hook-place"])] + hookable_macro.appendHook((hook_function, ["another-hook-place"])) + self.runMacro(hookable_macro) + +.. note:: Be aware of the following difference between setting the + :attr:`~sardana.macroserver.macro.Hookable.hooks` property and using the + :meth:`~sardana.macroserver.macro.Hookable.appendHook` method. + Setting the property applies all hooks at once but may override + :ref:`general hooks` eventually attached to + the macro. Using the method appends just one hook but does not affect + the general hooks eventually attached to the macro. + +.. _sardana-macro-using-external-libraries: + Using external python libraries ------------------------------- diff --git a/doc/source/devel/overview/overview_pseudocounter.rst b/doc/source/devel/overview/overview_pseudocounter.rst index a9ac88663f..a036e5966b 100644 --- a/doc/source/devel/overview/overview_pseudocounter.rst +++ b/doc/source/devel/overview/overview_pseudocounter.rst @@ -6,24 +6,26 @@ Pseudo counter overview ======================= -Pseudo counter acts like an abstraction layer for a counter or a set of -counters allowing the user to see the experiment results by means of an -interface which is more meaningful to him. +Pseudo counter acts like an abstraction layer for an experimental channel +(counter/timer, 0D, 1D or 2D) or a set of them allowing the user to see the +experiment results by means of an interface which is more meaningful to him. +Pseudo counters can be even build on top of another pseudo counters. One example of a pseudo counter is :class:`~sardana.pool.poolcontrollers.IoverI0` useful for normalizing the measurement results in order to make them comparable. -In order to translate the counter values into the pseudo counter values, -calculations have to be performed. The device pool provides +In order to translate the experimental channel values into the pseudo counter +values, calculations have to be performed. The device pool provides :class:`~sardana.pool.controller.PseudoCounterController` class that can be overwritten to provide new calculations. The pseudo counter value gets updated automatically every time one of its -counters value gets updated e.g. when the acquisition is in progress. +experimental channel values gets updated e.g. when the acquisition is in +progress. -Each pseudo counter is represented by a Tango_ device whose interface allows to -obtain a calculation result (scalar value). +Each pseudo counter is represented by a Tango_ device whose interface allows +to obtain a calculation result (scalar value). .. seealso:: diff --git a/doc/source/glossary.rst b/doc/source/glossary.rst index a44d300aba..fb0a9faa37 100644 --- a/doc/source/glossary.rst +++ b/doc/source/glossary.rst @@ -420,6 +420,10 @@ Glossary dial See :term:`dial position` + + ROI + *Region of interest* are samples within a data set identified for a + particular purpose. .. _plug-in: http://en.wikipedia.org/wiki/Plug-in_(computing) .. _CCD: http://en.wikipedia.org/wiki/Charge-coupled_device diff --git a/doc/source/sep/SEP18.md b/doc/source/sep/SEP18.md new file mode 100644 index 0000000000..3defa7c21e --- /dev/null +++ b/doc/source/sep/SEP18.md @@ -0,0 +1,311 @@ + Title: Extend acquisition and synchronization concepts for SEP2 needs. + SEP: 18 + State: ACCEPTED + Reason: + New acquisition and synchronization concepts are necessary in order to + properly integrate 1D and 2D experimental channels in Sardana (SEP2). + Date: 2018-05-30 + Drivers: Zbigniew Reszela + URL: https://github.com/reszelaz/sardana/blob/sep18/doc/source/sep/SEP18.md + License: http://www.jclark.com/xml/copying.txt + Abstract: + SEP6 defined some acquisition and synchronization concepts for the needs + of the continuous scan. When working on the 1D and 2D experimental + channels integration we see that more concepts are necessary. This SEP + introduces them. + +Terminology +----------- + +* Measurement - a measurement process that may, but not necessarily, involve +multiple acquisitions e.g. measurement group count, continuous scan, step scan. +* Acquisition - a single acquisition e.g. over an integration time, which is a +part of a measurement + +Current Limitations +------------------- + +* The Software and Hardware synchronization types terminology is not self +descriptive. Internal and External terminology describes better these types. +* Some hardware or even the Lima library allows some synchronization modes +that are not foreseen in Sardana and are necessary. +* Some experimental channels could benefit from preparing them for multiple +acquisitions before the measurement e.g. software synchronized continuous +scan or step scan. + +Objectives +---------- + +Overcome the above limitations. + +Design +------ +1. Use of the measurement group will change: + * From now on, in order to start the measurement group, it is mandatory + to prepare it. + * The measurement group is prepared for a number of starts. The + preparation will expire whenever all starts gets called or in case of + stop/abort. Afterwards measurement group requires another preparation. + **IMPORTANT**: Whenever we drop backwards compatibility explained in the + following point starting of the measurement group without prior + preparation will be considered as wrong usage and will cause exception. + This will break step scans with attached hooks which measure with the + same measurement group as used by the scan. + * Direct start of the measurement group (after prior configuration of + the integration time or synchronization) will be supported as backwards + compatibility and the corresponding warning will be logged. +2. Allow different types of preparation of channels - this still depends on +the option selected in the implementation of controllers. The following +assumes option 1. + * Per measurement preparation with number of starts = n - `PrepareOne` + * Per acquisition preparation with repetitions = n - `Load(One|All)` +3. Extend AcqSynch with two new options: + * SoftwareStart (which means internal start) + * HardwareStart (which means external start) +4. Extend AcqSynchType with one new option (supported from expconf): + * Start +5. Modify acquisition actions (and synchronization action if necessary) so +they support the new concepts added in points 2 and 3. +6. *Extend Generic Scan Framework* (GSF), more precisely scan in step mode +with measurement preparation (number of starts = n) if possible i.e. scan +macro knows beforehand the number of points. +7. Document well that: + * Software(Trigger|Gate) are synonyms to Internal (Trigger|Gate). + Internal means that Sardana will synchronize the acquisitions. + * Hardware(Trigger|Gate) are synonyms to External(Trigger|Gate). + External means that an external to Sardana object (could be hardware) + will synchronize the acquisitions. + +Implementation +-------------- + +### GSF + +* `SScan` (step scan) implemented according to the following pseudo code: + * If number of points is known: + * `prepare(synchronization, starts=n)` where synchronization + contains the integration time and n means number of scan points + * `for step in range(n): count_raw()` + * If number of points is unknown: + * `while new_step: count()` +* `CTScan` (continuous scan) does not require changes, it simply calls +`count_continuous` + +### Measurement Group + +Measurement group is extended by the *prepare* command (with no arguments) +and *number of starts* attribute. The use of the attribute is optional and +it indicates how many times measurement group will be started (with the +*start* command) to measure according to the synchronization description or +integration time. When it is not used number of starts of 1 will be assumed. + +1. Measurement group - Tango device class + * Add `Prepare` command. + * Add `NbStarts` (`DevLong`) attribute. +2. Measurement group - core class + * Add `prepare()` method. + * Add `nr_of_starts` property. +3. Backwards compatibility for using just the integration time attribute with +the start command (without calling prepare command in-between) will be +solved in the following way: start command will internally call the prepare. +4. Measurement group - Taurus extension + * Add `prepare()` method which simply maps to `Prepare` Tango command + * Add `count_raw()` method according to the following pseudo code: + * `start()` + * `waitFinish()` + * Implement `count(integration_time)` method according to the following + pseudo + code: + * set `integration_time` + * set `nr_of_starts=1` + * `prepare()` + * `count_raw()` + * Implement `count_continuous` (previous `measure`) method according to + the following pseudo code: + * `prepare()` where synchronization may + contain the continuous acquisition description + * `subscribeValueBuffer()` + * `count_raw()` + * `unsubscribeValueBuffer()` + +### Software synchronizer + +* Add `start` and `end` events. Start is emitted before the first `active` +event and end is emitted after the last `passive` event. + +### Acquisition actions + +* Add `PoolAcquisitionSoftwareStart` action that will start channels on +software synchronizer `start` event. +* `PoolAcquisitionSoftware` will stop channels on software synchronizer +`end` event. + +### Controllers + +C/T, 1D and 2D controllers (plugins) API is extended as follows: + +* Add `PrepareOne(axis, value, repeats, latency, starts)` +to the Loadable interface +* Add extra argument to `LoadOne`, etc. methods of the `Loadable` interface +`latency_time`: `LoadOne(axis, value, repeats, latency)` + +This maintains backwards compatibility. + +The following examples demonstrates the sequence of calls (only the ones +relevant to the SEP18) of one channel (axis 1) involved in the given +acquisition. This channel is at the same time the timer. + +* **step scan** 5 acquisitions x 0.1 s of integration time +```python +PrepareOne(1, 0.1, 1, 0, 5) +for acquisition in range(5): + LoadOne(1, 0.1, 1, 0) + StartOne(1) +``` + +* **continuous scan (hw trigger)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +PrepareOne(1, 0.1, 5, 0.05, 1) # latency time can be ignored +LoadOne(1, 0.1, 5, 0.05) # latency time can be ignored +StartOne(1) +``` + +* **continuous scan (sw trigger)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +PrepareOne(1, 0.1, 1, 0.05, 5) # latency time can be ignored +for trigger in range(5): + LoadOne(1, 0.1, 1, 0.05) # latency time can be ignored + StartOne(1) +``` + +* **continuous scan (hw gate)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +PrepareOne(1, 0.1, 5, 0.05, 1) # integration time and latency time can be ignored +LoadOne(1, 0.1, 5, 0.05) # integration time and latency time can be ignored +StartOne(1) +``` + +* **continuous scan (sw gate)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +PrepareOne(1, 0.1, 1, 0.05, 5) +for gate in range(5): + LoadOne(1, 0.1, 1, 0.05) # integration time and latency time can be ignored + StartOne(1) +``` +* **continuous scan (hw start)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +PrepareOne(1, 0.1, 5, 0.05, 1) +LoadOne(1, 0.1, 5, 0.05) +StartOne(1) +``` + +* **continuous scan (sw start)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +PrepareOne(1, 0.1, 5, 0.05, 1) +LoadOne(1, 0.1, 5, 0.05) +StartOne(1) +``` + +See *Appendix 1* and *Appendix 2* for alternative options which were finally +**not selected** for additional controllers API. + +### Dummy C/T controller +Implement `SoftwareStart` and `HardwareStart` in the +`DummyCounterTimerController` - minimal implementation. + +## Follow-up actions (TODOs) + +* **Stop channels using software trigger/gate on software synchronizer's `end` +event.** This is necessary since we introduced the preparation of channels, +and they may be aware in how many software trigger/gates they will +participates. Sardana does not guarantee to acquire on all triggers/gates, +for example if the previous acquisition is still in progress and a new +trigger/gate should be issued, and some channels may be waiting for them - +this is the case of Lima. + +* **Documentation**: + * Update [Measurement group API reference](https://sardana-controls.org/devel/api/api_measurementgroup.html#sardana-measurementgroup-api) + * Update [How to write a counter/timer controller](https://sardana-controls.org/devel/howto_controllers/howto_countertimercontroller.html) + or any other timerable controller + +* **Remove `StartMultiple` Tango command** and its underneath core +implementation. + +## Appendix 1 - alternative option 2 for extending controllers API + +* Add extra arguments to `LoadOne`, etc. methods of the `Loadable` interface +`latency` and `nr_of_starts` and switch the order of arguments so the API is: +`LoadOne(axis, value, latency, repeats, nr_of_starts)`. +* Make the `LoadOne`, etc. be called only once per measurement, in the +measurement group prepare command. + +This option **breaks** backwards compatibility. + +The following examples demonstrates the sequence of calls (only the ones +relevant to the SEP18) of one channel (axis 1) involved in the given +acquisition. This channel is at the same time the timer. + +* **step scan** 5 acquisitions x 0.1 s of integration time +```python +LoadOne(1, 0.1, 0, 1, 5) +for acquisition in range(5): + StartOne(1) +``` + +* **continuous scan (hw trigger)** 5 acquisitions x 0.1 s of integration +time and 0.05 s of latency time +```python +LoadOne(1, 0.1, 0.05, 5, 1) # latency time can be ignored +StartOne(1) +``` + +* **continuous scan (sw trigger)** +```python +LoadOne(1, 0.1, 0.05, 1, 5) # latency time can be ignored +for trigger in range(5): + StartOne(1) +``` + +* **continuous scan (hw gate)** +```python +LoadOne(1, 0.1, 0.05, 5, 1) # integration time and latency time can be ignored +StartOne(1) +``` + +* **continuous scan (sw gate)** +```python +LoadOne(1, 0.1, 0.05, 1, 5) # integration time and latency time can be ignored +for gate in range(5): + StartOne(1) +``` + +* **continuous scan (hw start)** +```python +LoadOne(1, 0.1, 0.05, 5, 1) +StartOne(1) +``` + +* **continuous scan (sw start)** +```python +LoadOne(1, 0.1, 0.05, 5, 1) +StartOne(1) +``` + +## Appendix 2 - alternative option 3 for extending controllers API + +The same as option 2 but maintaining the backwards compatibility in the +following way: +* Acquisition actions will call the `LoadOne`, etc. methods depending on the +controllers implementations (more precisely using the `inspect.getargspec` +and counting the number of arguments). This will require much more complicated +acquisition actions. + +## Changes + +- 2018-12-17 [reszelaz][]. CANDIDATE -> ACCEPTED diff --git a/doc/source/sep/index.md b/doc/source/sep/index.md index a293b472c5..584a57fccc 100644 --- a/doc/source/sep/index.md +++ b/doc/source/sep/index.md @@ -12,7 +12,7 @@ Proposals list ------------| --------- | --------------------------------------------------------- [SEP0][] | OBSOLETE | Introducing Sardana Enhancement Proposal [SEP1][] | OBSOLETE | Reorganization of code repos - [SEP2][] | DRAFT | Lima integration + [SEP2][] | CANDIDATE | Improve integration of 1D and 2D experimental channels [SEP3][] | REJECTED (handled in [#297][]) | Adapt to [TEP3][] (Tango-independent taurus.core) [SEP4][] | ACCEPTED | HKL integration [SEP5][] | ACCEPTED | Implementation of tests infrastructure @@ -26,13 +26,16 @@ Proposals list [SEP13][] | REJECTED (moved to [TEP13][]) | Unified plugins support in Taurus & Sardana [SEP14][] | DRAFT | MSENV taurus schema [SEP15][] | ACCEPTED | Moving Sardana to Github + [SEP16][] | DRAFT | Plugins (controllers, macros, etc.) register + [SEP17][] | DRAFT | Ongoing acquisition formalization and implementation + [SEP18][] | ACCEPTED | Extend acquisition and synchronization concepts for SEP2 needs [SEP0]: http://www.sardana-controls.org/sep/?SEP0.md [SEP1]: http://www.sardana-controls.org/sep/?SEP1.md -[SEP2]: http://www.sardana-controls.org/sep/?SEP2.md +[SEP2]: https://github.com/reszelaz/sardana/blob/sep2/doc/source/sep/SEP2.md [SEP3]: http://www.sardana-controls.org/sep/?SEP3.md [SEP4]: http://www.sardana-controls.org/sep/?SEP4.md [SEP5]: http://www.sardana-controls.org/sep/?SEP5.md @@ -46,6 +49,9 @@ Proposals list [SEP13]: http://www.sardana-controls.org/sep/?SEP13.md [SEP14]: http://www.sardana-controls.org/sep/?SEP14.md [SEP15]: http://www.sardana-controls.org/sep/?SEP15.md +[SEP16]: https://github.com/reszelaz/sardana/blob/sep16/doc/source/sep/SEP16.md +[SEP17]: https://github.com/reszelaz/sardana/blob/sep17/doc/source/sep/SEP17.md +[SEP18]: http://www.sardana-controls.org/sep/?SEP18.md [TEP3]: http://www.taurus-scada.org/tep/?TEP3.md diff --git a/doc/source/sep/res/sep0_workflow.png b/doc/source/sep/res/sep0_workflow.png old mode 100755 new mode 100644 diff --git a/doc/source/sep/res/sep6_motion.bmp b/doc/source/sep/res/sep6_motion.bmp old mode 100755 new mode 100644 diff --git a/doc/source/users/adding_elements.rst b/doc/source/users/adding_elements.rst index 2d677d2d45..dc9aff1b4d 100644 --- a/doc/source/users/adding_elements.rst +++ b/doc/source/users/adding_elements.rst @@ -14,9 +14,21 @@ If not, you can :ref:`write the plugin yourself `. Controllers =========== -The controller is a Sardana object that handles the communication with the hardware. -To create the controller you can use :class:`~sardana.macroserver.macros.expert.defctrl` -macro:: +The controller is a Sardana object that handles the communication with the +hardware (physical controller) or provides an abstraction layer (pseudo +controller). + +Before creating the controller instance you need to load the controller +plugin class into the Sardana. To check if it is already loaded use the +:class:`~sardana.macroserver.macros.lists.lsctrl` macro. If it is not, you will +need to configure the :ref:`controller plugin discovery path ` +(``PoolPath`` property) and either restart the Sardana server or call the +:class:`~sardana.macroserver.macros.expert.addctrllib` macro. After that +check again with the list macro if the controller class is present and if +yes let's continue... + +To create a controller instance you can use +:class:`~sardana.macroserver.macros.expert.defctrl` macro:: defctrl diff --git a/doc/source/users/configuration/macroserver.rst b/doc/source/users/configuration/macroserver.rst index 3ce5a6142b..a7b25979c3 100644 --- a/doc/source/users/configuration/macroserver.rst +++ b/doc/source/users/configuration/macroserver.rst @@ -49,6 +49,26 @@ MacroServer integrates natively with the instance. In case Sardana is used with Tango this configuration is accessible via the ``LogstashHost`` and ``LogstashPort`` :class:`~sardana.tango.macroserver.MacroServer.MacroServer` device properties. +You can use the intermediate SQLite cache database configured with +``LogstashCacheDbPath`` property, however this is discouraged due to logging +performance problems. -.. todo:: - Document RConsolePort +You can debug the MacroServer at runtime using the Python remote +console - ``rconsole`` (part of the `rfoo `_ +project). First, you need to specify at which port the MacroServer will +accept connections. For that, simply set the ``RConsolePort`` +:class:`~sardana.tango.macroserver.MacroServer.MacroServer` property. +Second, in order to open a connection to a MacroServer just type:: + + $> rconsole -p + +The most convenient way to debug the MacroServer internals is to use the +`Tango Util `_ singleton object. It is used to store Tango device server +process data and to provide the user with a set of utility methods. +For example, to access the MacroServer Sardana core object, in the rconsole +session, just type:: + + >>> import tango + >>> util = tango.Util.instance() + >>> ms = util.get_device_by_name("").macro_server diff --git a/doc/source/users/configuration/pool.rst b/doc/source/users/configuration/pool.rst index c4f8ffba7b..1c86f59e00 100644 --- a/doc/source/users/configuration/pool.rst +++ b/doc/source/users/configuration/pool.rst @@ -20,6 +20,9 @@ Device Pool integrates natively with the instance. In case Sardana is used with Tango this configuration is accessible via the ``LogstashHost`` and ``LogstashPort`` :class:`~sardana.tango.pool.Pool.Pool` device properties. +You can use the intermediate SQLite cache database configured with +``LogstashCacheDbPath`` property, however this is discouraged due to logging +performance problems. .. todo:: diff --git a/doc/source/users/macro_hooks.rst b/doc/source/users/macro_hooks.rst index 6a5616905f..cb396e2dd6 100644 --- a/doc/source/users/macro_hooks.rst +++ b/doc/source/users/macro_hooks.rst @@ -11,30 +11,35 @@ Macro Hooks =========== A hook is an extra code that can be run at certain points of a macro execution. -These points are predefined for each hookable macro and passed via a "hints" mechanism. -The hint tells the macro how and when to run the attached hook. -Hooks allow the customization of already existing macros and can be added using -three different ways: +These points, called *hook places* are predefined for each *hookable* macro. +The hook place tells the macro how and when to run the attached hook. +Hooks allow the customization of already existing macros and can be added +using three different ways: -- General Hooks +- :ref:`General Hooks ` - :ref:`Sequencer Hooks ` -- :ref:`Programmatic Hooks ` +- :ref:`Programmatic Hooks ` All available macros can be used as a hook. - + +.. _sardana-macros-hooks-general: + General Hooks ------------- The general hooks were implemented in Sardana after the programmatic hooks. The motivation for this implementation was to allow the customization -of the scan macros without having to redefine them. -The general hooks apply to all hookable macros and allow the definition -of new hints. -They can be controlled using dedicated macros: :class:`~sardana.macroserver.macros.env.lsgh`, -:class:`~sardana.macroserver.macros.env.defgh` and :class:`~sardana.macroserver.macros.env.udefgh`. -For each hook position, hint, several hooks can be run, they will be run in the -order they were added. The same hook can be run several times in the same position -if it's added several times. +of the scan macros without having to redefine them. The general hooks apply +to all hookable macros. + +They can be controlled using dedicated macros: +:class:`~sardana.macroserver.macros.env.lsgh`, +:class:`~sardana.macroserver.macros.env.defgh` and +:class:`~sardana.macroserver.macros.env.udefgh`. +For each hook place, several hooks can be attached, they will be run in the +order they were added. The same hook can be run several times in the same +place if it was added several times. + Examples: diff --git a/doc/source/users/spock.rst b/doc/source/users/spock.rst old mode 100755 new mode 100644 index 43c43303a8..f974ce48f0 --- a/doc/source/users/spock.rst +++ b/doc/source/users/spock.rst @@ -492,24 +492,127 @@ case a macro fails when being executed. Valid values are ``output``, ``critical``, ``error``, ``warning``, ``info``, ``debug`` and ``result``. -*Spock syntax* and *Advanced spock syntax* ------------------------------------------- - -*Spock syntax* is based on space separated list of parameter values. Not all macros -are allowed to be used with the spock syntax. Restrictions appear for those macros -using :ref:`repeat parameters ` as argument. The -*Spock syntax* would not allow: - -1. macros defining more than one repeat parameter -2. macros defining repeat parameter which is not at the end of the parameters definition -3. macros defining nested repeat parameters - -To overcome these restrictions an *Advanced spock syntax* was developed, this syntax introduces the -use of square brackets to group the repeat parameters and its repetitions. -The *Spock Syntax* was extended for the cases 1 and 2 in case only one repetion of the repeat -parameter is needed, this extension assumes that the parameter values passed by the user are a single -repetition of the repeat parameter. -A set of macro examples using both syntaxes can be found in :ref:`sardana-devel-macro-parameter-examples`. +Spock syntax +------------ + +*Spock syntax* is used to execute macros. It is based on space +separated list of parameter values. If the string parameter values contain +spaces itself these **must** be enclosed in quotes, either single quotes +``''`` or double quotes ``""``. + +The spock syntax was extended with the use of square brackets ``[]`` for +macros which define +:ref:`repeat parameters ` as arguments. +Repeat parameter values must be enclosed in square brackets. If the repeat +parameter is composed from more than one internal parameter its every +repetition must be enclosed in another square brackets as well. + +For example, the ``move_with_timeout`` macro:: + + class move_with_timeout(Macro): + """Execute move with a timeout""" + + param_def = [ + ['m_p_pair', + [['motor', Type.Motor, None, 'Motor to move'], + ['pos', Type.Float, None, 'Position to move to']], + None, 'List of motor/position pairs'], + ['timeout', Type.Float, None, 'Timeout value'] + ] + + def run(self, *args, **kwargs): + pass + +Must use the square brackets for the ``m_p_pair`` parameter and its +repeats: + +.. sourcecode:: spock + + Door_1 [1]: move_with_timeout [[th 8.4] [tth 16.8]] 50 + +However for the commodity reasons the square brackets may be skipped. The +following examples explain in which cases. + +Repeat parameter is the last one +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the repeat parameter is the last one in the parameters definition +both square brackets (for the repeat parameter and for the repetition) may +be skipped. + +For example, the ``move`` macro:: + + class move(Macro): + """Execute move""" + + param_def = [ + ['m_p_pair', + [['motor', Type.Motor, None, 'Motor to move'], + ['pos', Type.Float, None, 'Position to move to']], + None, 'List of motor/position pairs'] + ] + + def run(self, *args, **kwargs): + pass + +May skip the square brackets for the ``m_p_pair`` parameter and its +repeats: + +.. sourcecode:: spock + + Door_1 [1]: move th 8.4 tth 16.8 + +This is equivalent to: + +.. sourcecode:: spock + + Door_1 [1]: move [[th 8.4] [tth 16.8]] + +Repeat parameter has only one internal parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the repeat parameter contains only one internal parameter the square +brackets for the repetition **must** be skipped. + +For example, the ``power_motor`` macro:: + + class power_motor(Macro): + """Power on/off motor(s)""" + + param_def = [ + ['motor_list', [['motor', Type.Motor, None, 'motor name']], + None, 'List of motors'], + ['power_on', Type.Boolean, None, 'motor power state'] + ] + + def run(self, *args, **kwargs): + pass + +Must use the square brackets for the ``motor_list`` parameter but not for +its repeats: + +.. sourcecode:: spock + + Door_1 [1]: power_motor [th tth] True + +Repeat parameter has only one internal parameter and only one repetition value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When the repeat parameter contains only one internal parameter and you +would like to pass only one repetition value then the square brackets for +the repeat parameter may be skipped as well resulting in no square brackets +being used. + +This assumes the ``power_motor`` macro from the previous example. +The following two macro executions are equivalent: + +.. sourcecode:: spock + + Door_1 [1]: power_motor th True + Door_1 [2]: power_motor [th] True + +A set of macro examples defining complex repeat parameters can be found in +:ref:`sardana-devel-macro-parameter-examples`. You can see the invocation example for each of these macros in its docstring. diff --git a/doc/source/users/standard_macro_catalog.rst b/doc/source/users/standard_macro_catalog.rst index 35e7cf22b3..1d5a7207f7 100644 --- a/doc/source/users/standard_macro_catalog.rst +++ b/doc/source/users/standard_macro_catalog.rst @@ -202,3 +202,11 @@ scan macros * :class:`~sardana.macroserver.macros.scan.d2scanct` * :class:`~sardana.macroserver.macros.scan.d3scanct` * :class:`~sardana.macroserver.macros.scan.d4scanct` + +scan related macros +------------------- + +.. hlist:: + :columns: 5 + + * :class:`~sardana.macroserver.macros.standard.newfile` diff --git a/doc/source/users/taurus/experimentconfiguration.rst b/doc/source/users/taurus/experimentconfiguration.rst index 6dd31828b5..5d75849511 100644 --- a/doc/source/users/taurus/experimentconfiguration.rst +++ b/doc/source/users/taurus/experimentconfiguration.rst @@ -9,7 +9,7 @@ Experiment Configuration user interface .. contents:: -Experiment Configuration widget a.k.a. expconf is a complete interface to +Experiment Configuration widget a.k.a. ``expconf`` is a complete interface to define the experiment configuration. It consists of three main groups of parameters organized in tabs: @@ -21,8 +21,23 @@ The parameters may be modified in an arbitrary order, at any of the tabs, and will be maintained as pending to apply until either applied or reset by the user. +.. important:: + While editing configuration in the ``expconf`` widget, the experiment + configuration on the server may have changed, for example, another + ``expconf`` instance applied changes or a running macro changed it + programmatically. This is notified to the user with a pop-up dialog + offering the user to either keep the local version of the experiment + configuration or to load the new configuration from the server. Be aware that + the second option will **override all your local changes**. It is also + possible to use the ``expconf`` widget in *slave* mode and automatically + update on the server changes. You can enable/disable the "Auto update" mode + from the context menu. + + This widget is usually present in sardana-aware Taurus GUIs and is also invoked -by the `expconf` command in :ref:`Spock` +by the ``expconf`` command in :ref:`Spock`. + + .. _expconf_ui_measurementgroup: diff --git a/doc/source/users/taurus/macrobutton.rst b/doc/source/users/taurus/macrobutton.rst old mode 100755 new mode 100644 diff --git a/sardanaConfig/src/wizards/pool_page.py b/sardanaConfig/src/wizards/pool_page.py index d77ebd3936..4bffa6ede2 100644 --- a/sardanaConfig/src/wizards/pool_page.py +++ b/sardanaConfig/src/wizards/pool_page.py @@ -149,7 +149,7 @@ def nextId(self): def main(): tg_host, sardana = None, None - if len(sys.argv) > 1: + if len(sys.argv) == 2: tg_host = sys.argv[1] if len(sys.argv) > 2: sardana = sys.argv[2] diff --git a/setup.py b/setup.py index af32f712f3..43d54bc37c 100644 --- a/setup.py +++ b/setup.py @@ -55,8 +55,10 @@ def get_release_info(): # when using PyTango < 9 the dependency is >= 0.0.1 and < 0.1.0 # when using PyTango >= 9 the dependency is >= 0.1.6 'itango (>=0.0.1)', - # for Taurus3 requires >= 3.7.5; for Taurus4 requires >= 4.4.0 - 'taurus (>= 3.7.5)', + # for Taurus3 requires >= 3.12 (special version from + # taurus-org/taurus@3.x-sdn2.5.1 branch) + # for Taurus4 requires >= 4.5.0 + 'taurus (>= 3.12)', 'lxml (>=2.1)', # ordereddict is necessary for Python < 2.6 'ordereddict' @@ -65,8 +67,10 @@ def get_release_info(): install_requires = [ 'PyTango>=7.2.3', 'itango>=0.0.1', - # for Taurus3 requires >= 3.7.5; for Taurus4 requires >= 4.4.0 - 'taurus>=3.7.5,!=4.0.0,!=4.0.1,!=4.0.3,!=4.1.0,!=4.1.1,!=4.3.0,!=4.3.1', + # for Taurus3 requires >= 3.10 (special version from + # taurus-org/taurus@3.x-sdn2.5.1 branch) + # for Taurus4 requires >= 4.5.0 + 'taurus>=3.12,!=4.0,!=4.1,!=4.3,!=4.4', 'lxml>=2.1' ] diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py index 1fd5d99e33..2e6a41cc7f 100644 --- a/src/sardana/macroserver/macro.py +++ b/src/sardana/macroserver/macro.py @@ -32,7 +32,7 @@ __all__ = ["OverloadPrint", "PauseEvent", "Hookable", "ExecMacroHook", "MacroFinder", "Macro", "macro", "iMacro", "imacro", "MacroFunc", "Type", "ParamRepeat", "Table", "List", "ViewOption", - "LibraryError"] + "LibraryError", "Optional"] __docformat__ = 'restructuredtext' @@ -55,7 +55,8 @@ from sardana.sardanadefs import State from sardana.util.wrap import wraps -from sardana.macroserver.msparameter import Type, ParamType, ParamRepeat +from sardana.macroserver.msparameter import Type, ParamType, ParamRepeat, \ + Optional from sardana.macroserver.msexception import StopException, AbortException, \ MacroWrongParameterType, UnknownEnv, UnknownMacro, LibraryError from sardana.macroserver.msoptions import ViewOption @@ -229,57 +230,67 @@ def appendHook(self, hook_info): except KeyError: self._hookHintsDict[hint] = [hook] - @propertx - def hooks(): - def get(self): - return self._getHooks() + @property + def hooks(self): + """Hooks (callables) attached to the macro object together with the + hook places (places where they will be called). + + :getter: Return all hooks attached to the macro object (including + general hooks). + :setter: Set hooks to the object. **This may override eventual + general hooks.** + Use :meth:`~sardana.macroserver.macro.Hookable.appendHook` + if the general hooks want to be kept. For backwards compatibility + accepts hook in the :obj:`list`\ format. + :type: :obj:`list`\<:obj:`tuple`\> where each tuple has two + elements: callable and :obj:`list`\<:obj:`str`\> + """ # noqa + return self._getHooks() + + @hooks.setter + def hooks(self, hooks): + """Sets hooks. Internally two variables instance members are created: + + - _hooks (list>) (will be a tuple regardless of + what was passed) + - _hookHintsDict (dict) a dict of key=hint and value=list + of hooks with that hint. _hookHintsDict also stores two special + keys: "_ALL_": which contains all the hooks "_NOHINTS_": which + contains the hooks that don't provide hints - def set(self, hooks): - '''hooks must be list>. Exceptionally, for - backwards compatibility, list is also admitted, but may - not be supported in the future. - "two variables are created: - - self._hooks (list>) (will be a tuple - regardless of what was passed) - - self._hookHintsDict (dict) a dict of key=hint and - value=list of hooks with that hint. - self._hookHintsDict also stores two - special keys: "_ALL_": which contains all - the hooks "_NOHINTS_": which contains the - hooks that don't provide hints - ''' - if not isinstance(hooks, list): - self.error( - 'the hooks must be passed as a list>') - return - - # store self._hooks, making sure it is of type: - # list> - self._hooks = [] - for h in hooks: - if isinstance(h, (tuple, list)) and len(h) == 2: - self._hooks.append(h) - else: # we assume that hooks is a list - self._hooks.append((h, [])) - self.info( - 'Deprecation warning: hooks should be set with a list of hints. See Hookable API docs') - - # delete _hookHintsDict to force its recreation on the next access - if hasattr(self, '_hookHintsDict'): - del self._hookHintsDict - # create _hookHintsDict - self._getHookHintsDict()['_ALL_'] = zip(*self._hooks)[0] - nohints = self._hookHintsDict['_NOHINTS_'] - for hook, hints in self._hooks: - if len(hints) == 0: - nohints.append(hook) - else: - for hint in hints: - try: - self._hookHintsDict[hint].append(hook) - except KeyError: - self._hookHintsDict[hint] = [hook] - return get, set + """ + if not isinstance(hooks, list): + self.error( + 'the hooks must be passed as a list>') + return + + # store self._hooks, making sure it is of type: + # list> + self._hooks = [] + for h in hooks: + if isinstance(h, (tuple, list)) and len(h) == 2: + self._hooks.append(h) + else: # we assume that hooks is a list + self._hooks.append((h, [])) + msg = ("Deprecation warning: hooks should be set with a" + " list of hints. See Hookable API docs") + self.info(msg) + + # delete _hookHintsDict to force its recreation on the next access + if hasattr(self, '_hookHintsDict'): + del self._hookHintsDict + # create _hookHintsDict + self._getHookHintsDict()['_ALL_'] = zip(*self._hooks)[0] + nohints = self._hookHintsDict['_NOHINTS_'] + for hook, hints in self._hooks: + if len(hints) == 0: + nohints.append(hook) + else: + for hint in hints: + try: + self._hookHintsDict[hint].append(hook) + except KeyError: + self._hookHintsDict[hint] = [hook] class ExecMacroHook(object): @@ -1185,7 +1196,7 @@ def prepareMacro(self, *args, **kwargs): self.execMacro('mv', th, 0) # backwards compatibility - see note # a sequence of parameters: - self.execMacro(['ascan', 'th', '0', '100', '10', '1.0') + 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)) diff --git a/src/sardana/macroserver/macros/discrete.py b/src/sardana/macroserver/macros/discrete.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/macros/examples/motion.py b/src/sardana/macroserver/macros/examples/motion.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/macros/examples/parameters.py b/src/sardana/macroserver/macros/examples/parameters.py index 486e5201a3..681aed83b8 100644 --- a/src/sardana/macroserver/macros/examples/parameters.py +++ b/src/sardana/macroserver/macros/examples/parameters.py @@ -23,7 +23,7 @@ """This module contains macros that demonstrate the usage of macro parameters""" -from sardana.macroserver.macro import * +from sardana.macroserver.macro import Macro, Type, ParamRepeat __all__ = ["pt0", "pt1", "pt2", "pt3", "pt3d", "pt4", "pt5", "pt6", "pt7", "pt7d1", "pt7d2", "pt8", "pt9", "pt10", "pt11", "pt12", "pt13", @@ -56,6 +56,19 @@ def run(self, f): pass +class pt1d(Macro): + """Macro with one float parameter with default value.. + Usage from Spock, ex.: + pt1d 1 + pt1d + """ + + param_def = [['value', Type.Float, None, 'some bloody float']] + + def run(self, f): + pass + + class pt2(Macro): """Macro with one Motor parameter: Each parameter is described in the param_def sequence as being a sequence of four elements: name, type, @@ -186,6 +199,7 @@ class pt7d1(Macro): pt7d1 mot1 1 mot2 3 Using default value, ex.: pt7d1 [[mot1] [mot2 3]] # at any repetition + pt7d1 mot1 # if only one repetition """ @@ -262,6 +276,7 @@ class pt10(Macro): parameter may be defined as first one. Usage from Spock, ex.: pt10 [1 3] mot1 + pt10 1 mot1 # if only one repetition """ param_def = [ @@ -279,6 +294,7 @@ class pt11(Macro): parameters. Usages from Spock, ex.: pt11 ct1 [1 3] mot1 + pt11 ct1 1 mot1 # if only one repetition """ param_def = [ @@ -296,6 +312,7 @@ class pt12(Macro): parameters may defined. Usage from Spock, ex.: pt12 [1 3 4] [mot1 mot2] + pt12 1 mot1 # if only one repetition for each repeat parameter """ param_def = [ diff --git a/src/sardana/macroserver/macros/expert.py b/src/sardana/macroserver/macros/expert.py index d5cd2dc960..2ef69de29f 100644 --- a/src/sardana/macroserver/macros/expert.py +++ b/src/sardana/macroserver/macros/expert.py @@ -25,13 +25,13 @@ from __future__ import print_function -__docformat__ = 'restructuredtext' - __all__ = ["addctrllib", "addmaclib", "commit_ctrllib", "defctrl", "defelem", "defm", "defmeas", "edctrlcls", "edctrllib", "prdef", "relctrlcls", "relctrllib", "rellib", "relmac", "relmaclib", "send2ctrl", "udefctrl", "udefelem", "udefmeas", "sar_info"] +__docformat__ = 'restructuredtext' + import sys import traceback import array @@ -339,6 +339,12 @@ def run(self, ctrl_library): class addctrllib(Macro): """Adds the given controller library code to the pool server filesystem. + + .. note:: Currently this macro does not report eventual errors, + for example Python syntax errors, in the controller plugin + module. So if it silently exits but the controller library is + not correctly loaded please check the server logs for more + information. """ param_def = [["ctrl_library_name", Type.String, None, diff --git a/src/sardana/macroserver/macros/hkl.py b/src/sardana/macroserver/macros/hkl.py index 3073a540b4..4dbfc4527f 100644 --- a/src/sardana/macroserver/macros/hkl.py +++ b/src/sardana/macroserver/macros/hkl.py @@ -19,7 +19,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Sardana. If not, see . ## -############################################################################# +############################################################################## """ Macro library containning diffractometer related macros for the macros @@ -27,14 +27,6 @@ """ -__all__ = ["addreflection", "affine", "br", "ca", "caa", "ci", "computeub", - "freeze", "getmode", "hklscan", "hscan", "kscan", "latticecal", - "loadcrystal", "lscan", "newcrystal", "or0", "or1", "orswap", - "pa", "savecrystal", "setaz", "setlat", "setmode", "setor0", - "setor1", "setorn", "th2th", "ubr", "wh"] - - - # TODO: use taurus instead of PyTango API e.g. read_attribute, # write_attribute. This module is full of PyTango centric calls. @@ -43,6 +35,11 @@ # using getDevice. However this getter seems to accept only the elements names # and not the full names. +__all__ = ["addreflection", "affine", "br", "ca", "caa", "ci", "computeub", + "freeze", "getmode", "hklscan", "hscan", "kscan", "latticecal", + "loadcrystal", "lscan", "newcrystal", "or0", "or1", "orswap", + "pa", "savecrystal", "setaz", "setlat", "setmode", "setor0", + "setor1", "setorn", "th2th", "ubr", "wh"] import time import math @@ -50,7 +47,7 @@ import re import numpy as np -from sardana.macroserver.macro import * +from sardana.macroserver.macro import Macro, iMacro, Type from sardana.macroserver.macros.scan import aNscan from sardana.macroserver.msexception import UnknownEnv @@ -114,7 +111,8 @@ def prepare(self): 'Chi': "chi", 'Phi': "phi", 'Tth': "tth"} elif self.nb_motors == 6: self.labelmotor = {'Mu': "mu", 'Theta': "omega", 'Chi': "chi", - 'Phi': "phi", 'Gamma': "gamma", 'Delta': "delta"} + 'Phi': "phi", 'Gamma': "gamma", + 'Delta': "delta"} prop = self.diffrac.get_property(['DiffractometerType']) for v in prop['DiffractometerType']: @@ -363,6 +361,23 @@ def run(self, H, K, L, Trajectory): str_pos[self.labelmotor["Omega"]], str_pos[self.labelmotor["Chi"]], str_pos[self.labelmotor["Phi"]])) + elif self.nb_motors == 7: + self.output("%10s %11s %12s %11s %11s %11s %11s" % + (self.angle_names[0], + self.angle_names[1], + self.angle_names[2], + self.angle_names[3], + self.angle_names[4], + self.angle_names[5], + self.angle_names[6])) + self.output("%10s %11s %12s %11s %11s %11s %11s" % + (str_pos[self.angle_names[0]], + str_pos[self.angle_names[1]], + str_pos[self.angle_names[2]], + str_pos[self.angle_names[3]], + str_pos[self.angle_names[4]], + str_pos[self.angle_names[5]], + str_pos[self.angle_names[6]])) @@ -416,18 +431,25 @@ class ci(Macro, _diffrac): ['phi', Type.Float, None, "Phi value"], ['gamma', Type.Float, -999, "Gamma value"], ['delta', Type.Float, -999, "Delta value"], + ['omega_t', Type.Float, -999, "Omega_t value"], ] - def prepare(self, mu, theta, chi, phi, gamma, delta): + def prepare(self, mu, theta, chi, phi, gamma, delta, omega_t): _diffrac.prepare(self) - def run(self, mu, theta, chi, phi, gamma, delta): + def run(self, mu, theta, chi, phi, gamma, delta, omega_t): if delta == -999 and self.nb_motors == 6: self.error("Six angle values are need as argument") + elif omega_t == -999 and self.nb_motors == 7: + msg = ("Seven angle values are need as argument (omega_t is " + "missed - last argument)") + self.error(msg) else: - - angles = [mu, theta, chi, phi, gamma, delta] + if self.nb_motors != 7: + angles = [mu, theta, chi, phi, gamma, delta] + else: + angles = [omega_t, mu, theta, chi, phi, gamma, delta] self.diffrac.write_attribute("computehkl", angles) @@ -633,6 +655,38 @@ def run(self): ("Tth", "Omega", "Chi", "Phi")) self.output("%10s %11s %12s %11s" % (str_pos1, str_pos2, str_pos3, str_pos4)) + elif self.nb_motors == 7: + str_pos1 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[0]]).Position + str_pos2 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[1]]).Position + str_pos3 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[2]]).Position + str_pos4 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[3]]).Position + str_pos5 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[4]]).Position + str_pos6 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[5]]).Position + str_pos7 = "%7.5f" % self.getDevice( + self.angle_device_names[self.angles_names[6]]).Position + self.output("%10s %11s %12s %11s %11s %11s %11s" % + (self.angle_names[0], + self.angle_names[1], + self.angle_names[2], + self.angle_names[3], + self.angle_names[4], + self.angle_names[5], + self.angle_names[6])) + self.output("%10s %11s %12s %11s %11s %11s %11s" % + (str_pos1, + str_pos2, + str_pos3, + str_pos4, + str_pos5, + str_pos6, + str_pos7)) + self.setEnv('Q', [self.h_device.position, self.k_device.position, self.l_device.position, self.diffrac.WaveLength]) @@ -864,80 +918,96 @@ def run(self, H, K, L): class setor0(Macro, _diffrac): - """Set primary orientation reflection choosing hkl and angle values""" + """Set primary orientation reflection choosing hkl and angle values. + Run it without any argument to see the order real positions""" param_def = [ ['H', Type.Float, -999, "H value"], ['K', Type.Float, -999, "K value"], ['L', Type.Float, -999, "L value"], - ['mu', Type.Float, -999, "Mu value"], - ['theta', Type.Float, -999, "Theta value"], - ['chi', Type.Float, -999, "Chi value"], - ['phi', Type.Float, -999, "Phi value"], - ['gamma', Type.Float, -999, "Gamma value"], - ['delta', Type.Float, -999, "Delta value"], + ['ang1', Type.Float, -999, "Real position"], + ['ang2', Type.Float, -999, "Real position"], + ['ang3', Type.Float, -999, "Real position"], + ['ang4', Type.Float, -999, "Real position"], + ['ang5', Type.Float, -999, "Real position"], + ['ang6', Type.Float, -999, "Real position"], + ['ang7', Type.Float, -999, "Real position"], ] - def prepare(self, H, K, L, mu, theta, chi, phi, gamma, delta): + def prepare(self, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7): _diffrac.prepare(self) - def run(self, H, K, L, mu, theta, chi, phi, gamma, delta): - + def run(self, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7): setorn, pars = self.createMacro( - "setorn", 0, H, K, L, mu, theta, chi, phi, gamma, delta) + "setorn", 0, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7) self.runMacro(setorn) class setor1(Macro, _diffrac): - """Set secondary orientation reflection choosing hkl and angle values""" + """Set secondary orientation reflection choosing hkl and angle values. + Run it without any argument to see the order real positions""" param_def = [ ['H', Type.Float, -999, "H value"], ['K', Type.Float, -999, "K value"], ['L', Type.Float, -999, "L value"], - ['mu', Type.Float, -999, "Mu value"], - ['theta', Type.Float, -999, "Theta value"], - ['chi', Type.Float, -999, "Chi value"], - ['phi', Type.Float, -999, "Phi value"], - ['gamma', Type.Float, -999, "Gamma value"], - ['delta', Type.Float, -999, "Delta value"], + ['ang1', Type.Float, -999, "Real position"], + ['ang2', Type.Float, -999, "Real position"], + ['ang3', Type.Float, -999, "Real position"], + ['ang4', Type.Float, -999, "Real position"], + ['ang5', Type.Float, -999, "Real position"], + ['ang6', Type.Float, -999, "Real position"], + ['ang7', Type.Float, -999, "Real position"], ] - def prepare(self, H, K, L, mu, theta, chi, phi, gamma, delta): + def prepare(self, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7): + self.output("setor1 prepare") + self.output(ang3) + self.output(ang7) _diffrac.prepare(self) - def run(self, H, K, L, mu, theta, chi, phi, gamma, delta): - + def run(self, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7): setorn, pars = self.createMacro( - "setorn", 1, H, K, L, mu, theta, chi, phi, gamma, delta) + "setorn", 1, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7) self.runMacro(setorn) class setorn(iMacro, _diffrac): - """Set orientation reflection indicated by the index.""" + """Set orientation reflection indicated by the index. + Run it without any argument to see the order of the angles to be set""" param_def = [ ['ref_id', Type.Integer, None, "reflection index (starting at 0)"], ['H', Type.Float, -999, "H value"], ['K', Type.Float, -999, "K value"], ['L', Type.Float, -999, "L value"], - ['mu', Type.Float, -999, "Mu value"], - ['theta', Type.Float, -999, "Theta value"], - ['chi', Type.Float, -999, "Chi value"], - ['phi', Type.Float, -999, "Phi value"], - ['gamma', Type.Float, -999, "Gamma value"], - ['delta', Type.Float, -999, "Delta value"], + ['ang1', Type.Float, -999, "Real position"], + ['ang2', Type.Float, -999, "Real position"], + ['ang3', Type.Float, -999, "Real position"], + ['ang4', Type.Float, -999, "Real position"], + ['ang5', Type.Float, -999, "Real position"], + ['ang6', Type.Float, -999, "Real position"], + ['ang7', Type.Float, -999, "Real position"], ] - def prepare(self, ref_id, H, K, L, mu, theta, chi, phi, gamma, delta): + def prepare(self, ref_id, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, + ang7): _diffrac.prepare(self) - def run(self, ref_id, H, K, L, mu, theta, chi, phi, gamma, delta): + def run(self, ref_id, H, K, L, ang1, ang2, ang3, ang4, ang5, ang6, ang7): + + if H == -999.0: + msg = "Order of the real motor positions to be given as argument:" + self.output(msg) + for el in self.angle_names: + self.output(el) + return - if (delta == -999 and self.nb_motors == 6) or ( - phi == -999 and self.nb_motors == 4): + if ((ang6 == -999 and self.nb_motors == 6) + or (ang4 == -999 and self.nb_motors == 4) + or (ang7 == -999 and self.nb_motors == 7)): reflections = [] try: reflections = self.diffrac.reflectionlist @@ -972,30 +1042,14 @@ def run(self, ref_id, H, K, L, mu, theta, chi, phi, gamma, delta): ref_txt = "reflection " + str(ref_id) self.output("Enter %s angles" % ref_txt) - if self.nb_motors == 6: - delta = float(self.input(" Delta?", default_value=tmp_ref[ - "delta"], data_type=Type.String)) - - theta = float(self.input(" Theta? ", default_value=tmp_ref[ - "omega"], data_type=Type.String)) - chi = float(self.input(" Chi?", default_value=tmp_ref[ - "chi"], data_type=Type.String)) - phi = float(self.input(" Phi?", default_value=tmp_ref[ - "phi"], data_type=Type.String)) - gamma = float(self.input(" Gamma?", default_value=tmp_ref[ - "gamma"], data_type=Type.String)) - mu = float(self.input(" Mu?", default_value=tmp_ref[ - "mu"], data_type=Type.String)) - if self.nb_motors == 4: - - omega = float(self.input(" Omega?", default_value=tmp_ref[ - "omega"], data_type=Type.String)) - chi = float(self.input(" Chi?", default_value=tmp_ref[ - "chi"], data_type=Type.String)) - phi = float(self.input(" Phi?", default_value=tmp_ref[ - "phi"], data_type=Type.String)) - tth = float(self.input(" Tth?", default_value=tmp_ref[ - "omega"], data_type=Type.String)) + angles_to_set = [] + for el in self.angle_names: + angles_to_set.append( + float(self.input(el+"?", + default_value=tmp_ref[el], + data_type=Type.String) + ) + ) self.output("") @@ -1007,6 +1061,11 @@ def run(self, ref_id, H, K, L, mu, theta, chi, phi, gamma, delta): L = float(self.input(" L?", default_value=tmp_ref[ "l"], data_type=Type.String)) self.output("") + else: + angles = [ang1, ang2, ang3, ang4, ang5, ang6, ang7] + angles_to_set = [] + for i in range(0, self.nb_motors): + angles_to_set.append(angles[i]) # Check collinearity @@ -1028,22 +1087,10 @@ def run(self, ref_id, H, K, L, mu, theta, chi, phi, gamma, delta): values = [ref_id, H, K, L] self.diffrac.write_attribute("SubstituteReflection", values) - # Adjust angles - - if self.nb_motors == 6: - self.angle_values = {"mu": mu, "omega": theta, - "chi": chi, "phi": phi, "gamma": gamma, - "delta": delta} - elif self.nb_motors == 4: - self.angle_values = {"omega": omega, "chi": chi, - "phi": phi, "tth": tth} - - values = [] values.append(ref_id) - - for angle_name in self.angle_names: - values.append(self.angle_values[angle_name]) + for el in angles_to_set: + values.append(el) self.diffrac.write_attribute("AdjustAnglesToReflection", values) diff --git a/src/sardana/macroserver/macros/scan.py b/src/sardana/macroserver/macros/scan.py index 726e80de1d..10d04c8b9c 100644 --- a/src/sardana/macroserver/macros/scan.py +++ b/src/sardana/macroserver/macros/scan.py @@ -1415,21 +1415,26 @@ def do_restore(self): self._motion.move(self.originalPositions) -class ascanct(aNscan, Macro): +class aNscanct(aNscan): + """N-dimensional continuous scan. This is **not** meant to be called by + the user, but as a generic base to construct ascanct, a2scanct, a3scanct, + ...""" + + hints = {"scan": "aNscanct", + "allowsHooks": ("pre-scan", "pre-configuration", + "post-configuration", "pre-move", + "post-move", "pre-acq", "pre-start", + "post-acq", "pre-cleanup", "post-cleanup", + "post-scan")} + + +class ascanct(aNscanct, Macro): """Do an absolute continuous scan of the specified motor. ascanct scans one motor, as specified by motor. The motor starts before the position given by start_pos in order to reach the constant velocity at the start_pos and finishes at the position after the final_pos in order to maintain the constant velocity until the final_pos.""" - hints = {'scan': 'ascanct', 'allowsHooks': ('pre-configuration', - 'post-configuration', - 'pre-start', - 'pre-acq', - 'post-acq', - 'pre-cleanup', - 'post-cleanup')} - param_def = [['motor', Type.Moveable, None, 'Moveable name'], ['start_pos', Type.Float, None, 'Scan start position'], ['final_pos', Type.Float, None, 'Scan final position'], @@ -1444,7 +1449,7 @@ def prepare(self, motor, start_pos, final_pos, nr_interv, latency_time=latency_time, **opts) -class a2scanct(aNscan, Macro): +class a2scanct(aNscanct, Macro): """Two-motor continuous scan. a2scanct scans two motors, as specified by motor1 and motor2. Each motor starts before the position given by its start_pos in order to reach the @@ -1452,14 +1457,6 @@ class a2scanct(aNscan, Macro): its final_pos in order to maintain the constant velocity until its final_pos.""" - hints = {'scan': 'a2scanct', 'allowsHooks': ('pre-configuration', - 'post-configuration', - 'pre-start', - 'pre-acq', - 'post-acq', - 'pre-cleanup', - 'post-cleanup')} - param_def = [ ['motor1', Type.Moveable, None, 'Moveable 1 to move'], ['start_pos1', Type.Float, None, 'Scan start position 1'], @@ -1478,7 +1475,7 @@ def prepare(self, m1, s1, f1, m2, s2, f2, nr_interv, latency_time=latency_time, **opts) -class a3scanct(aNscan, Macro): +class a3scanct(aNscanct, Macro): """Three-motor continuous scan. a2scanct scans three motors, as specified by motor1, motor2 and motor3. Each motor starts before the position given by its start_pos in order to @@ -1486,14 +1483,6 @@ class a3scanct(aNscan, Macro): after its final_pos in order to maintain the constant velocity until its final_pos.""" - hints = {'scan': 'a2scanct', 'allowsHooks': ('pre-configuration', - 'post-configuration', - 'pre-start', - 'pre-acq', - 'post-acq', - 'pre-cleanup', - 'post-cleanup')} - param_def = [ ['motor1', Type.Moveable, None, 'Moveable 1 to move'], ['start_pos1', Type.Float, None, 'Scan start position 1'], @@ -1523,14 +1512,6 @@ class a4scanct(aNscan, Macro): position after its final_pos in order to maintain the constant velocity until its final_pos.""" - hints = {'scan': 'a2scanct', 'allowsHooks': ('pre-configuration', - 'post-configuration', - 'pre-start', - 'pre-acq', - 'post-acq', - 'pre-cleanup', - 'post-cleanup')} - param_def = [ ['motor1', Type.Moveable, None, 'Moveable 1 to move'], ['start_pos1', Type.Float, None, 'Scan start position 1'], @@ -1555,7 +1536,20 @@ def prepare(self, m1, s1, f1, m2, s2, f2, m3, s3, f3, m4, s4, f4, latency_time=latency_time, **opts) -class dscanct(dNscan, Macro): +class dNscanct(dNscan): + """N-dimensional continuous scan. This is **not** meant to be called by + the user, but as a generic base to construct ascanct, a2scanct, a3scanct, + ...""" + + hints = {"scan": "dNscanct", + "allowsHooks": ("pre-scan", "pre-configuration", + "post-configuration", "pre-move", + "post-move", "pre-acq", "pre-start", + "post-acq", "pre-cleanup", "post-cleanup", + "post-scan")} + + +class dscanct(dNscanct, Macro): """Do an a relative continuous motor scan, dscanct scans a motor, as specified by motor1. The Motor starts before the position given by its start_pos in order to @@ -1577,7 +1571,7 @@ def prepare(self, motor, start_pos, final_pos, nr_interv, latency_time=latency_time, **opts) -class d2scanct(dNscan, Macro): +class d2scanct(dNscanct, Macro): """continuous two-motor scan relative to the starting positions, d2scanct scans three motors, as specified by motor1 and motor2. Each motor starts before the position given by its start_pos in order to @@ -1601,7 +1595,7 @@ def prepare(self, m1, s1, f1, m2, s2, f2, integ_time, slow_down, **opts): mode=ContinuousHwTimeMode, **opts) -class d3scanct(dNscan, Macro): +class d3scanct(dNscanct, Macro): """continuous three-motor scan relative to the starting positions, d3scanct scans three motors, as specified by motor1, motor2 and motor3. Each motor starts before the position given by its start_pos in order to @@ -1629,7 +1623,7 @@ def prepare(self, m1, s1, f1, m2, s2, f2, m3, s3, f3, integ_time, integ_time, mode=ContinuousHwTimeMode, **opts) -class d4scanct(dNscan, Macro): +class d4scanct(dNscanct, Macro): """continuous four-motor scan relative to the starting positions, d4scanct scans three motors, as specified by motor1, motor2, motor3 and motor4. @@ -1672,10 +1666,12 @@ class meshct(Macro, Hookable): first motor scan is nested within the second motor scan. """ - hints = {'scan': 'meshct', 'allowsHooks': ('pre-scan', 'pre-move', - 'post-move', 'pre-acq', - 'post-acq', 'post-step', - 'post-scan')} + hints = {"scan": "meshct", + "allowsHooks": ("pre-scan", "pre-configuration", + "post-configuration", "pre-move", + "post-move", "pre-acq", "pre-start", + "post-acq", "pre-cleanup", "post-cleanup", + "post-scan")} env = ('ActiveMntGrp',) param_def = [ diff --git a/src/sardana/macroserver/macros/standard.py b/src/sardana/macroserver/macros/standard.py index 64f793d99d..1354c13729 100644 --- a/src/sardana/macroserver/macros/standard.py +++ b/src/sardana/macroserver/macros/standard.py @@ -25,11 +25,12 @@ __all__ = ["ct", "mstate", "mv", "mvr", "pwa", "pwm", "repeat", "set_lim", "set_lm", "set_pos", "settimer", "uct", "umv", "umvr", "wa", "wm", - "tw", "logmacro"] + "tw", "logmacro", "newfile"] __docformat__ = 'restructuredtext' import datetime +import os import numpy as np from taurus import Device @@ -39,8 +40,10 @@ from sardana.macroserver.macro import Macro, macro, Type, ParamRepeat, \ ViewOption, iMacro, Hookable -from sardana.macroserver.msexception import StopException +from sardana.macroserver.msexception import StopException, UnknownEnv from sardana.macroserver.scan.scandata import Record +from sardana.macroserver.macro import Optional + ########################################################################## # # Motion related macros @@ -156,6 +159,7 @@ def run(self, motor_list): motor_names = [] motor_pos = [] motor_list = sorted(motor_list) + pos_format = self.getViewOption(ViewOption.PosFormat) for motor in motor_list: name = motor.getName() motor_names.append([name]) @@ -166,6 +170,8 @@ def run(self, motor_list): motor_width = max(motor_width, len(name)) fmt = '%c*.%df' % ('%', motor_width - 5) + if pos_format > -1: + fmt = '%c*.%df' % ('%', int(pos_format)) table = Table(motor_pos, elem_fmt=[fmt], col_head_str=motor_names, col_head_width=motor_width, @@ -648,17 +654,25 @@ class ct(Macro, Hookable): env = ('ActiveMntGrp',) hints = {'allowsHooks': ('pre-acq', 'post-acq')} param_def = [ - ['integ_time', Type.Float, 1.0, 'Integration time'] + ['integ_time', Type.Float, 1.0, 'Integration time'], + ['mnt_grp', Type.MeasurementGroup, Optional, 'Measurement Group to ' + 'use'] + ] - def prepare(self, integ_time, **opts): - mnt_grp_name = self.getEnv('ActiveMntGrp') - self.mnt_grp = self.getObj( - mnt_grp_name, type_class=Type.MeasurementGroup) + def prepare(self, integ_time, mnt_grp, **opts): + if mnt_grp is None: + self.mnt_grp_name = self.getEnv('ActiveMntGrp') + self.mnt_grp = self.getObj(self.mnt_grp_name, + type_class=Type.MeasurementGroup) + else: + self.mnt_grp_name = mnt_grp.name + self.mnt_grp = mnt_grp - def run(self, integ_time): + def run(self, integ_time, mnt_grp): if self.mnt_grp is None: - self.error('ActiveMntGrp is not defined or has invalid value') + self.error('The MntGrp {} is not defined or has invalid ' + 'value'.format(self.mnt_grp_name)) return # integration time has to be accessible from with in the hooks # so declare it also instance attribute @@ -699,16 +713,23 @@ class uct(Macro): env = ('ActiveMntGrp',) param_def = [ - ['integ_time', Type.Float, 1.0, 'Integration time'] + ['integ_time', Type.Float, 1.0, 'Integration time'], + ['mnt_grp', Type.MeasurementGroup, Optional, 'Measurement Group to ' + 'use'] + ] - def prepare(self, integ_time, **opts): + def prepare(self, integ_time, mnt_grp, **opts): self.print_value = False - mnt_grp_name = self.getEnv('ActiveMntGrp') - self.mnt_grp = self.getObj( - mnt_grp_name, type_class=Type.MeasurementGroup) + if mnt_grp is None: + self.mnt_grp_name = self.getEnv('ActiveMntGrp') + self.mnt_grp = self.getObj(self.mnt_grp_name, + type_class=Type.MeasurementGroup) + else: + self.mnt_grp_name = mnt_grp.name + self.mnt_grp = mnt_grp if self.mnt_grp is None: return @@ -726,9 +747,10 @@ def prepare(self, integ_time, **opts): valueObj = channel.getValueObj_() valueObj.subscribeEvent(self.counterChanged, channel) - def run(self, integ_time): + def run(self, integ_time, mnt_grp): if self.mnt_grp is None: - self.error('ActiveMntGrp is not defined or has invalid value') + self.error('The MntGrp {} is not defined or has invalid ' + 'value'.format(self.mnt_grp_name)) return self.print_value = True @@ -822,6 +844,7 @@ def run(self, offon, mode): else: self.setEnv('LogMacro', False) + class repeat(Hookable, Macro): """This macro executes as many repetitions of a set of macros as specified by nr parameter. The macros to be repeated can be @@ -870,3 +893,100 @@ def run(self, nr, macro_name_params): self.__loop() progress = ((i + 1) / float(nr)) * 100 yield progress + + +class newfile(Hookable, Macro): + """ Sets the ScanDir and ScanFile as well as ScanID in the environment. + + If ScanFilePath is only a file name, the ScanDir must be set externally + via `senv ScanDir ` or using the %expconf. Otherwise, + the path in ScanFilePath must be absolute and existing on the + MacroServer host. + + The ScanID should be set to the value before the upcoming scan number. + Default value is 0. + """ + + hints = {'allowsHooks': ('post-newfile')} + + param_def = [ + ['ScanFilePath_list', + [['ScanFilePath', Type.String, None, '(ScanDir/)ScanFile']], + None, 'List of (ScanDir/)ScanFile'], + ['ScanID', Type.Integer, 0, 'Scan ID'], + ] + + def run(self, ScanFilePath_list, ScanID): + path_list = [] + fileName_list = [] + # traverse the repeat parameters for the ScanFilePath_list + for i, ScanFilePath in enumerate(ScanFilePath_list): + path = os.path.dirname(ScanFilePath) + fileName = os.path.basename(ScanFilePath) + if not path and i == 0: + # first entry and no given ScanDir: check if ScanDir exists + try: + ScanDir = self.getEnv('ScanDir') + except UnknownEnv: + ScanDir = '' + if not (isinstance(ScanDir, basestring) and len(ScanDir) > 0): + msg = ('Data is not stored until ScanDir is correctly ' + 'set! Provide ScanDir with newfile macro: ' + '`newfile [/] ` ' + 'or `senv ScanDir ` or with %expconf') + self.error(msg) + return + else: + path = ScanDir + elif not path and i > 0: + # not first entry and no given path: use path of last iteration + path = path_list[i-1] + elif not os.path.isabs(path): + # relative path + self.error('Only absolute path are allowed!') + return + else: + # absolute path + path = os.path.normpath(path) + + if i > 0 and (path not in path_list): + # check if paths are equal + self.error('Multiple paths to the data files are not allowed') + return + elif not os.path.exists(path): + # check if folder exists + self.error('Path %s does not exists on the host of the ' + 'MacroServer and has to be created in ' + 'advance.' % path) + return + else: + self.debug('Path %s appended.' % path) + path_list.append(path) + + if not fileName: + self.error('No filename is given.') + return + elif fileName in fileName_list: + self.error('Duplicate filename %s is not allowed.' % fileName) + return + else: + self.debug('Filename is %s.' % fileName) + fileName_list.append(fileName) + + if ScanID < 1: + ScanID = 0 + + self.setEnv('ScanFile', fileName_list) + self.setEnv('ScanDir', path_list[0]) + self.setEnv('ScanID', ScanID) + + self.output('ScanDir is\t: %s', path_list[0]) + for i, ScanFile in enumerate(fileName_list): + if i == 0: + self.output('ScanFile set to\t: %s', ScanFile) + else: + self.output('\t\t %s', ScanFile) + self.output('Next scan is\t: #%d', ScanID+1) + + for postNewfileHook in self.getHooks('post-newfile'): + postNewfileHook() diff --git a/src/sardana/macroserver/macros/test/test_gh.py b/src/sardana/macroserver/macros/test/test_gh.py new file mode 100644 index 0000000000..31bd9c8a68 --- /dev/null +++ b/src/sardana/macroserver/macros/test/test_gh.py @@ -0,0 +1,66 @@ +#!/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 . +## +############################################################################## + +from taurus.external import unittest + +from sardana.macroserver.macros.test import RunMacroTestCase, testRun +from sardana.tango.macroserver.test import BaseMacroServerTestCase +from sardana.tango.pool.test.test_measurementgroup import MeasSarTestTestCase +from sardana.macroserver.macros.test.test_scanct import mg_config4 + + +@testRun(macro_name="lsgh", wait_timeout=1) +@testRun(macro_name="defgh", macro_params=["lsm", "pre-acq"], wait_timeout=1) +@testRun(macro_name="defgh", macro_params=["lsm mot.*", "pre-acq"], + wait_timeout=1) +@testRun(macro_name="udefgh", wait_timeout=1) +class GeneralHooksTest(MeasSarTestTestCase, BaseMacroServerTestCase, + RunMacroTestCase, unittest.TestCase): + + def setUp(self): + MeasSarTestTestCase.setUp(self) + BaseMacroServerTestCase.setUp(self) + RunMacroTestCase.setUp(self) + unittest.TestCase.setUp(self) + + def create_meas(self, config): + MeasSarTestTestCase.create_meas(self, config) + self.macro_executor.run(macro_name='senv', + macro_params=['ActiveMntGrp', '_test_mg_1'], + sync=True, timeout=1.) + + def test_gh(self): + self.macro_runs(macro_name="defgh", macro_params=["lsm", "pre-acq"], + wait_timeout=1) + self.create_meas(mg_config4) + self.macro_runs(macro_name="ct", macro_params=[".1"], wait_timeout=1) + self.macro_runs(macro_name="udefgh", macro_params=["lsm", "pre-acq"], + wait_timeout=1) + + def tearDown(self): + unittest.TestCase.tearDown(self) + RunMacroTestCase.tearDown(self) + BaseMacroServerTestCase.tearDown(self) + MeasSarTestTestCase.tearDown(self) diff --git a/src/sardana/pool/poolcontrollers/test/test_DummyCounterTimerController.py b/src/sardana/macroserver/macros/test/test_macro.py similarity index 56% rename from src/sardana/pool/poolcontrollers/test/test_DummyCounterTimerController.py rename to src/sardana/macroserver/macros/test/test_macro.py index 1955ad1213..cca43cd648 100644 --- a/src/sardana/pool/poolcontrollers/test/test_DummyCounterTimerController.py +++ b/src/sardana/macroserver/macros/test/test_macro.py @@ -4,7 +4,7 @@ ## # This file is part of Sardana ## -# http://www.tango-controls.org/static/sardana/latest/doc/html/index.html +# http://www.sardana-controls.org/ ## # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain ## @@ -23,29 +23,29 @@ ## ############################################################################## +import os + from taurus.external import unittest -from taurus.test import insertTest -from sardana.pool.poolsynchronization import PoolSynchronization -from sardana.pool.test.test_acquisition import AcquisitionTestCase -import logging + +from sardana.macroserver.macros.test import RunMacroTestCase, testRun +from sardana.tango.macroserver.test import BaseMacroServerTestCase -@insertTest(helper_name='hw_step_acquisition', repetitions=1, - integ_time=0.4) -class DummyCounterTimerControllerTestCase(AcquisitionTestCase, unittest.TestCase): - """Integration test. - """ - chn_ctrl_name = '_test_ct_ctrl_1' - chn_elem_name1 = '_test_ct_1_1' +@testRun(macro_name="runMacro") +@testRun(macro_name="createMacro") +@testRun(macro_name="execMacro") +class MacroTest(BaseMacroServerTestCase, RunMacroTestCase, unittest.TestCase): def setUp(self): - """#Create a Controller, TriggerGate and PoolSynchronization objects from - #Ni660XTriggerGateController and Ni660XPositionCTCtrl configurations. - """ + macros_test_path = '../../test/res/macros' + source = os.path.join(os.path.dirname(__file__), macros_test_path) + path = os.path.abspath(source) + properties = {'MacroPath': [path]} unittest.TestCase.setUp(self) - AcquisitionTestCase.setUp(self) - self.channel_names.append(self.chn_elem_name1) + BaseMacroServerTestCase.setUp(self, properties) + RunMacroTestCase.setUp(self) def tearDown(self): - AcquisitionTestCase.tearDown(self) + BaseMacroServerTestCase.tearDown(self) + RunMacroTestCase.tearDown(self) unittest.TestCase.tearDown(self) diff --git a/src/sardana/macroserver/macros/test/test_scan.py b/src/sardana/macroserver/macros/test/test_scan.py index 8bba57aa7d..0aa51fa887 100644 --- a/src/sardana/macroserver/macros/test/test_scan.py +++ b/src/sardana/macroserver/macros/test/test_scan.py @@ -167,3 +167,9 @@ class MeshTest(RunStopMacroTestCase, unittest.TestCase): stoped. See :class:`.RunStopMacroTestCase` for requirements. """ macro_name = 'mesh' + + +@testRun(macro_params=['10', '0.1'], wait_timeout=30) +class TimescanTest(RunStopMacroTestCase, unittest.TestCase): + + macro_name = 'timescan' diff --git a/src/sardana/macroserver/macros/test/test_scanct.py b/src/sardana/macroserver/macros/test/test_scanct.py index b9972159df..3cf83777bb 100644 --- a/src/sardana/macroserver/macros/test/test_scanct.py +++ b/src/sardana/macroserver/macros/test/test_scanct.py @@ -1,4 +1,28 @@ #!/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 . +## +############################################################################## + """Tests for continuous scans (ct-like)""" import time import PyTango @@ -63,7 +87,8 @@ class ScanctTest(MeasSarTestTestCase, BaseMacroServerTestCase, def setUp(self): MeasSarTestTestCase.setUp(self) - BaseMacroServerTestCase.setUp(self, self.pool_name) + properties = {'PoolNames': self.pool_name} + BaseMacroServerTestCase.setUp(self, properties) RunStopMacroTestCase.setUp(self) def configure_motors(self, motor_names): diff --git a/src/sardana/macroserver/macros/test/test_standard.py b/src/sardana/macroserver/macros/test/test_standard.py index 9f5278978b..b7a51006c7 100644 --- a/src/sardana/macroserver/macros/test/test_standard.py +++ b/src/sardana/macroserver/macros/test/test_standard.py @@ -81,13 +81,13 @@ def test_move(self): self.macro_runs("umvr", macro_params=[MOT_NAME1, "-1"], wait_timeout=3) -@testRun(macro_params=[MOT_NAME1]) +@testRun(macro_params=[MOT_NAME1], wait_timeout=1) class MstateTest(RunMacroTestCase, unittest.TestCase): macro_name = "mstate" -@testRun(macro_params=["blabla"]) +@testRun(macro_params=["blabla"], wait_timeout=1) class ReportTest(RunMacroTestCase, unittest.TestCase): macro_name = "report" diff --git a/src/sardana/macroserver/msmacromanager.py b/src/sardana/macroserver/msmacromanager.py index 29d88d70a8..c84668998e 100644 --- a/src/sardana/macroserver/msmacromanager.py +++ b/src/sardana/macroserver/msmacromanager.py @@ -70,7 +70,7 @@ from sardana.macroserver.msexception import UnknownMacroLibrary, \ LibraryError, UnknownMacro, MissingEnv, AbortException, StopException, \ MacroServerException, UnknownEnv -from sardana.spock.parser import ParamParser +from sardana.util.parser import ParamParser # These classes are imported from the "client" part of sardana, if finally # both the client and the server side needs them, place them in some @@ -484,13 +484,6 @@ def reloadMacroLib(self, module_name, path=None): means the current MacroPath will be used] :return: the MacroLibrary object for the reloaded macro library""" path = path or self.getMacroPath() - # reverse the path order: - # more priority elements last. This way if there are repeated elements - # they first ones (lower priority) will be overwritten by the last ones - if path: - path = copy.copy(path) - path.reverse() - mod_manager = ModuleManager() m, exc_info = None, None valid, exc_info = mod_manager.isValidModule(module_name, path) @@ -719,18 +712,23 @@ def getMacroInfo(self, macro_names, format='json'): ret.append(json_codec.encode(('', macro_meta.serialize()))[1]) return ret - def _createMacroNode(self, macro_name, macro_params): + def _createMacroNode(self, macro_name, macro_params_raw): macro = self.getMacro(macro_name) params_def = macro.get_parameter() + # merge params to a single, space separated, string (spock like) + macro_params_str = " ".join(macro_params_raw) + param_parser = ParamParser(params_def) + # parse string with macro params to the correct list representation + macro_params = param_parser.parse(macro_params_str) return createMacroNode(macro_name, params_def, macro_params) def decodeMacroParameters(self, door, raw_params): """Decode macro parameters :param door: (sardana.macroserver.msdoor.MSDoor) door object - :param raw_params: (lxml.etree._Element or list) xml element representing - macro with subelements representing parameters or list - with macro name followed by parameter values + :param raw_params: (lxml.etree._Element or list) xml element + representing macro with subelements representing parameters or + list with macro name followed by parameter values """ if isinstance(raw_params, etree._Element): macro_name = raw_params.get("name") @@ -1200,7 +1198,16 @@ def _prepareGeneralHooks(self, macro_obj): if len(general_hooks) == 0: return for hook_info_raw, hook_places in general_hooks: - hook_info = ParamParser().parse(hook_info_raw) + hook_info_tokens = hook_info_raw.split(" ", 1) + hook_name = hook_info_tokens[0] + hook_info = [hook_name] + if len(hook_info_tokens) == 2: + hook_params_raw = hook_info_tokens[1] + hook_param_def = self.macro_manager.getMacro( + hook_name).get_parameter() + param_parser = ParamParser(hook_param_def) + hook_params = param_parser.parse(hook_params_raw) + hook_info += hook_params hook = ExecMacroHook(macro_obj, hook_info) macro_obj.appendHook((hook, hook_places)) diff --git a/src/sardana/macroserver/msparameter.py b/src/sardana/macroserver/msparameter.py index f0422c8959..f7bd8ade2c 100644 --- a/src/sardana/macroserver/msparameter.py +++ b/src/sardana/macroserver/msparameter.py @@ -36,15 +36,38 @@ from copy import deepcopy from lxml import etree - from taurus.core.util.containers import CaselessDict from sardana import ElementType, INTERFACES_EXPANDED +from sardana.sardanautils import is_non_str_seq from sardana.macroserver.msbase import MSBaseObject from sardana.macroserver.msexception import MacroServerException, \ UnknownMacro, UnknownMacroLibrary +class OptionalParamClass(dict): + def __init__(self, obj): + super(OptionalParamClass, self).__init__(obj) + attributes = dir(self) + + for attr in attributes: + # iteritems is necessary fo python 2.6 implementation of json + if attr in ['__setattr__', '__repr__', 'raise_error', '__class__', + '__dict__', '__weakref__', 'iteritems']: + continue + self.__setattr__(attr, self.raise_error) + self.__setattr__ = self.raise_error + + def __repr__(self): + return 'Optional' + + def raise_error(*args, **kwargs): + raise RuntimeError('can not be accessed') + + +Optional = OptionalParamClass({'___optional_parameter__': True}) + + class WrongParam(MacroServerException): def __init__(self, *args): @@ -386,6 +409,7 @@ def decodeNormal(self, raw_param, param_def): """ param_type = param_def["type"] name = param_def["name"] + optional_param = False if isinstance(param_type, list): param = self.decodeRepeat(raw_param, param_def) else: @@ -397,22 +421,26 @@ def decodeNormal(self, raw_param, param_def): else: value = raw_param # None or [] indicates default value - if value is None or (isinstance(value, list) and len(value) == 0): + if value is None or (isinstance(value, list) and + len(value) == 0): value = param_def['default_value'] if value is None: - raise MissingParam, "'%s' not specified" % name + raise MissingParam("'%s' not specified" % name) + elif value is Optional: + param = None + optional_param = True else: # cast to sting to fulfill with ParamType API - value = str(value) - param = param_type.getObj(value) - except ValueError, e: - raise WrongParamType, e.message - except UnknownParamObj, e: - raise WrongParam, e.message - if param is None: + param = param_type.getObj(str(value)) + + except ValueError as e: + raise WrongParamType(e.message) + except UnknownParamObj as e: + raise WrongParam(e.message) + if param is None and not optional_param: msg = 'Could not create %s parameter "%s" for "%s"' % \ (param_type.getName(), name, raw_param) - raise WrongParam, msg + raise WrongParam(msg) return param def decodeRepeat(self, raw_param_repeat, param_repeat_def): @@ -442,7 +470,14 @@ def decodeRepeat(self, raw_param_repeat, param_repeat_def): if max_rep and len_rep > max_rep: msg = 'Found %d repetitions of param %s, max is %d' % \ (len_rep, name, max_rep) - raise SupernumeraryRepeat, msg + raise SupernumeraryRepeat(msg) + # repeat params with only one member and only one repetition value are + # allowed - encapsulate it in list and try to decode anyway; + # for the moment this only works for non XML decoding but could be + # extended in the future to support XML as well + if not is_non_str_seq(raw_param_repeat)\ + and not isinstance(raw_param_repeat, etree._Element): + raw_param_repeat = [raw_param_repeat] for raw_repeat in raw_param_repeat: if len(param_type) > 1: repeat = [] diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py index 83862aea0a..c65437bd61 100644 --- a/src/sardana/macroserver/scan/gscan.py +++ b/src/sardana/macroserver/scan/gscan.py @@ -271,6 +271,8 @@ def __init__(self, macro, generator=None, moveables=[], env={}, moveable_names.append(moveable.moveable.getName()) self._moveables.append(moveable) + self._check_moveables_limits() + name = self.__class__.__name__ self.call__init__(Logger, name) @@ -369,6 +371,43 @@ def __init__(self, macro, generator=None, moveables=[], env={}, # --------------------------------------------------------------------- self._setupEnvironment(env) + def _check_moveables_limits(self): + for m in self._moveables: + pos_range = m.moveable.getAttribute("Position").range + try: + high = float(pos_range[1].magnitude) # Taurus 4 + except AttributeError: + try: + high = float(pos_range[1]) # Taurus 3 + except ValueError: + high = None + try: + low = float(pos_range[0].magnitude) # Taurus 4 + except AttributeError: + try: + low = float(pos_range[0]) # Taurus 3 + except ValueError: + low = None + + if any((high, low)) and not any((m.min_value, m.max_value)): + self._macro.info("Scan range is not defined for %s and could " + "not be verified against motor limits." + % m.moveable.getName()) + + for pos in (m.min_value, m.max_value): + if pos is None: + continue + if high is not None: + if float(pos) > high: + raise RuntimeError( + "requested movement of %s is above its upper limit" + % m.moveable.getName()) + if low is not None: + if float(pos) < low: + raise RuntimeError( + "requested movement of %s is below its lower limit" + % m.moveable.getName()) + def _getExtraColumns(self): ret = [] try: @@ -944,6 +983,10 @@ def scan(self): pass def step_scan(self): + macro = self.macro + if hasattr(macro, 'getHooks'): + for hook in macro.getHooks('pre-scan'): + hook() self.start() try: for i in self.scan_loop(): @@ -960,7 +1003,11 @@ def step_scan(self): self._env["endstatus"] = endstatus self.end() self.do_restore() - if endstatus != ScanEndStatus.Normal: + if endstatus == ScanEndStatus.Normal: + if hasattr(macro, 'getHooks'): + for hook in macro.getHooks('post-scan'): + hook() + else: raise def scan_loop(self): @@ -998,16 +1045,19 @@ def scan_loop(self): macro = self.macro scream = False + self._deterministic_scan = False if hasattr(macro, "nr_points"): nr_points = float(macro.nr_points) + if hasattr(macro, "integ_time"): + integ_time = macro.integ_time + self.measurement_group.putIntegrationTime(integ_time) + self.measurement_group.setNbStarts(nr_points) + self.measurement_group.prepare() + self._deterministic_scan = True scream = True else: yield 0.0 - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('pre-scan'): - hook() - self._sum_motion_time = 0 self._sum_acq_time = 0 @@ -1019,10 +1069,6 @@ def scan_loop(self): if scream: yield ((i + 1) / nr_points) * 100.0 - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('post-scan'): - hook() - if not scream: yield 100.0 @@ -1092,7 +1138,10 @@ def stepUp(self, n, step, lstep): integ_time = step['integ_time'] # Acquire data self.debug("[START] acquisition") - state, data_line = mg.count(integ_time) + if self._deterministic_scan: + state, data_line = mg.count_raw() + else: + state, data_line = mg.count(integ_time) for ec in self._extra_columns: data_line[ec.getName()] = ec.read() self.debug("[ END ] acquisition") @@ -1481,6 +1530,12 @@ def get_min_pos(self, motor): ''' pos_obj = motor.getPositionObj() min_pos, _ = pos_obj.getRange() + try: + # Taurus 4 uses quantities however Sardana does not support them + # yet - use magnitude for the moment. + min_pos = min_pos.magnitude + except AttributeError: + pass try: min_pos = float(min_pos) except ValueError: @@ -1494,6 +1549,12 @@ def get_max_pos(self, motor): ''' pos_obj = motor.getPositionObj() _, max_pos = pos_obj.getRange() + try: + # Taurus 4 uses quantities however Sardana does not support them + # yet - use magnitude for the moment. + max_pos = max_pos.magnitude + except AttributeError: + pass try: max_pos = float(max_pos) except ValueError: @@ -1755,10 +1816,6 @@ def scan_loop(self): point_nb, step = -1, None # data = self.data - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('pre-scan'): - hook() - # start move & acquisition as close as possible # from this point on synchronization becomes critical manager.add_job(self.go_through_waypoints) @@ -1872,10 +1929,6 @@ def scan_loop(self): self.motion_end_event.wait() - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('post-scan'): - hook() - env = self._env env['acqtime'] = sum_integ_time env['delaytime'] = sum_delay @@ -2452,16 +2505,8 @@ def scan_loop(self): # point_nb, step = -1, None # data = self.data - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('pre-scan'): - hook() - self.go_through_waypoints() - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('post-scan'): - hook() - env = self._env env['acqtime'] = sum_integ_time env['delaytime'] = sum_delay @@ -2668,20 +2713,17 @@ def scan_loop(self): msg = "Relative timestamp (dt) column contains theoretical values" self.macro.warning(msg) - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('pre-scan'): - hook() - if hasattr(macro, 'getHooks'): for hook in macro.getHooks('pre-acq'): hook() yield 0 - measurement_group.measure(synchronization, - self.value_buffer_changed) + measurement_group.count_continuous(synchronization, + self.value_buffer_changed) self.debug("Waiting for value buffer events to be processed") self.wait_value_buffer() self.join_thread_pool() + self.macro.checkPoint() self._fill_missing_records() yield 100 @@ -2689,10 +2731,6 @@ def scan_loop(self): for hook in macro.getHooks('post-acq'): hook() - if hasattr(macro, 'getHooks'): - for hook in macro.getHooks('post-scan'): - hook() - def _fill_missing_records(self): # fill record list with dummy records for the final padding nr_points = self.macro.nr_points diff --git a/src/sardana/macroserver/scan/test/test_gscan.py b/src/sardana/macroserver/scan/test/test_gscan.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/__init__.py b/src/sardana/macroserver/test/__init__.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/res/__init__.py b/src/sardana/macroserver/test/res/__init__.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/res/fakerecorders.py b/src/sardana/macroserver/test/res/fakerecorders.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/res/macros/testmacros.py b/src/sardana/macroserver/test/res/macros/testmacros.py new file mode 100644 index 0000000000..638315865b --- /dev/null +++ b/src/sardana/macroserver/test/res/macros/testmacros.py @@ -0,0 +1,244 @@ +from sardana.macroserver.macro import Type, Macro + +FAIL_MSG = "Parsing or decoding failed (result: %r; expected: %r)" + + +class runMacro(Macro): + """ + Macro to test the parameters parsing/decoding using the macro API, + 'runMacro' + """ + def run(self, *args): + params = expected_params = (99, [1., 2.]) + macro, _ = self.prepareMacro("pt6_base", *params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = (99, 1., 2.) + macro, _ = self.prepareMacro("pt6_base", *params) + expected_params = (99, [1., 2.]) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt6_base 99 [1. 2.]" + macro, _ = self.prepareMacro(params) + expected_params = (99, [1., 2.]) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt6_base 99 1. 2." + macro, _ = self.prepareMacro(params) + expected_params = (99, [1., 2.]) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = expected_params = ([92], True) + macro, _ = self.prepareMacro("pt10_base", *params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = (91, True) + expected_params = ([91], True) + macro, _ = self.prepareMacro("pt10_base", *params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt10_base [91] True" + expected_params = ([91], True) + macro, _ = self.prepareMacro(params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt10_base 91 True" + expected_params = ([91], True) + macro, _ = self.prepareMacro(params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + +class createMacro(Macro): + """ + Macro to test the parameters parsing/decoding using the macro API, + 'createMacro' + """ + def run(self, *args): + + params = expected_params = (99, [1., 2.]) + macro, pars = self.createMacro('pt6_base', *params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + 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 + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt6_base 99 [1. 2.]" + macro, _ = self.createMacro(params) + expected_params = (99, [1., 2.]) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt6_base 99 1. 2." + macro, _ = self.createMacro(params) + expected_params = (99, [1., 2.]) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = expected_params = ([92], True) + macro, _ = self.createMacro("pt10_base", *params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = (91, True) + expected_params = ([91.], True) + macro, _ = self.createMacro("pt10_base", *params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt10_base [91] True" + expected_params = ([91], True) + macro, _ = self.createMacro(params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt10_base 91 True" + expected_params = ([91], True) + macro, _ = self.createMacro(params) + self.runMacro(macro) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + +class execMacro(Macro): + """ + Macro to test the parameters parsing/decoding using the macro API, + 'execMacro' + """ + def run(self, *args): + params = expected_params = (99, [1., 2.]) + macro = self.execMacro('pt6_base', *params) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = (99, 1., 2.) + macro = self.execMacro('pt6_base', *params) + result = macro.data + expected_params = (99, [1., 2.]) + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt6_base 99 [1 2]" + macro = self.execMacro(params) + expected_params = (99, [1., 2.]) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt6_base 99 1 2" + macro = self.execMacro(params) + expected_params = (99, [1., 2.]) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = expected_params = ([92], True) + macro = self.execMacro('pt10_base', *params) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + expected_params = ([99.], True) + macro = self.execMacro("pt10_base [99] True") + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + expected_params = ([999.], True) + macro = self.execMacro("pt10_base", 999, True) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt10_base [91] True" + expected_params = ([91], True) + macro = self.execMacro(params) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + params = "pt10_base 91 True" + expected_params = ([91], True) + macro = self.execMacro(params) + result = macro.data + msg = FAIL_MSG % (result, expected_params) + assert expected_params == result, msg + + +class pt6_base(Macro): + """Macro with a number parameter followed by a list of numbers. + Usages from Spock, ex.: + pt6_base 99 [1 3] + pt6_base 99 1 3 + """ + + param_def = [ + ['val1', Type.Float, None, 'value 1'], + ['numb_list', [['pos', Type.Float, None, 'value']], None, + 'List of values'], + ] + + def run(self, *args, **kwargs): + self.data = args + + +class pt10_base(Macro): + """Macro with a list of numbers followed by a boolean parameter. + Usages from Spock, ex.: + pt10_base [1] True + pt10_base 1 True + """ + + param_def = [ + + ['numb_list', [['pos', Type.Float, None, 'value']], None, + 'List of values'], + ['val1', Type.Boolean, None, 'value 1'], + ] + + def run(self, *args, **kwargs): + self.data = args diff --git a/src/sardana/macroserver/test/res/recorders/path1/fakerecorders.py b/src/sardana/macroserver/test/res/recorders/path1/fakerecorders.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/res/recorders/path2/fakerecorders.py b/src/sardana/macroserver/test/res/recorders/path2/fakerecorders.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/res/recorders/path3/fakerecorders.py b/src/sardana/macroserver/test/res/recorders/path3/fakerecorders.py old mode 100755 new mode 100644 diff --git a/src/sardana/macroserver/test/test_msrecordermanager.py b/src/sardana/macroserver/test/test_msrecordermanager.py old mode 100755 new mode 100644 diff --git a/src/sardana/pool/__init__.py b/src/sardana/pool/__init__.py index 232369c6cb..0990a95788 100644 --- a/src/sardana/pool/__init__.py +++ b/src/sardana/pool/__init__.py @@ -25,7 +25,8 @@ """This is the main device pool module""" -__all__ = ["ControllerAPI", "AcqTriggerType", "AcqMode", "PoolUtil"] +__all__ = ["ControllerAPI", "AcqTriggerType", "AcqSynch", "AcqSynchType", + "AcqMode", "PoolUtil"] __docformat__ = 'restructuredtext' diff --git a/src/sardana/pool/controller.py b/src/sardana/pool/controller.py index 6b948902fb..8bd75acec1 100644 --- a/src/sardana/pool/controller.py +++ b/src/sardana/pool/controller.py @@ -661,22 +661,40 @@ class Loadable(object): .. note: Do not inherit directly from Loadable.""" + #: axis of the default timer + default_timer = None + + def PrepareOne(self, axis, value, repetitions, latency, nb_starts): + """**Controller API**. Override if necessary. + Called to prepare the master channel axis with the measurement + parameters. + Default implementation does nothing. + + :param int axis: axis number + :param int repetitions: number of repetitions + :param float value: integration time / monitor count + :param float latency: latency time + :param int nb_starts: number of starts + """ + pass + def PreLoadAll(self): """**Controller API**. Override if necessary. Called to prepare loading the integration time / monitor value. Default implementation does nothing.""" pass - def PreLoadOne(self, axis, value, repetitions): + def PreLoadOne(self, axis, value, repetitions, latency): """**Controller API**. Override if necessary. - Called to prepare loading the master channel axis with the integration - time / monitor value. + Called to prepare loading the master channel axis with the + acquisition parameters. Default implementation returns True. :param int axis: axis number :param float value: integration time /monitor value :param int repetitions: number of repetitions - :return: True means a successfull PreLoadOne or False for a failure + :param float latency: latency time + :return: True means a successful PreLoadOne or False for a failure :rtype: bool""" return True @@ -686,7 +704,7 @@ def LoadAll(self): Default implementation does nothing.""" pass - def LoadOne(self, axis, value, repetitions): + def LoadOne(self, axis, value, repetitions, latency): """**Controller API**. Override is MANDATORY! Called to load the integration time / monitor value. Default implementation raises :exc:`NotImplementedError`. @@ -694,6 +712,7 @@ def LoadOne(self, axis, value, repetitions): :param int axis: axis number :param float value: integration time /monitor value :param int repetitions: number of repetitions + :param float latency: latency time :param float value: integration time /monitor value""" raise NotImplementedError("LoadOne must be defined in the controller") @@ -869,7 +888,8 @@ def DefinePosition(self, axis, position): pass -class CounterTimerController(Controller, Readable, Startable, Stopable, Loadable): +class CounterTimerController(Controller, Readable, Startable, Stopable, + Loadable): """Base class for a counter/timer controller. Inherit from this class to implement your own counter/timer controller for the device pool. @@ -882,6 +902,11 @@ class CounterTimerController(Controller, Readable, Startable, Stopable, Loadable #: A :class:`dict` containing the standard attributes present on each axis #: device standard_axis_attributes = { + 'IntegrationTime': {'type': float, + 'description': 'Integration time used in ' + 'independent acquisition'}, + 'Timer': {'type': str, + 'description': 'Timer used in independent acquisition'}, 'Value': {'type': float, 'description': 'Value', }, 'Data': {'type': str, @@ -1014,6 +1039,9 @@ class ZeroDController(Controller, Readable, Stopable): #: A :class:`dict` containing the standard attributes present on each axis #: device standard_axis_attributes = { + 'IntegrationTime': {'type': float, + 'description': 'Integration time used in ' + 'independent acquisition'}, 'Value': {'type': float, 'description': 'Value', }, 'Data': {'type': str, @@ -1039,6 +1067,11 @@ class OneDController(Controller, Readable, Startable, Stopable, Loadable): .. versionadded:: 1.2""" standard_axis_attributes = { + 'IntegrationTime': {'type': float, + 'description': 'Integration time used in ' + 'independent acquisition'}, + 'Timer': {'type': str, + 'description': 'Timer used in independent acquisition'}, 'Value': {'type': (float,), 'description': 'Value', 'maxdimsize': (16 * 1024,)}, @@ -1075,6 +1108,11 @@ class TwoDController(Controller, Readable, Startable, Stopable, Loadable): implement your own 2D controller for the device pool.""" standard_axis_attributes = { + 'IntegrationTime': {'type': float, + 'description': 'Integration time used in ' + 'independent acquisition'}, + 'Timer': {'type': str, + 'description': 'Timer used in independent acquisition'}, 'Value': {'type': ((float,),), 'description': 'Value', 'maxdimsize': (4 * 1024, 4 * 1024)}, @@ -1383,6 +1421,9 @@ class PseudoCounterController(Controller): #: A :class:`dict` containing the standard attributes present on each axis #: device standard_axis_attributes = { + 'IntegrationTime': {'type': float, + 'description': 'Integration time used in ' + 'independent acquisition'}, 'Value': {'type': float, 'description': 'Value', }, 'Data': {'type': str, diff --git a/src/sardana/pool/poolacquisition.py b/src/sardana/pool/poolacquisition.py old mode 100755 new mode 100644 index 3d5719ed6c..e5b42c711d --- a/src/sardana/pool/poolacquisition.py +++ b/src/sardana/pool/poolacquisition.py @@ -23,15 +23,18 @@ ## ############################################################################## -"""This module is part of the Python Pool libray. It defines the class for an +"""This module is part of the Python Pool library. It defines the class for an acquisition""" -__all__ = ["AcquisitionState", "AcquisitionMap", "PoolCTAcquisition", - "Pool0DAcquisition", "Channel", "PoolIORAcquisition"] +__all__ = ["get_acq_ctrls", "AcquisitionState", "AcquisitionMap", + "PoolCTAcquisition", "Pool0DAcquisition", "PoolIORAcquisition", + "PoolAcquisitionHardware", "PoolAcquisitionSoftware", + "PoolAcquisitionSoftwareStart"] __docformat__ = 'restructuredtext' import time +import weakref import datetime from taurus.core.util.log import DebugIt @@ -39,8 +42,8 @@ from sardana import SardanaValue, State, ElementType, TYPE_TIMERABLE_ELEMENTS from sardana.sardanathreadpool import get_thread_pool -from sardana.pool import SynchParam, SynchDomain, AcqSynch -from sardana.pool.poolaction import ActionContext, PoolActionItem, PoolAction +from sardana.pool import AcqSynch, AcqMode +from sardana.pool.poolaction import ActionContext, PoolAction from sardana.pool.poolsynchronization import PoolSynchronization #: enumeration representing possible motion states @@ -62,194 +65,260 @@ } -def split_MGConfigurations(mg_cfg_in): - """Split MeasurementGroup configuration with channels - triggered by SW Trigger and channels triggered by HW trigger +def is_value_error(value): + if isinstance(value, SardanaValue) and value.error: + return True + return False + + +def get_acq_ctrls(ctrls): + """Converts configuration controllers into acquisition controllers. + + Takes care about converting their internals as well. - TODO: (technical debt) All the MeasurementGroup configuration - logic should be encapsulate in a dedicated class instead of - using a basic data structures like dict or lists... + :param ctrls: sequence of configuration controllers objects + :type ctrls: sardana.pool.poolmeasurementgroup.ControllerConfiguration + :return: sequence of acquisition controllers + :rtype: :class:`~sardana.pool.poolacquisition.AcqController` + + .. note:: + The get_acq_ctrls function has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. """ - ctrls_in = mg_cfg_in['controllers'] - mg_sw_cfg_out = {} - mg_0d_cfg_out = {} - mg_hw_cfg_out = {} - mg_sw_cfg_out['controllers'] = ctrls_sw_out = {} - mg_0d_cfg_out['controllers'] = ctrls_0d_out = {} - mg_hw_cfg_out['controllers'] = ctrls_hw_out = {} - for ctrl, ctrl_info in ctrls_in.items(): - external = isinstance(ctrl, str) and ctrl.startswith('__') - # skipping external controllers e.g. Tango attributes - if external: - continue - # splitting ZeroD based on the type - if ctrl.get_ctrl_types()[0] == ElementType.ZeroDExpChannel: - ctrls_0d_out[ctrl] = ctrl_info - # ignoring PseudoCounter - elif ctrl.get_ctrl_types()[0] == ElementType.PseudoCounter: - pass - # splitting rest of the channels based on the assigned trigger - else: - synchronizer = ctrl_info.get('synchronizer') - if synchronizer is None or synchronizer == 'software': - ctrls_sw_out[ctrl] = ctrl_info - else: - ctrls_hw_out[ctrl] = ctrl_info - - def find_master(ctrls, role): - master_idx = float("+inf") - master = None - for ctrl_info in ctrls.values(): - element = ctrl_info[role] - element_idx = ctrl_info["channels"][element]["index"] - element_enabled = ctrl_info["channels"][element]["enabled"] - # Find master only if is enabled - if element_idx < master_idx and element_enabled: - master = element - master_idx = element_idx - return master - - if len(ctrls_sw_out): - mg_sw_cfg_out["timer"] = find_master(ctrls_sw_out, "timer") - mg_sw_cfg_out["monitor"] = find_master(ctrls_sw_out, "monitor") - if len(ctrls_hw_out): - mg_hw_cfg_out["timer"] = find_master(ctrls_hw_out, "timer") - mg_hw_cfg_out["monitor"] = find_master(ctrls_hw_out, "monitor") - return (mg_hw_cfg_out, mg_sw_cfg_out, mg_0d_cfg_out) - - -def getTGConfiguration(MGcfg): - '''Build TG configuration from complete MG configuration. - - TODO: (technical debt) All the MeasurementGroup configuration - logic should be encapsulate in a dedicated class instead of - using a basic data structures like dict or lists... - - :param MGcfg: configuration dictionary of the whole Measurement Group. - :type MGcfg: dict<> - :return: a configuration dictionary of TG elements organized by controller - :rtype: dict<> - ''' - - # Create list with not repeated elements - _tg_element_list = [] - - for ctrl in MGcfg["controllers"]: - tg_element = MGcfg["controllers"][ctrl].get('synchronizer', None) - if (tg_element is not None and - tg_element != "software" and - tg_element not in _tg_element_list): - _tg_element_list.append(tg_element) - - # Intermediate dictionary to organize each ctrl with its elements. - ctrl_tgelem_dict = {} - for tgelem in _tg_element_list: - tg_ctrl = tgelem.get_controller() - if tg_ctrl not in ctrl_tgelem_dict.keys(): - ctrl_tgelem_dict[tg_ctrl] = [tgelem] - else: - ctrl_tgelem_dict[tg_ctrl].append(tgelem) - - # Build TG configuration dictionary. - TGcfg = {} - TGcfg['controllers'] = {} - - for ctrl in ctrl_tgelem_dict: - TGcfg['controllers'][ctrl] = ctrls = {} - ctrls['channels'] = {} - for tg_elem in ctrl_tgelem_dict[ctrl]: - ch = ctrls['channels'][tg_elem] = {} - ch['full_name'] = tg_elem.full_name - # TODO: temporary returning tg_elements - return TGcfg, _tg_element_list - - -def extract_integ_time(synchronization): - """Extract integration time(s) from synchronization dict. If there is only - one group in the synchronization than returns float with the integration - time. Otherwise a list of floats with different integration times. - - TODO: (technical debt) All the MeasurementGroup synchronization - logic should be encapsulate in a dedicated class instead of - using a basic data structures like dict or lists... - - :param synchronization: group(s) where each group is described by - SynchParam(s) - :type synchronization: list(dict) - :return list(float) or float + action_ctrls = [] + for ctrl in ctrls: + action_ctrl = AcqController(ctrl) + action_ctrls.append(action_ctrl) + return action_ctrls + + +def get_timerable_ctrls(ctrls, acq_mode): + """Converts timerable configuration controllers into acquisition + controllers. + + Take care about converting their internals as well. + Take care about assigning master according to acq_mode. + + :param ctrls: sequence of configuration controllers objects + :type ctrls: sardana.pool.poolmeasurementgroup.ControllerConfiguration + :param acq_mode: acquisition mode (timer/monitor) + :type acq_mode: :class:`sardana.pool.AcqMode` + :return: sequence of acquisition controllers + :rtype: :class:`~sardana.pool.poolacquisition.AcqController` + + .. note:: + The get_timerable_ctrls function has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + action_ctrls = [] + for ctrl in ctrls: + attrs = {} + if acq_mode is not None: + master = None + if acq_mode is AcqMode.Timer: + master = ctrl.timer + elif acq_mode is AcqMode.Monitor: + master = ctrl.monitor + attrs = {'master': master} + action_ctrl = AcqController(ctrl, attrs) + action_ctrls.append(action_ctrl) + return action_ctrls + + +def get_timerable_items(ctrls, master, acq_mode=AcqMode.Timer): + """Converts timerable configuration items into acquisition items. + + The timerable items are controllers and master. Convert these into + the corresponding acquisition items. + + Take care about converting their internals as well. + Take care about assigning master according to acq_mode. + + :param ctrls: sequence of configuration controllers objects + :type ctrls: :obj:list<:class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration`> # noqa + :param master: master configuration object + :type master: :class:`~sardana.pool.poolmeasurementgroup.ChannelConfiguration` # noqa + :param acq_mode: acquisition mode (timer/monitor) + :type acq_mode: :class:`sardana.pool.AcqMode` + :return: sequence of acquisition controllers + :rtype: :class:`~sardana.pool.poolacquisition.AcqController` + + .. note:: + The get_timerable_ctrls function has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. """ - if len(synchronization) == 1: - integ_time = synchronization[0][SynchParam.Active][SynchDomain.Time] - else: - integ_time = [] - for group in synchronization: - active_time = group[SynchParam.Active][SynchDomain.Time] - repeats = group[SynchParam.Repeats] - integ_time += [active_time] * repeats - return integ_time - - -def extract_repetitions(synchronization): - """Extract repetitions from synchronization dict. - - TODO: (technical debt) All the MeasurementGroup synchronization - logic should be encapsulate in a dedicated class instead of - using a basic data structures like dict or lists... - - :param synchronization: group(s) where each group is described by - SynchParam(s) - :type synchronization: list(dict) - :return: number of repetitions - :rtype: int + ctrls = get_timerable_ctrls(ctrls, acq_mode) + # Search master AcqConfigurationItem obj + for ctrl in ctrls: + for channel in ctrl.get_channels(): + if channel.configuration == master: + master = channel + break + return ctrls, master + + +class ActionArgs(object): + + def __init__(self, args, kwargs=None): + self.args = args + if kwargs is None: + kwargs = {} + self.kwargs = kwargs + + +class AcqConfigurationItem(object): + """Wrapper for configuration item that will be used in an action. + + .. note:: + The AcqConfigurationItem function has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. """ - repetitions = 0 - for group in synchronization: - repetitions += group[SynchParam.Repeats] - return repetitions + def __init__(self, configuration, attrs=None): + """Constructs action item from a configuration item. -def is_value_error(value): - if isinstance(value, SardanaValue) and value.error: - return True - return False + Eventually it can be enriched with attrs. + + :param configuration: item configuration object + :type configuration: + :class:`sardana.pool.poolmeasurementgroup.ConfigurationItem` + :param attrs: extra attributes to be inserted + :type attrs: dict + """ + self._configuration = weakref.ref(configuration) + self.enabled = True + + if attrs is not None: + self.__dict__.update(attrs) + + def __getattr__(self, item): + return getattr(self.configuration, item) + + def get_configuration(self): + """Returns the element associated with this item""" + return self._configuration() + + def set_configuration(self, configuration): + """Sets the element for this item""" + self._configuration = weakref.ref(configuration) + + configuration = property(get_configuration) + + +class AcqController(AcqConfigurationItem): + """Wrapper for controller configuration that will be used in an action. + + .. note:: + The AcqController function has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + def __init__(self, configuration, attrs=None): + """Constructs action controller from a configuration controller. + + Eventually it can be enriched with attrs. + + :param configuration: controller configuration object + :type configuration: + :class:`sardana.pool.poolmeasurementgroup.ControllerConfiguration` + :param attrs: extra attributes to be inserted + :type attrs: dict + """ + master = None + if attrs is not None: + master = attrs.get('master') + self._channels = [] + self._channels_enabled = [] + self._channels_disabled = [] + ch_attrs = {'controller': self} + for conf_channel in configuration.get_channels(): + action_channel = AcqConfigurationItem(conf_channel, ch_attrs) + self._channels.append(action_channel) + if conf_channel in configuration.get_channels(enabled=True): + self._channels_enabled.append(action_channel) + if conf_channel in configuration.get_channels(enabled=False): + self._channels_disabled.append(action_channel) + if master is None: + continue + if master == conf_channel: + attrs['master'] = action_channel + master = None + AcqConfigurationItem.__init__(self, configuration, attrs) + + def get_channels(self, enabled=None): + if enabled is None: + return list(self._channels) + elif enabled: + return list(self._channels_enabled) + else: + return list(self._channels_disabled) class PoolAcquisition(PoolAction): + """Acquisition action which is internally composed for sub-actions. + + Handle acquisition of experimental channels of the following types: + * timerable (C/T, 1D and 2D) synchronized by software or hardware + trigger/gate/start + * 0D + + Synchronized by T/G elements or sofware synchronizer. + """ def __init__(self, main_element, name="Acquisition"): PoolAction.__init__(self, main_element, name) zerodname = name + ".0DAcquisition" hwname = name + ".HardwareAcquisition" swname = name + ".SoftwareAcquisition" + sw_start_name = name + ".SoftwareStartAcquisition" synchname = name + ".Synchronization" - self._sw_acq_config = None - self._0d_config = None - self._0d_acq = Pool0DAcquisition(main_element, name=zerodname) + self._sw_acq_args = None + self._sw_start_acq_args = None + self._0d_acq_args = None + self._hw_acq_args = None + self._synch_args = None self._sw_acq = PoolAcquisitionSoftware(main_element, name=swname) + self._sw_start_acq = PoolAcquisitionSoftwareStart( + main_element, name=sw_start_name) + self._0d_acq = Pool0DAcquisition(main_element, name=zerodname) self._hw_acq = PoolAcquisitionHardware(main_element, name=hwname) self._synch = PoolSynchronization(main_element, name=synchname) - def set_sw_config(self, config): - self._sw_acq_config = config - - def set_0d_config(self, config): - self._0d_config = config - def event_received(self, *args, **kwargs): + """Callback executed on event of software synchronizer. + + Reacts on start, active, passive or end type of events + """ timestamp = time.time() - _, type_, value = args + _, type_, index = args name = type_.name if name == "state": return t_fmt = '%Y-%m-%d %H:%M:%S.%f' t_str = datetime.datetime.fromtimestamp(timestamp).strftime(t_fmt) - msg = '%s event with id: %d received at: %s' % (name, value, t_str) + msg = '%s event with id: %d received at: %s' % (name, index, t_str) self.debug(msg) - if name == "active": + if name == "start": + if self._sw_start_acq_args is not None: + self.debug('Executing software start acquisition.') + get_thread_pool().add(self._sw_start_acq.run, None, + *self._sw_start_acq_args.args, + **self._sw_start_acq_args.kwargs) + elif name == "active": # this code is not thread safe, but for the moment we assume that # only one EventGenerator will work at the same time - if self._sw_acq_config: + if self._sw_acq_args is not None: if self._sw_acq._is_started() or self._sw_acq.is_running(): msg = ('Skipping trigger: software acquisition is still' ' in progress.') @@ -257,13 +326,12 @@ def event_received(self, *args, **kwargs): return else: self.debug('Executing software acquisition.') - args = () - kwargs = self._sw_acq_config - kwargs['synch'] = True - kwargs['idx'] = value + self._sw_acq_args.kwargs.update({'index': index}) self._sw_acq._started = True - get_thread_pool().add(self._sw_acq.run, *args, **kwargs) - if self._0d_config: + get_thread_pool().add(self._sw_acq.run, None, + *self._sw_acq_args.args, + **self._sw_acq_args.kwargs) + if self._0d_acq_args is not None: if self._0d_acq._is_started() or self._0d_acq.is_running(): msg = ('Skipping trigger: ZeroD acquisition is still in' ' progress.') @@ -271,27 +339,154 @@ def event_received(self, *args, **kwargs): return else: self.debug('Executing ZeroD acquisition.') - args = () - kwargs = self._0d_config - kwargs['synch'] = True - kwargs['idx'] = value + self._0d_acq_args.kwargs.update({'index': index}) self._0d_acq._started = True self._0d_acq._stopped = False self._0d_acq._aborted = False - get_thread_pool().add(self._0d_acq.run, *args, **kwargs) + get_thread_pool().add(self._0d_acq.run, None, + *self._0d_acq_args.args, + **self._0d_acq_args.kwargs) elif name == "passive": - if self._0d_config and (self._0d_acq._is_started() or - self._0d_acq.is_running()): + # TODO: _0d_acq_args comparison may not be necessary + if (self._0d_acq_args is not None + and (self._0d_acq._is_started() + or self._0d_acq.is_running())): self.debug('Stopping ZeroD acquisition.') self._0d_acq.stop_action() + def prepare(self, config, acq_mode, value, synchronization=None, + moveable=None, sw_synch_initial_domain=None, + nb_starts=1, **kwargs): + """Prepare measurement process. + + Organize sub-action arguments and loads configuration parameters to + the hardware controllers. + """ + self._sw_acq_args = None + self._sw_start_acq_args = None + self._0d_acq_args = None + self._hw_acq_args = None + self._synch_args = None + ctrls_hw = [] + ctrls_sw = [] + ctrls_sw_start = [] + + repetitions = synchronization.repetitions + latency = synchronization.passive_time + # Prepare controllers synchronized by hardware + acq_sync_hw = [AcqSynch.HardwareTrigger, AcqSynch.HardwareStart, + AcqSynch.HardwareGate] + ctrls = config.get_timerable_ctrls(acq_synch=acq_sync_hw, enabled=True) + if len(ctrls) > 0: + ctrls_hw = get_timerable_ctrls(ctrls, acq_mode) + hw_args = (ctrls_hw, value, repetitions, latency) + hw_kwargs = {} + hw_kwargs.update(kwargs) + self._hw_acq_args = ActionArgs(hw_args, hw_kwargs) + + # Prepare controllers synchronized by software Trigger and Gate + acq_sync_sw = [AcqSynch.SoftwareGate, AcqSynch.SoftwareTrigger] + ctrls = config.get_timerable_ctrls(acq_synch=acq_sync_sw, enabled=True) + if len(ctrls) > 0: + if acq_mode is AcqMode.Timer: + master = config.get_master_timer_software() + elif acq_mode is AcqMode.Monitor: + master = config.get_master_monitor_software() + + ctrls_sw, master_sw = get_timerable_items(ctrls, master, acq_mode) + + sw_args = (ctrls_sw, value, master_sw) + sw_kwargs = {'synch': True} + sw_kwargs.update(kwargs) + self._sw_acq_args = ActionArgs(sw_args, sw_kwargs) + + # Prepare controllers synchronized by software Start + ctrls = config.get_timerable_ctrls(acq_synch=AcqSynch.SoftwareStart, + enabled=True) + if len(ctrls) > 0: + if acq_mode is AcqMode.Timer: + master = config.get_master_timer_software_start() + elif acq_mode is AcqMode.Monitor: + master = config.get_master_monitor_software_start() + + ctrls_sw_start, master_sw_start = get_timerable_items(ctrls, + master, + acq_mode) + sw_start_args = (ctrls_sw_start, value, master_sw_start, + repetitions, latency) + sw_start_kwargs = {'synch': True} + sw_start_kwargs.update(kwargs) + self._sw_start_acq_args = ActionArgs(sw_start_args, + sw_start_kwargs) + + # Prepare 0D controllers + ctrls = config.get_zerod_ctrls(enabled=True) + if len(ctrls) > 0: + ctrls_acq_0d = get_acq_ctrls(ctrls) + zerod_args = (ctrls_acq_0d,) + zerod_kwargs = {'synch': True} + zerod_kwargs.update(kwargs) + self._0d_acq_args = ActionArgs(zerod_args, zerod_kwargs) + + # Prepare synchronizer controllers + ctrls = config.get_synch_ctrls(enabled=True) + ctrls_synch = get_acq_ctrls(ctrls) + synch_args = (ctrls_synch, synchronization) + synch_kwargs = {'moveable': moveable, + 'sw_synch_initial_domain': sw_synch_initial_domain} + synch_kwargs.update(kwargs) + self._synch_args = ActionArgs(synch_args, synch_kwargs) + + # Load the configuration to the timerable controllers if it changed: + if config.changed: + ctrls = ctrls_hw + ctrls_sw_start + ctrls_sw + + for ctrl in ctrls: + pool_ctrl = ctrl.element + if not pool_ctrl.is_online(): + raise RuntimeError('The controller {0} is ' + 'offline'.format(pool_ctrl.name)) + pool_ctrl.set_ctrl_par('acquisition_mode', acq_mode) + pool_ctrl.operator = self.main_element + pool_ctrl.set_ctrl_par('timer', ctrl.timer.axis) + pool_ctrl.set_ctrl_par('monitor', ctrl.monitor.axis) + synch = config.get_acq_synch_by_controller(pool_ctrl) + pool_ctrl.set_ctrl_par('synchronization', synch) + config.changed = False + + # Call hardware and software start controllers prepare method + ctrls = ctrls_hw + ctrls_sw_start + self._prepare_ctrls(ctrls, value, repetitions, latency, + nb_starts) + + # Call software controllers prepare method + nb_starts = repetitions + repetitions = 1 + self._prepare_ctrls(ctrls_sw, value, repetitions, latency, + nb_starts) + + @staticmethod + def _prepare_ctrls(ctrls, value, repetitions, latency, nb_starts): + for ctrl in ctrls: + axis = ctrl.master.axis + pool_ctrl = ctrl.element + pool_ctrl.ctrl.PrepareOne(axis, value, repetitions, latency, + nb_starts) + def is_running(self): - return self._0d_acq.is_running() or\ - self._sw_acq.is_running() or\ - self._hw_acq.is_running() or\ - self._synch.is_running() + """Checks if acquisition is running. + + Acquisition is runnin if any of its sub-actions is running. + """ + return self._sw_start_acq.is_running()\ + or self._0d_acq.is_running()\ + or self._sw_acq.is_running()\ + or self._hw_acq.is_running()\ + or self._synch.is_running() def run(self, *args, **kwargs): + """Runs acquisition according to previous preparation.""" + for elem in self.get_elements(): elem.put_state(None) # TODO: temporarily clear value buffers at the beginning of the @@ -306,54 +501,41 @@ def run(self, *args, **kwargs): # participate directly in the acquisition for pseudo_elem in elem.get_pseudo_elements(): pseudo_elem.clear_value_buffer() - config = kwargs['config'] - synchronization = kwargs["synchronization"] - integ_time = extract_integ_time(synchronization) - repetitions = extract_repetitions(synchronization) - # TODO: this code splits the global mg configuration into - # experimental channels triggered by hw and experimental channels - # triggered by sw. Refactor it!!!! - (hw_acq_cfg, sw_acq_cfg, zerod_acq_cfg) = split_MGConfigurations( - config) - synch_cfg, _ = getTGConfiguration(config) - # starting continuous acquisition only if there are any controllers - if len(hw_acq_cfg['controllers']): - cont_acq_kwargs = dict(kwargs) - cont_acq_kwargs['config'] = hw_acq_cfg - cont_acq_kwargs['integ_time'] = integ_time - cont_acq_kwargs['repetitions'] = repetitions - self._hw_acq.run(*args, **cont_acq_kwargs) - if len(sw_acq_cfg['controllers']) or len(zerod_acq_cfg['controllers']): + + if self._hw_acq_args is not None: + self._hw_acq.run(*self._hw_acq_args.args, + **self._hw_acq_args.kwargs) + + if self._sw_acq_args is not None\ + or self._sw_start_acq_args is not None\ + or self._0d_acq_args is not None: self._synch.add_listener(self) - if len(sw_acq_cfg['controllers']): - sw_acq_kwargs = dict(kwargs) - sw_acq_kwargs['config'] = sw_acq_cfg - sw_acq_kwargs['integ_time'] = integ_time - sw_acq_kwargs['repetitions'] = 1 - self.set_sw_config(sw_acq_kwargs) - if len(zerod_acq_cfg['controllers']): - zerod_acq_kwargs = dict(kwargs) - zerod_acq_kwargs['config'] = zerod_acq_cfg - self.set_0d_config(zerod_acq_kwargs) - synch_kwargs = dict(kwargs) - synch_kwargs['config'] = synch_cfg - self._synch.run(*args, **synch_kwargs) + + if self._synch_args is not None: + self._synch.run(*self._synch_args.args, + **self._synch_args.kwargs) def _get_action_for_element(self, element): elem_type = element.get_type() if elem_type in TYPE_TIMERABLE_ELEMENTS: - main_element = self.main_element - channel_to_acq_synch = main_element._channel_to_acq_synch - acq_synch = channel_to_acq_synch.get(element) + config = self.main_element.configuration + try: + acq_synch = config.get_acq_synch_by_channel(element) + # when configuration was not yet set and one sets the + # measurement group's integration time (this may happen on Tango + # device initialization when memorized attributes are set we + # fallback to software acquisition + except KeyError: + acq_synch = AcqSynch.SoftwareTrigger if acq_synch in (AcqSynch.SoftwareTrigger, AcqSynch.SoftwareGate): return self._sw_acq + elif acq_synch == AcqSynch.SoftwareStart: + return self._sw_start_acq elif acq_synch in (AcqSynch.HardwareTrigger, - AcqSynch.HardwareGate): + AcqSynch.HardwareGate, + AcqSynch.HardwareStart): return self._hw_acq - else: - # by default software synchronization is in use - return self._sw_acq elif elem_type == ElementType.ZeroDExpChannel: return self._0d_acq elif elem_type == ElementType.TriggerGate: @@ -412,6 +594,7 @@ def get_pool_controllers(self): ret = {} ret.update(self._hw_acq.get_pool_controllers()) ret.update(self._sw_acq.get_pool_controllers()) + ret.update(self._sw_start_acq.get_pool_controllers()) ret.update(self._0d_acq.get_pool_controllers()) return ret @@ -434,17 +617,6 @@ def read_value(self, ret=None, serial=False): return ret -class Channel(PoolActionItem): - - def __init__(self, acquirable, info=None): - PoolActionItem.__init__(self, acquirable) - if info: - self.__dict__.update(info) - - def __getattr__(self, name): - return getattr(self.element, name) - - class PoolAcquisitionBase(PoolAction): """Base class for acquisitions with a generic start_action method. @@ -457,7 +629,12 @@ class PoolAcquisitionBase(PoolAction): def __init__(self, main_element, name): PoolAction.__init__(self, main_element, name) - self._channels = None + self._channels = [] + self._index = None + self._nb_states_per_value = None + self._acq_sleep_time = None + self._pool_ctrl_dict_loop = None + # TODO: for the moment we can not clear value buffers at the end of # the acquisition. This is because of the pseudo counters that are # based on channels synchronized by hardware and software. @@ -484,193 +661,173 @@ def in_acquisition(self, states): return True @DebugIt() - def start_action(self, *args, **kwargs): - """Prepares everything for acquisition and starts it. - :param acq_sleep_time: sleep time between state queries - :param nb_states_per_value: how many state queries between readouts - :param integ_time: integration time(s) - :type integ_time: float or seq + def start_action(self, ctrls, value, master, repetitions, latency, + index, acq_sleep_time, nb_states_per_value, + **kwargs): + """ + Prepares everything for acquisition and starts it + :param ctrls: List of enabled pool acquisition controllers + :type ctrls: list + :param value: integration time/monitor counts + :type value: float/int or seq :param repetitions: repetitions :type repetitions: int - :param config: configuration dictionary (with information about - involved controllers and channels) + :param latency: + :type latency: float + :param master: master channel is the last one to start + :type master: Channel + :param index: + :type index: int + :param acq_sleep_time: sleep time between state queries + :type acq_sleep_time: float + :param nb_states_per_value: how many state queries between readouts + :type nb_states_per_value: int + :param args: + :param kwargs: + :return: """ - pool = self.pool + pool = self.pool self._aborted = False self._stopped = False - self._acq_sleep_time = kwargs.pop("acq_sleep_time", - pool.acq_loop_sleep_time) - self._nb_states_per_value = kwargs.pop("nb_states_per_value", - pool.acq_loop_states_per_value) - - self._integ_time = integ_time = kwargs.get("integ_time") - self._mon_count = mon_count = kwargs.get("monitor_count") - self._repetitions = repetitions = kwargs.get("repetitions") - if integ_time is None and mon_count is None: - raise Exception("must give integration time or monitor counts") - if integ_time is not None and mon_count is not None: - msg = ("must give either integration time or monitor counts " - "(not both)") - raise Exception(msg) - - _ = kwargs.get("items", self.get_elements()) - cfg = kwargs['config'] - # determine which is the controller which holds the master channel - - if integ_time is not None: - master_key = 'timer' - master_value = integ_time - if mon_count is not None: - master_key = 'monitor' - master_value = -mon_count - master = cfg[master_key] - if master is None: - self.main_element.set_state(State.Fault, propagate=2) - msg = "master {0} is unknown (probably disabled)".format( - master_key) - raise RuntimeError(msg) - master_ctrl = master.controller - - pool_ctrls_dict = dict(cfg['controllers']) - pool_ctrls_dict.pop('__tango__', None) - - # controllers to be started (only enabled) in the right order - pool_ctrls = [] - # controllers that will be read at the end of the action - self._pool_ctrl_dict_loop = _pool_ctrl_dict_loop = {} - # channels that are acquired (only enabled) - self._channels = channels = {} + self._index = index - # select only suitable e.g. enabled, timerable controllers & channels - for ctrl, pool_ctrl_data in pool_ctrls_dict.items(): - # skip not timerable controllers e.g. 0D - if not ctrl.is_timerable(): - continue - ctrl_enabled = False - elements = pool_ctrl_data['channels'] - for element, element_info in elements.items(): - # skip disabled elements - if not element_info['enabled']: - continue - # Add only the enabled channels - channel = Channel(element, info=element_info) - channels[element] = channel - ctrl_enabled = True - # check if the ctrl has enabled channels - if ctrl_enabled: - # enabled controller can no be offline - if not ctrl.is_online(): - self.main_element.set_state(State.Fault, propagate=2) - msg = "controller {0} is offline".format(ctrl.name) - raise RuntimeError(msg) - pool_ctrls.append(ctrl) - # only CT will be read in the loop, 1D and 2D not - if ElementType.CTExpChannel in ctrl.get_ctrl_types(): - _pool_ctrl_dict_loop[ctrl] = pool_ctrl_data - - # timer/monitor channels can not be disabled - for pool_ctrl in pool_ctrls: - ctrl = pool_ctrl.ctrl - pool_ctrl_data = pool_ctrls_dict[pool_ctrl] - timer_monitor = pool_ctrl_data[master_key] - if timer_monitor not in channels: - self.main_element.set_state(State.Fault, propagate=2) - msg = "timer/monitor ({0}) of {1} controller is "\ - "disabled)".format(timer_monitor.name, pool_ctrl.name) - raise RuntimeError(msg) + self._acq_sleep_time = acq_sleep_time + if self._acq_sleep_time is None: + self._acq_sleep_time = pool.acq_loop_sleep_time + + self._nb_states_per_value = nb_states_per_value + if self._nb_states_per_value is None: + self._nb_states_per_value = pool.acq_loop_states_per_value # make sure the controller which has the master channel is the last to # be called - pool_ctrls.remove(master_ctrl) - pool_ctrls.append(master_ctrl) + if master is not None: + ctrls.remove(master.controller) + ctrls.append(master.controller) + + # controllers that will be read at during the action + self._set_pool_ctrl_dict_loop(ctrls) + + # channels that are acquired (only enabled) + self._channels = [] + + def load(channel, value, repetitions, latency=0): + axis = channel.axis + pool_ctrl = channel.controller + ctrl = pool_ctrl.ctrl + ctrl.PreLoadAll() + try: + res = ctrl.PreLoadOne(axis, value, repetitions, + latency) + except TypeError: + try: + res = ctrl.PreLoadOne(axis, value, repetitions) + msg = ("PreLoadOne(axis, value, repetitions) is " + "deprecated since version 2.7.0. Use PreLoadOne(" + "axis, value, repetitions, latency_time) instead.") + self.warning(msg) + except TypeError: + res = ctrl.PreLoadOne(axis, value) + msg = ("PreLoadOne(axis, value) is deprecated since " + "version 2.3.0. Use PreLoadOne(axis, value, " + "repetitions, latency_time) instead.") + self.warning(msg) + if not res: + msg = ("%s.PreLoadOne(%d) returned False" % + (pool_ctrl.name, axis)) + raise Exception(msg) + try: + ctrl.LoadOne(axis, value, repetitions, latency) + except TypeError: + try: + ctrl.LoadOne(axis, value, repetitions) + msg = ("LoadOne(axis, value, repetitions) is deprecated " + "since version Jan18. Use LoadOne(axis, value, " + "repetitions, latency_time) instead.") + self.warning(msg) + except TypeError: + ctrl.LoadOne(axis, value) + msg = ("LoadOne(axis, value) is deprecated since " + "version 2.3.0. Use LoadOne(axis, value, " + "repetitions) instead.") + self.warning(msg) + ctrl.LoadAll() with ActionContext(self): # PreLoadAll, PreLoadOne, LoadOne and LoadAll - for pool_ctrl in pool_ctrls: - try: - ctrl = pool_ctrl.ctrl - pool_ctrl_data = pool_ctrls_dict[pool_ctrl] - ctrl.PreLoadAll() - master = pool_ctrl_data[master_key] - axis = master.axis - try: - res = ctrl.PreLoadOne(axis, master_value, repetitions) - except TypeError: - msg = ("PreLoadOne(axis, value) is deprecated since " - "version 2.3.0. Use PreLoadOne(axis, value, " - "repetitions) instead.") - self.warning(msg) - res = ctrl.PreLoadOne(axis, master_value) - if not res: - msg = ("%s.PreLoadOne(%d) returned False" % - (pool_ctrl.name, axis)) - raise Exception(msg) - try: - ctrl.LoadOne(axis, master_value, repetitions) - except TypeError: - msg = ("LoadOne(axis, value) is deprecated since " - "version 2.3.0. Use LoadOne(axis, value, " - "repetitions) instead.") - self.warning(msg) - ctrl.LoadOne(axis, master_value) - ctrl.LoadAll() - except Exception, e: - self.debug(e, exc_info=True) - master.set_state(State.Fault, propagate=2) - msg = ("Load sequence of %s failed" % pool_ctrl.name) - raise Exception(msg) + for ctrl in ctrls: + # TODO find solution for master now sardana only use timer + load(ctrl.timer, value, repetitions, latency) + + # TODO: remove when the action allows to use tango attributes + try: + ctrls.pop('__tango__') + except Exception: + pass # PreStartAll on all enabled controllers - for pool_ctrl in pool_ctrls: + for ctrl in ctrls: + pool_ctrl = ctrl.element pool_ctrl.ctrl.PreStartAll() # PreStartOne & StartOne on all enabled elements - for pool_ctrl in pool_ctrls: - ctrl = pool_ctrl.ctrl - pool_ctrl_data = pool_ctrls_dict[pool_ctrl] - elements = pool_ctrl_data['channels'].keys() - timer_monitor = pool_ctrl_data[master_key] - # make sure that the timer/monitor is started as the last one - elements.remove(timer_monitor) - elements.append(timer_monitor) - for element in elements: - try: - channel = channels[element] - except KeyError: - continue - axis = element.axis - ret = ctrl.PreStartOne(axis, master_value) + for ctrl in ctrls: + channels = ctrl.get_channels(enabled=True) + + # make sure that the master timer/monitor is started as the + # last one + channels.remove(ctrl.master) + channels.append(ctrl.master) + for channel in channels: + axis = channel.axis + pool_ctrl = ctrl.element + ret = pool_ctrl.ctrl.PreStartOne(axis, value) if not ret: msg = ("%s.PreStartOne(%d) returns False" % - (pool_ctrl.name, axis)) + (ctrl.name, axis)) raise Exception(msg) try: - ctrl.StartOne(axis, master_value) - except Exception, e: + pool_ctrl = ctrl.element + pool_ctrl.ctrl.StartOne(axis, value) + except Exception as e: self.debug(e, exc_info=True) - element.set_state(State.Fault, propagate=2) + channel.set_state(State.Fault, propagate=2) msg = ("%s.StartOne(%d) failed" % - (pool_ctrl.name, axis)) + (ctrl.name, axis)) raise Exception(msg) + self._channels.append(channel) + # set the state of all elements to and inform their listeners - for channel in channels: + for channel in self._channels: channel.set_state(State.Moving, propagate=2) # StartAll on all enabled controllers - for pool_ctrl in pool_ctrls: + for ctrl in ctrls: try: + pool_ctrl = ctrl.element pool_ctrl.ctrl.StartAll() - except Exception, e: + except Exception as e: + channels = ctrl.get_channels(enabled=True) self.debug(e, exc_info=True) - elements = pool_ctrl_data['channels'].keys() - for element in elements: - element.set_state(State.Fault, propagate=2) - msg = ("%s.StartAll() failed" % pool_ctrl.name) + for channel in channels: + channel.set_state(State.Fault, propagate=2) + msg = ("%s.StartAll() failed" % ctrl.name) raise Exception(msg) + def _set_pool_ctrl_dict_loop(self, ctrls): + ctrl_channels = {} + for ctrl in ctrls: + pool_channels = [] + pool_ctrl = ctrl.element + # TODO: filter 1D and 2D for software synchronize acquisition + for channel in ctrl.get_channels(enabled=True): + pool_channels.append(channel.element) + ctrl_channels[pool_ctrl] = pool_channels + self._pool_ctrl_dict_loop = ctrl_channels + def clear_value_buffers(self): for channel in self._channels: channel.clear_value_buffer() @@ -684,17 +841,29 @@ class PoolAcquisitionHardware(PoolAcquisitionBase): on a provisional basis. Backwards incompatible changes (up to and including removal of the module) may occur if deemed necessary by the core developers. + + .. todo:: Try to move the action loop logic to base class it is + basically the same as in PoolAcquisitionSoftwareStart. """ def __init__(self, main_element, name="AcquisitionHardware"): PoolAcquisitionBase.__init__(self, main_element, name) + def start_action(self, ctrls, value, repetitions, latency, + acq_sleep_time=None, nb_states_per_value=None, + **kwargs): + PoolAcquisitionBase.start_action(self, ctrls, value, None, + repetitions, latency, None, + acq_sleep_time, nb_states_per_value, + **kwargs) + @DebugIt() def action_loop(self): i = 0 states, values = {}, {} - for element in self._channels: + for channel in self._channels: + element = channel.element states[element] = None values[element] = None @@ -759,30 +928,21 @@ def __init__(self, main_element, name="AcquisitionSoftware", slaves=None): slaves = () self._slaves = slaves - @DebugIt() - def start_action(self, *args, **kwargs): - """Prepares everything for acquisition and starts it. - :param acq_sleep_time: sleep time between state queries - :param nb_states_per_value: how many state queries between readouts - :param integ_time: integration time(s) - :type integ_time: float or seq - :param repetitions: repetitions - :type repetitions: int - :param config: configuration dictionary (with information about - involved controllers and channels) - :param index: trigger index that will be assigned to the acquired value - :type index: int - """ + def get_read_value_loop_ctrls(self): + return self._pool_ctrl_dict_loop - PoolAcquisitionBase.start_action(self, *args, **kwargs) - self.index = kwargs.get("idx") + def start_action(self, ctrls, value, master, index, acq_sleep_time=None, + nb_states_per_value=None, **kwargs): + PoolAcquisitionBase.start_action(self, ctrls, value, master, 1, 0, + index, acq_sleep_time, + nb_states_per_value, **kwargs) @DebugIt() def action_loop(self): states, values = {}, {} - for element in self._channels: + for channel in self._channels: + element = channel.element states[element] = None - values[element] = None nap = self._acq_sleep_time nb_states_per_value = self._nb_states_per_value @@ -810,6 +970,82 @@ def action_loop(self): slave.getLogName()) self.debug("Details", exc_info=1) + with ActionContext(self): + self.raw_read_state_info(ret=states) + self.raw_read_value(ret=values) + + for acquirable, state_info in states.items(): + # first update the element state so that value calculation + # that is done after takes the updated state into account + acquirable.set_state_info(state_info, propagate=0) + if acquirable in values: + value = values[acquirable] + if is_value_error(value): + self.error("Loop final read value error for: %s" % + acquirable.name) + acquirable.append_value_buffer(value, self._index) + with acquirable: + acquirable.clear_operation() + state_info = acquirable._from_ctrl_state_info(state_info) + acquirable.set_state_info(state_info, propagate=2) + + +class PoolAcquisitionSoftwareStart(PoolAcquisitionBase): + """Acquisition action for controllers synchronized by software start + + .. note:: + The PoolAcquisitionSoftwareStart class has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the module) may occur if + deemed necessary by the core developers. + + .. todo:: Try to move the action loop logic to base class it is + basically the same as in PoolAcquisitionHardware. + """ + + def __init__(self, main_element, name="AcquisitionSoftwareStart"): + PoolAcquisitionBase.__init__(self, main_element, name) + + def start_action(self, ctrls, value, master, repetitions, latency, + acq_sleep_time=None, nb_states_per_value=None, + **kwargs): + PoolAcquisitionBase.start_action(self, ctrls, value, master, + repetitions, latency, None, + acq_sleep_time, nb_states_per_value, + **kwargs) + + @DebugIt() + def action_loop(self): + i = 0 + + states, values = {}, {} + for channel in self._channels: + element = channel.element + states[element] = None + values[element] = None + + nap = self._acq_sleep_time + nb_states_per_value = self._nb_states_per_value + + while True: + self.read_state_info(ret=states) + if not self.in_acquisition(states): + break + + # read value every n times + if not i % nb_states_per_value: + self.read_value_loop(ret=values) + for acquirable, value in values.items(): + if is_value_error(value): + self.error("Loop read value error for %s" % + acquirable.name) + acquirable.put_value(value) + else: + acquirable.extend_value_buffer(value) + + time.sleep(nap) + i += 1 + with ActionContext(self): self.raw_read_state_info(ret=states) self.raw_read_value_loop(ret=values) @@ -823,7 +1059,9 @@ def action_loop(self): if is_value_error(value): self.error("Loop final read value error for: %s" % acquirable.name) - acquirable.append_value_buffer(value, self.index) + acquirable.put_value(value) + else: + acquirable.extend_value_buffer(value, propagate=2) with acquirable: acquirable.clear_operation() state_info = acquirable._from_ctrl_state_info(state_info) @@ -920,58 +1158,42 @@ class Pool0DAcquisition(PoolAction): def __init__(self, main_element, name="0DAcquisition"): self._channels = None + self._index = None PoolAction.__init__(self, main_element, name) - def start_action(self, *args, **kwargs): + def start_action(self, conf_ctrls, index, acq_sleep_time=None, + nb_states_per_value=None, **kwargs): """Prepares everything for acquisition and starts it. :param: config""" pool = self.pool - - self._index = kwargs.get("idx") - - # prepare data structures # TODO: rollback this change when a proper synchronization between # acquisition actions will be develop. # Now the meta acquisition action is resettung them to 0. -# self._aborted = False -# self._stopped = False - - self._acq_sleep_time = kwargs.pop("acq_sleep_time", - pool.acq_loop_sleep_time) - self._nb_states_per_value = \ - kwargs.pop("nb_states_per_value", - pool.acq_loop_states_per_value) - - items = kwargs.get("items") - if items is None: - items = self.get_elements() - cfg = kwargs['config'] - - pool_ctrls_dict = dict(cfg['controllers']) - pool_ctrls_dict.pop('__tango__', None) - pool_ctrls = [] - for ctrl in pool_ctrls_dict: - if ElementType.ZeroDExpChannel in ctrl.get_ctrl_types(): - pool_ctrls.append(ctrl) - - # Determine which channels are active - self._channels = channels = {} - for pool_ctrl in pool_ctrls: - ctrl = pool_ctrl.ctrl - pool_ctrl_data = pool_ctrls_dict[pool_ctrl] - elements = pool_ctrl_data['channels'] + # self._aborted = False + # self._stopped = False - for element, element_info in elements.items(): - channel = Channel(element, info=element_info) - channels[element] = channel + self._index = index + self._acq_sleep_time = acq_sleep_time + if self._acq_sleep_time is None: + self._acq_sleep_time = pool.acq_loop_sleep_time + + self._nb_states_per_value = nb_states_per_value + if self._nb_states_per_value is None: + self._nb_states_per_value = pool.acq_loop_states_per_value + + # channels that are acquired (only enabled) + self._channels = [] with ActionContext(self): # set the state of all elements to and inform their listeners - for channel in channels: - channel.clear_buffer() - channel.set_state(State.Moving, propagate=2) + + for conf_ctrl in conf_ctrls: + for conf_channel in conf_ctrl.get_channels(enabled=True): + conf_channel.clear_buffer() + conf_channel.set_state(State.Moving, propagate=2) + self._channels.append(conf_channel) def in_acquisition(self, states): """Determines if we are in acquisition or if the acquisition has ended @@ -990,7 +1212,8 @@ def in_acquisition(self, states): def action_loop(self): states, values = {}, {} - for element in self._channels: + for conf_channel in self._channels: + element = conf_channel.element states[element] = None values[element] = None diff --git a/src/sardana/pool/poolbasechannel.py b/src/sardana/pool/poolbasechannel.py index 9cebdaa2d9..fa2ec1c125 100644 --- a/src/sardana/pool/poolbasechannel.py +++ b/src/sardana/pool/poolbasechannel.py @@ -33,8 +33,13 @@ from sardana.sardanaattribute import SardanaAttribute from sardana.sardanabuffer import SardanaBuffer from sardana.pool.poolelement import PoolElement -from sardana.pool.poolacquisition import PoolCTAcquisition +from sardana.pool.poolacquisition import PoolAcquisitionSoftware,\ + get_timerable_items +from sardana.pool.poolmeasurementgroup import ChannelConfiguration,\ + ControllerConfiguration +from sardana.sardanaevent import EventType +from sardana.pool import AcqSynch, AcqMode class ValueBuffer(SardanaBuffer): @@ -68,7 +73,7 @@ class PoolBaseChannel(PoolElement): ValueAttributeClass = Value ValueBufferClass = ValueBuffer - AcquisitionClass = PoolCTAcquisition + AcquisitionClass = PoolAcquisitionSoftware def __init__(self, **kwargs): PoolElement.__init__(self, **kwargs) @@ -79,6 +84,7 @@ def __init__(self, **kwargs): if not self.AcquisitionClass is None: acq_name = "%s.Acquisition" % self._name self.set_action_cache(self.AcquisitionClass(self, name=acq_name)) + self._integration_time = 0 def has_pseudo_elements(self): """Informs whether this channel forms part of any pseudo element @@ -278,13 +284,130 @@ def clear_value_buffer(self): val_attr = self._value_buffer val_attr.clear() - def start_acquisition(self, value=None): + # -------------------------------------------------------------------------- + # integration time + # -------------------------------------------------------------------------- + + def get_integration_time(self): + """Return the integration time for this object. + + :return: the current integration time + :rtype: :obj:`float`""" + return self._integration_time + + def set_integration_time(self, integration_time, propagate=1): + """Set the integration time for this object. + + :param integration_time: integration time in seconds to set + :type integration_time: :obj:`float` + :param propagate: + 0 for not propagating, 1 to propagate, 2 propagate with priority + :type propagate: :obj:`int` + """ + if integration_time == self._integration_time: + # integration time is not changed. Do nothing + return + self._integration_time = integration_time + if not propagate: + return + self.fire_event(EventType("integration_time", priority=propagate), + integration_time) + + integration_time = property(get_integration_time, set_integration_time, + doc="channel integration time") + + def start_acquisition(self): + msg = "{0} does not support independent acquisition".format( + self.__class__.__name__) + raise NotImplementedError(msg) + + +class PoolTimerableChannel(PoolBaseChannel): + + def __init__(self, **kwargs): + PoolBaseChannel.__init__(self, **kwargs) + # TODO: part of the configuration could be moved to the base class + # so other experimental channels could reuse it + self._conf_channel = ChannelConfiguration(self) + self._conf_ctrl = ControllerConfiguration(self.controller) + self._conf_ctrl.add_channel(self._conf_channel) + self._timer = "__default" + self._conf_timer = None + + # ------------------------------------------------------------------------ + # timer + # ------------------------------------------------------------------------ + + def get_timer(self): + """Return the timer for this object. + + :return: the current timer + :rtype: :obj:`str` + """ + return self._timer + + def set_timer(self, timer, propagate=1): + """Set timer for this object. + + :param timer: new timer to set + :type timer: :obj:`str` + :param propagate: + 0 for not propagating, 1 to propagate, 2 propagate with priority + :type propagate: :obj:`int` + """ + if timer == self._timer: + return + if self._conf_timer is not None: + timer_elem = self._conf_timer.element + if timer_elem is not self: + self.acquisition.remove_element(timer_elem) + self._conf_ctrl.remove_channel(self._conf_timer) + self._timer = timer + self._conf_timer = None + if not propagate: + return + self.fire_event(EventType("timer", priority=propagate), timer) + + timer = property(get_timer, set_timer, + doc="timer for the timerable channel") + + def _configure_timer(self): + timer = self.timer + if timer == "__self": + conf_timer = self._conf_channel + else: + ctrl = self.get_controller() + if timer == "__default": + axis = ctrl.get_default_timer() + if axis is None: + msg = "default_timer not defined in controller" + raise ValueError(msg) + timer_elem = ctrl.get_element(axis=axis) + else: + timer_elem = self.pool.get_element_by_name(timer) + if timer_elem is self: + conf_timer = self._conf_channel + else: + self.acquisition.add_element(timer_elem) + conf_timer = ChannelConfiguration(timer_elem) + self._conf_ctrl.add_channel(conf_timer) + self._conf_ctrl.timer = conf_timer + self._conf_timer = conf_timer + + def start_acquisition(self): + """Start software triggered acquisition""" self._aborted = False self._stopped = False - if value is None: - value = self.get_write_value() - if value is None: - raise Exception( - "Invalid integration_time '%s'. Hint set a new value for 'value' first" % value) - if not self._simulation_mode: - acq = self.acquisition.run(integ_time=value) + if self._simulation_mode: + return + if self.timer is None: + msg = "no timer configured - acquisition is not possible" + raise RuntimeError(msg) + + if self._conf_timer is None: + self._configure_timer() + self.controller.set_ctrl_par("synchronization", + AcqSynch.SoftwareTrigger) + ctrls, master = get_timerable_items( + [self._conf_ctrl], self._conf_timer, AcqMode.Timer) + self.acquisition.run(ctrls, self.integration_time, master, None) diff --git a/src/sardana/pool/poolcontroller.py b/src/sardana/pool/poolcontroller.py index fe617468ad..10eec61fc7 100644 --- a/src/sardana/pool/poolcontroller.py +++ b/src/sardana/pool/poolcontroller.py @@ -922,6 +922,28 @@ def write_one(self, axis, value): # END SPECIFIC TO IOR CONTROLLER ----------------------------------------- + # START SPECIFIC TO TIMERABLE CONTROLLER --------------------------------- + + def get_default_timer(self): + """Get default timer as announced by the controller (plug-in). + + Only for *Timerable* controllers, e.g. + :class:`~sardana.pool.controller.CounterTimerController`, + :class:`~sardana.pool.controller.OneDController`, + :class:`~sardana.pool.controller.TwoDController`. + + :return: axis of the default timer or :obj:`None` if not defined + :rtype: :obj:`int` or :obj:`None` + """ + if not self.is_timerable(): + raise TypeError("non-timerable controller") + try: + return self.ctrl.default_timer + except AttributeError: + return None + + # END SPECIFIC TO IOR CONTROLLER ----------------------------------------- + class PoolPseudoMotorController(PoolController): diff --git a/src/sardana/pool/poolcontrollers/DummyCounterTimerController.py b/src/sardana/pool/poolcontrollers/DummyCounterTimerController.py index 3f94eb7886..036aa7558c 100644 --- a/src/sardana/pool/poolcontrollers/DummyCounterTimerController.py +++ b/src/sardana/pool/poolcontrollers/DummyCounterTimerController.py @@ -30,17 +30,25 @@ Description, Memorize, NotMemorized -class Channel: +class Channel(object): def __init__(self, idx): self.idx = idx # 1 based index self.value = 0.0 self.is_counting = False self.active = True - self.repetitions = 0 + self.repetitions = 1 + self.acq_latency_time = 0 self._counter = 0 self.mode = AcqSynch.SoftwareTrigger self.buffer_values = [] + self.estimated_duration = None + + def calculate_duration(self, intergration_time): + if self.mode not in (AcqSynch.SoftwareStart, AcqSynch.HardwareStart): + self.acq_latency_time = 0 + self.estimated_duration = (intergration_time + + self.acq_latency_time) * self.repetitions class DummyCounterTimerController(CounterTimerController): @@ -57,6 +65,8 @@ class DummyCounterTimerController(CounterTimerController): MonitorMode = 2 CounterMode = 3 + default_timer = 1 + def __init__(self, inst, props, *args, **kwargs): CounterTimerController.__init__(self, inst, props, *args, **kwargs) self._synchronization = AcqSynch.SoftwareTrigger @@ -117,12 +127,13 @@ def _updateChannelState(self, ind, elapsed_time): v = int(elapsed_time * 100 * ind) if v >= self.monitor_count: self._finish(elapsed_time) - elif channel.mode in (AcqSynch.HardwareTrigger, AcqSynch.HardwareGate): + elif channel.mode in (AcqSynch.HardwareTrigger, + AcqSynch.HardwareGate, + AcqSynch.SoftwareStart, + AcqSynch.HardwareStart): if self.integ_time is not None: # counting in time - # if elapsed_time >= self.integ_time*channel.repetitions: - if elapsed_time > channel.repetitions * (self.integ_time + - self._latency_time): + if elapsed_time > channel.estimated_duration: # if elapsed_time >= self.integ_time: self._finish(elapsed_time) @@ -142,7 +153,10 @@ def _updateChannelValue(self, ind, elapsed_time): if ind == self._monitor: if not channel.is_counting: channel.value = self.monitor_count - elif channel.mode in (AcqSynch.HardwareTrigger, AcqSynch.HardwareGate): + elif channel.mode in (AcqSynch.HardwareTrigger, + AcqSynch.HardwareGate, + AcqSynch.SoftwareStart, + AcqSynch.HardwareStart): if self.integ_time is not None: t = elapsed_time n = int(t / self.integ_time) @@ -192,7 +206,10 @@ def ReadOne(self, ind): self._log.debug('ReadOne(%d): entering...' % ind) channel = self.read_channels[ind] ret = None - if channel.mode in (AcqSynch.HardwareTrigger, AcqSynch.HardwareGate): + if channel.mode in (AcqSynch.HardwareTrigger, + AcqSynch.HardwareGate, + AcqSynch.SoftwareStart, + AcqSynch.HardwareStart): values = copy.deepcopy(channel.buffer_values) ret = [] for v in values: @@ -225,17 +242,19 @@ def StartOne(self, ind, value=None): def StartAll(self): self.start_time = time.time() - def LoadOne(self, ind, value, repetitions): + def LoadOne(self, ind, value, repetitions, latency_time): if value > 0: self.integ_time = value self.monitor_count = None else: self.integ_time = None self.monitor_count = -value - self._repetitions = repetitions + for channel in self.channels: if channel: channel.repetitions = repetitions + channel.acq_latency_time = latency_time + channel.calculate_duration(value) def AbortOne(self, ind): now = time.time() diff --git a/src/sardana/pool/poolcontrollers/HklPseudoMotorController.py b/src/sardana/pool/poolcontrollers/HklPseudoMotorController.py index 738671660d..9de372e631 100644 --- a/src/sardana/pool/poolcontrollers/HklPseudoMotorController.py +++ b/src/sardana/pool/poolcontrollers/HklPseudoMotorController.py @@ -452,7 +452,7 @@ def CalcAllPhysical(self, pseudo_pos, curr_physical_pos): values = None if engine_name == "hkl": values = [pseudo_pos[0], pseudo_pos[1], pseudo_pos[2]] - elif self.nb_ph_axes == 4: # noqa "E4CV", "E4CH", "SOLEIL MARS": hkl(3), psi(1), q(1); "K4CV": hkl(3), psi(1), q(1), eulerians(3); "SOLEIL SIXS MED1+2": hkl(3), q2(2), qper_qpar(2) + elif self.nb_ph_axes == 4: # noqa "E4CV", "E4CH", "SOLEIL MARS": hkl(3), psi(1), q(1); "K4CV": hkl(3), psi(1), q(1), eulerians(3); "SOLEIL SIXS MED1+2": hkl(3), q2(2), qper_qpar(2); "PETRA3 P23 4C": hkl(3), q2(2), qper_qpar(2), tth2(2), petra3_p23_4c_incidence(1), _petra3_p23_4c_emergence(1) if engine_name == "psi": values = [pseudo_pos[3]] elif engine_name == "q": @@ -463,7 +463,13 @@ def CalcAllPhysical(self, pseudo_pos, curr_physical_pos): values = [pseudo_pos[3], pseudo_pos[4]] elif engine_name == "qper_qpar": values = [pseudo_pos[5], pseudo_pos[6]] - elif self.nb_ph_axes == 6: # noqa "E6C", "SOLEIL SIXS MED2+2": hkl(3), psi(1), q2(2), qper_qpar(2); "K6C": hkl(3), psi(1), q2(2), qper_qpar(2), eulerians(3); "PETRA3 P09 EH2": hkl(3) + elif engine_name == "tth2": + values = [pseudo_pos[7], pseudo_pos[8]] + elif engine_name == "petra3_p23_4c_incidence": + values = [pseudo_pos[9]] + elif engine_name == "petra3_p23_4c_emergence": + values = [pseudo_pos[10]] + elif self.nb_ph_axes == 6: # noqa "E6C", "SOLEIL SIXS MED2+2": hkl(3), psi(1), q2(2), qper_qpar(2); "K6C": hkl(3), psi(1), q2(2), qper_qpar(2), eulerians(3); "PETRA3 P09 EH2": hkl(3), "PETRA3 P23 6C": hkl(3), psi(1), q2(2), qper_qpar(2), tth2(2), petra3_p23_6c_incidence(1), _petra3_p23_6c_emergence(1) if engine_name == "psi": values = [pseudo_pos[3]] elif engine_name == "q2": @@ -472,6 +478,12 @@ def CalcAllPhysical(self, pseudo_pos, curr_physical_pos): values = [pseudo_pos[6], pseudo_pos[7]] elif engine_name == "eulerians": values = [pseudo_pos[8], pseudo_pos[9], pseudo_pos[10]] + elif engine_name == "tth2": + values = [pseudo_pos[8], pseudo_pos[9]] + elif engine_name == "petra3_p23_6c_incidence": + values = [pseudo_pos[10]] + elif engine_name == "petra3_p23_6c_emergence": + values = [pseudo_pos[11]] # getWavelength updates wavelength in the library in case automatic # energy update is set. Needed before computing trajectories. @@ -1396,6 +1408,22 @@ def setComputeHKL(self, value): # 6C Diffractometers #################### +class Diffrac6Cp23(DiffracBasis): # DiffractometerType: "PETRA3 P23 6C" + + """ The PseudoMotor controller for the diffractometer""" + + pseudo_motor_roles = "h", "k", "l", "psi", "q", "alpha", "tth2", + "alpha_tth2", "incidence", "emergence" + motor_roles = "omega_t", "mu", "omega", "chi", "phi", "gamma", "delta" + + def __init__(self, inst, props, *args, **kwargs): + """ Do the default init plus the specific diffractometer + staff. + @param properties of the controller + """ + + DiffracBasis.__init__(self, inst, props, *args, **kwargs) + class Diffrac6C(DiffracBasis): # DiffractometerType: "PETRA3 P09 EH2" """ The PseudoMotor controller for the diffractometer""" @@ -1430,6 +1458,22 @@ def __init__(self, inst, props, *args, **kwargs): # 4C Diffractometers #################### +class Diffrac4Cp23(DiffracBasis): # DiffractometerType: "PETRA3 P23 4C" + + """ The PseudoMotor controller for the diffractometer""" + + pseudo_motor_roles = "h", "k", "l", "q", "alpha", "qper", "qpar", + "tth2", "alpha_tth2", "incidence", "emergence" + motor_roles = "omega_t", "mu", "gamma", "delta" + + def __init__(self, inst, props, *args, **kwargs): + """ Do the default init plus the specific diffractometer + staff. + @param properties of the controller + """ + + DiffracBasis.__init__(self, inst, props, *args, **kwargs) + class DiffracE4C(DiffracBasis): """ The PseudoMotor controller for the diffractometer""" diff --git a/src/sardana/pool/poolcontrollers/TaurusController.py b/src/sardana/pool/poolcontrollers/TaurusController.py old mode 100755 new mode 100644 diff --git a/src/sardana/pool/poolcontrollers/test/test_DummyTriggerGateController.py b/src/sardana/pool/poolcontrollers/test/test_DummyTriggerGateController.py index 9119a6377d..bb54e75288 100644 --- a/src/sardana/pool/poolcontrollers/test/test_DummyTriggerGateController.py +++ b/src/sardana/pool/poolcontrollers/test/test_DummyTriggerGateController.py @@ -4,10 +4,9 @@ from sardana.pool.poolsynchronization import PoolSynchronization from sardana.pool.pooldefs import SynchDomain, SynchParam -from sardana.pool.test import (FakePool, createPoolController, - createPoolTriggerGate, dummyPoolTGCtrlConf01, - dummyTriggerGateConf01, - createPoolSynchronizationConfiguration) +from sardana.pool.test import FakePool, createPoolController, \ + createPoolTriggerGate, dummyPoolTGCtrlConf01, dummyTriggerGateConf01, \ + createControllerConfiguration synchronization1 = [{SynchParam.Delay: {SynchDomain.Time: 0}, SynchParam.Active: {SynchDomain.Time: .03}, @@ -43,8 +42,8 @@ def setUp(self): # marrying the element with the controller dummy_tg_ctrl.add_element(self.dummy_tg) - self.cfg = createPoolSynchronizationConfiguration((dummy_tg_ctrl,), - ((self.dummy_tg,),)) + self.ctrl_conf = createControllerConfiguration(dummy_tg_ctrl, + [self.dummy_tg]) # marrying the element with the action self.tg_action = PoolSynchronization(self.dummy_tg) @@ -53,11 +52,8 @@ def setUp(self): def generation(self, synchronization): """Verify that the created PoolTGAction start_action starts correctly the involved controller.""" - args = () - kwargs = {'config': self.cfg, - 'synchronization': synchronization - } - self.tg_action.start_action(*args, **kwargs) + args = ([self.ctrl_conf], synchronization) + self.tg_action.start_action(*args) self.tg_action.action_loop() # TODO: add asserts applicable to a dummy controller e.g. listen to # state changes and verify if the change ON->MOVING-ON was emitted diff --git a/src/sardana/pool/poolcountertimer.py b/src/sardana/pool/poolcountertimer.py index 8f1fa8a416..02ae46972f 100644 --- a/src/sardana/pool/poolcountertimer.py +++ b/src/sardana/pool/poolcountertimer.py @@ -32,14 +32,15 @@ from sardana import ElementType -from sardana.pool.poolbasechannel import PoolBaseChannel +from sardana.pool.poolbasechannel import PoolTimerableChannel -class PoolCounterTimer(PoolBaseChannel): +class PoolCounterTimer(PoolTimerableChannel): def __init__(self, **kwargs): + self._timer = None kwargs['elem_type'] = ElementType.CTExpChannel - PoolBaseChannel.__init__(self, **kwargs) + PoolTimerableChannel.__init__(self, **kwargs) # ------------------------------------------------------------------------- # value diff --git a/src/sardana/pool/pooldefs.py b/src/sardana/pool/pooldefs.py index 89dd4bea2d..afb3092ada 100644 --- a/src/sardana/pool/pooldefs.py +++ b/src/sardana/pool/pooldefs.py @@ -110,25 +110,46 @@ class SynchParam(SynchEnum): Repeats = 3 Initial = 4 -# TODO: convert to to python enums, but having in ming problems with -# JSON serialization: https://bugs.python.org/issue18264 -# class AcqSynchType(Enumeration): -# -# Trigger = 0 -# Gate = 1 +AcqSynchType = Enumeration("AcqSynchType", ["Trigger", "Gate", "Start"]) +AcqSynchType.__doc__ = \ + """Enumeration of synchronization types. -AcqSynchType = Enumeration("AcqSynchType", ["Trigger", "Gate"]) + Options: + - Trigger - Start each acquisition (experimental channel will decide on + itself when to end, based on integration time / monitor count) + - Gate - Start and end each acquisition + - Start - Start only the first acquisition (experimental channel will + drive the acquisition based on integration time / monitor count, latency + time and number of repetitions) -# TODO: convert to to python enums, but having in ming problems with -# JSON serialization: https://bugs.python.org/issue18264 -class AcqSynch(Enumeration): + .. todo:: convert to python enums, but having in mind problems with + JSON serialization: https://bugs.python.org/issue18264 + """ + + +class AcqSynch(IntEnum): + """Enumeration of synchronization options. + Uses software/hardware naming to refer to internal (software + synchronizer) or external (hardware synchronization device) + synchronization modes. See :obj:`~sardana.pool.pooldefs.AcqSynchType` + to get more details about the synchronization type e.g. trigger, gate or + start. + """ + #: Internal (software) trigger SoftwareTrigger = 0 + #: External (hardware) trigger HardwareTrigger = 1 + #: Internal (software) gate SoftwareGate = 2 + #: External (hardware) gate HardwareGate = 3 + #: Internal (software) start (triggers just the first acquisition) + SoftwareStart = 4 + #: External (hardware) start (triggers just the first acquisition) + HardwareStart = 5 @classmethod def from_synch_type(self, software, synch_type): @@ -145,6 +166,11 @@ def from_synch_type(self, software, synch_type): return AcqSynch.SoftwareGate else: return AcqSynch.HardwareGate + elif synch_type is AcqSynchType.Start: + if software: + return AcqSynch.SoftwareStart + else: + return AcqSynch.HardwareStart else: raise ValueError("Unable to determine AcqSynch from %s" % synch_type) diff --git a/src/sardana/pool/poolmeasurementgroup.py b/src/sardana/pool/poolmeasurementgroup.py index 0ad982b431..84c8e04193 100644 --- a/src/sardana/pool/poolmeasurementgroup.py +++ b/src/sardana/pool/poolmeasurementgroup.py @@ -26,11 +26,14 @@ """This module is part of the Python Pool library. It defines the base classes for""" -__all__ = ["PoolMeasurementGroup"] +__all__ = ["PoolMeasurementGroup", "MeasurementConfiguration", + "ControllerConfiguration", "ChannelConfiguration", + "SynchronizerConfiguration", "build_measurement_configuration"] __docformat__ = 'restructuredtext' import threading +import weakref try: from taurus.core.taurusvalidator import AttributeNameValidator as\ @@ -39,13 +42,14 @@ # TODO: For Taurus 4 compatibility from taurus.core.tango.tangovalidator import TangoAttributeNameValidator -from sardana import State, ElementType, \ - TYPE_EXP_CHANNEL_ELEMENTS, TYPE_TIMERABLE_ELEMENTS +from sardana import State, ElementType, TYPE_EXP_CHANNEL_ELEMENTS from sardana.sardanaevent import EventType -from sardana.pool.pooldefs import (AcqMode, AcqSynchType, SynchParam, AcqSynch, - SynchDomain) +from sardana.pool.pooldefs import AcqMode, SynchParam, AcqSynch, \ + SynchDomain, AcqSynchType + from sardana.pool.poolgroupelement import PoolGroupElement from sardana.pool.poolacquisition import PoolAcquisition +from sardana.pool.poolsynchronization import SynchronizationDescription from sardana.pool.poolexternal import PoolExternalObject from sardana.taurus.core.tango.sardana import PlotType, Normalization @@ -115,6 +119,8 @@ def _to_fqdn(name, logger=None): # if Taurus3 in use just continue except ImportError: pass + if full_name is None: + full_name = name if full_name != name and logger: msg = ("PQDN full name is deprecated in favor of FQDN full name." " Re-apply configuration in order to upgrade.") @@ -122,42 +128,770 @@ def _to_fqdn(name, logger=None): return full_name -class PoolMeasurementGroup(PoolGroupElement): +class ConfigurationItem(object): + """Container of configuration attributes related to a given element. + + Wrap an element to pretend its API. + Manage the element's configuration. + Hold an information whether the element is enabled. + By default it is enabled. + + .. note:: + The ConfigurationItem class has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + def __init__(self, element, attrs=None): + """Construct a wrapper around the element + + :param element: element to wrap + :type element: obj + :param: attrs: configuration attributes and their values + :type attrs: dict + """ + self._element = weakref.ref(element) + self.enabled = True + + if attrs is not None: + self.__dict__.update(attrs) + + def __getattr__(self, item): + return getattr(self.element, item) + + def get_element(self): + """Returns the element associated with this item""" + return self._element() + + def set_element(self, element): + """Sets the element for this item""" + self._element = weakref.ref(element) + + element = property(get_element) + + +class ControllerConfiguration(ConfigurationItem): + """Container of configuration attributes related to a given controller. + + Inherit behavior from + :class:`~sardana.pool.poolmeasurementgroup.ConfigurationItem` + and additionally hold information about its enabled/disabled channels. + By default it is disabled. + + .. note:: + The ControllerConfiguration class has been included in Sardana + on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + def __init__(self, element, attrs=None): + ConfigurationItem.__init__(self, element, attrs) + self.enabled = False + self._channels = [] + self._channels_enabled = [] + self._channels_disabled = [] + + def add_channel(self, channel_item): + """Aggregate a channel configuration item.""" + self._channels.append(channel_item) + if channel_item.enabled: + self.enabled = True + if self._channels_enabled is None: + self._channels_enabled = [] + self._channels_enabled.append(channel_item) + else: + if self._channels_disabled is None: + self._channels_disabled = [] + self._channels_disabled.append(channel_item) + + def update_state(self): + """Update internal state based on the aggregated channels.""" + self.enabled = False + self._channels_enabled = [] + self._channels_disabled = [] + for channel_item in self._channels: + if channel_item.enabled: + self.enabled = True + self._channels_enabled.append(channel_item) + else: + self._channels_disabled.append(channel_item) + + def get_channels(self, enabled=None): + """Return aggregated channels. + + :param enabled: which channels to return + - True - only enabled + - False - only disabled + - None - all + + :type enabled: bool or None + """ + if enabled is None: + return list(self._channels) + elif enabled: + return list(self._channels_enabled) + else: + return list(self._channels_disabled) + + def validate(self): + pass + + +class TimerableControllerConfiguration(ControllerConfiguration): + """Container of configuration attributes related to a given + timerable controller. + + Inherit behavior from + :class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration` + and additionally validate *timer* and *monitor* configuration. + + .. note:: + The TimerableControllerConfiguration class has been included in + Sardana on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + def update_timer(self): + self._update_master("timer") + + def update_monitor(self): + self._update_master("monitor") + + def _update_master(self, role): + master = getattr(self, role, None) + if master is None: + idx = float("+inf") + for channel in self._channels_enabled: + if channel.index > idx: + continue + master = channel + idx = channel.index + else: + for channel in self._channels_enabled: + if channel.full_name == master: + master = channel + setattr(self, role, master) + + def validate(self): + # validate if the timer and monitor are disabled if the + # controller is enabled + if self.enabled \ + and not self.timer.enabled \ + and not self.monitor.enabled: + err_msg = 'The channel {0} used as timer and the channel ' \ + '{1} used as monitor are disabled. One of them ' \ + 'must be enabled'.format(self.timer.name, + self.monitor.name) + raise ValueError(err_msg) + + +class ExternalControllerConfiguration(ControllerConfiguration): + """Container of configuration attributes related to a given + external controller. + + Inherit behavior from + :class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration`. + + .. note:: + The ExternalControllerConfiguration class has been included in + Sardana on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + def __init__(self, element, attrs=None): + ControllerConfiguration.__init__(self, self, attrs) + self.full_name = element + + +class ChannelConfiguration(ConfigurationItem): + """Container of configuration attributes related to a given + experimental channel. + + Inherit behavior from + :class:`~sardana.pool.poolmeasurementgroup.ConfigurationItem`. + + .. note:: + The ChannelConfiguration class has been included in + Sardana on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + +class SynchronizerConfiguration(ConfigurationItem): + """Container of configuration attributes related to a given + synchronizer element. + + Inherit behavior from + :class:`~sardana.pool.poolmeasurementgroup.ConfigurationItem`. + By default it is disabled. + + .. note:: + The ChannelConfiguration class has been included in + Sardana on a provisional basis. Backwards incompatible changes + (up to and including removal of the class) may occur if + deemed necessary by the core developers. + """ + + def __init__(self, element, attrs=None): + ConfigurationItem.__init__(self, element, attrs) + self.enabled = False + + +def build_measurement_configuration(user_elements): + """Create a minimal measurement configuration data structure from the + user_elements list. + + .. highlight:: none + + Minimal configuration data structure:: + + dict with keys: + - 'controllers' : where value is a dict where: + - key: controller's full name + - value: dict with keys: + - 'channels' where value is a dict where: + - key: channel's full name + - value: dict with keys: + - 'index' : where value is the channel's index + + .. highlight:: default + + .. note:: + The build_measurement_configuration function has been included in + Sardana on a provisional basis. Backwards incompatible changes + (up to and including removal of the function) may occur if + deemed necessary by the core developers. + """ + user_config = {} + external_user_elements = [] + user_config["controllers"] = controllers = {} + + for index, element in enumerate(user_elements): + elem_type = element.get_type() + if elem_type == ElementType.External: + external_user_elements.append((index, element)) + continue + + ctrl = element.controller + ctrl_data = controllers.get(ctrl.full_name) + + if ctrl_data is None: + controllers[ctrl.full_name] = ctrl_data = {} + ctrl_data['channels'] = channels = {} + else: + channels = ctrl_data['channels'] + channels[element.full_name] = channel_data = {} + channel_data['index'] = index + + if len(external_user_elements) > 0: + controllers['__tango__'] = ctrl_data = {} + ctrl_data['channels'] = channels = {} + for index, element in external_user_elements: + channels[element.full_name] = channel_data = {} + channel_data['index'] = index + return user_config + + +class MeasurementConfiguration(object): + """Configuration of a measurement. + + Accepts import and export from/to a serializable data structure (based on + dictionaries/lists and strings). + Provides getter methods that facilitate extracting of information e.g. + controllers of different types, master timers/monitors, etc. + + .. note:: + The build_measurement_configuration function has been included in + Sardana on a provisional basis. Backwards incompatible changes + (up to and including removal of the function) may occur if + deemed necessary by the core developers. + """ + + DFT_DESC = 'General purpose measurement configuration' + + def __init__(self, parent=None): + """Initialize measurement configuration object + + :param parent: (optional) object that this measurement configuration + refers to (usually + :class:`~sardana.pool.poolmeasurementgroup.PoolMeasurementGroup)` + """ + self._parent = None + if parent is not None: + self._parent = weakref.proxy(parent) + + self._config = None + self._use_fqdn = True + + # Structure to store the controllers and their channels + self._timerable_ctrls = {} + self._zerod_ctrls = [] + self._synch_ctrls = {} + self._other_ctrls = [] + self._master_timer_sw = None + self._master_monitor_sw = None + self._master_timer_sw_start = None + self._master_monitor_sw_start = None + self._label = None + self._description = None + self._user_confg = {} + self._channel_acq_synch = {} + self._ctrl_acq_synch = {} + self.changed = False + + def get_acq_synch_by_channel(self, channel): + """Return acquisition synchronization configured for this element. + + :param channel: channel to look for its acquisition synchronization + :type channel: :class:`~sardana.pool.poolbasechannel.PoolBaseChannel` + or :class:`~sardana.pool.poolmeasurementgroup.ChannelConfiguration` + :return: acquisition synchronization + :rtype: :obj:`~sardana.pool.pooldefs.AcqSynch` + """ + if isinstance(channel, ChannelConfiguration): + channel = channel.element + return self._channel_acq_synch[channel] + + def get_acq_synch_by_controller(self, controller): + """Return acquisition synchronization configured for this controller. + + :param controller: controller to look for its acquisition + synchronization + :type controller: :class:`~sardana.pool.poolcontroller.PoolController` + or :class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration` + :return: acquisition synchronization + :rtype: :obj:`~sardana.pool.pooldefs.AcqSynch` + """ + if isinstance(controller, ConfigurationItem): + controller = controller.element + return self._ctrl_acq_synch[controller] + + def _filter_ctrls(self, ctrls, enabled): + if enabled is None: + return ctrls + + filtered_ctrls = [] + for ctrl in ctrls: + if ctrl.enabled == enabled: + filtered_ctrls.append(ctrl) + return filtered_ctrls + + def get_timerable_ctrls(self, acq_synch=None, enabled=None): + """Return timerable controllers. + + Allow to filter controllers based on acquisition synchronization or + whether these are enabled/disabled. + + :param acq_synch: (optional) filter controller based on acquisition + synchronization + :type acq_synch: :class:`~sardana.pool.pooldefs.AcqSynch` + :param enabled: (optional) filter controllers whether these are + enabled/disabled: + + - :obj:`True` - enabled only + - :obj:`False` - disabled only + - :obj:`None` - all + + :type enabled: :obj:`bool` or :obj:`None` + :return: timerable controllers that fulfils the filtering criteria + :rtype: list<:class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration`> # noqa + """ + timerable_ctrls = [] + if acq_synch is None: + for ctrls in self._timerable_ctrls.values(): + timerable_ctrls += ctrls + elif isinstance(acq_synch, list): + acq_synch_list = acq_synch + for acq_synch in acq_synch_list: + timerable_ctrls += self._timerable_ctrls[acq_synch] + else: + timerable_ctrls = list(self._timerable_ctrls[acq_synch]) + + return self._filter_ctrls(timerable_ctrls, enabled) + + def get_zerod_ctrls(self, enabled=None): + """Return 0D controllers. + + Allow to filter controllers whether these are enabled/disabled. + + :param enabled: (optional) filter controllers whether these are + enabled/disabled: + + - :obj:`True` - enabled only + - :obj:`False` - disabled only + - :obj:`None` - all + + :type enabled: :obj:`bool` or :obj:`None` + :return: 0D controllers that fulfils the filtering criteria + :rtype: list<:class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration`> # noqa + """ + return self._filter_ctrls(self._zerod_ctrls, enabled) + + def get_synch_ctrls(self, enabled=None): + """Return synchronizer (currently only trigger/gate) controllers. + + Allow to filter controllers whether these are enabled/disabled. + + :param enabled: (optional) filter controllers whether these are + enabled/disabled: + + - :obj:`True` - enabled only + - :obj:`False` - disabled only + - :obj:`None` - all + + :type enabled: :obj:`bool` or :obj:`None` + :return: synchronizer controllers that fulfils the filtering criteria + :rtype: list<:class:`~sardana.pool.poolmeasurementgroup.ControllerConfiguration`> # noqa + """ + return self._filter_ctrls(self._synch_ctrls, enabled) + + def get_master_timer_software(self): + """Return master timer in software acquisition. + + :return: master timer in software acquisition + :rtype: :class:`~sardana.pool.poolmeasurementgroup.ChannelConfiguration` # noqa + """ + return self._master_timer_sw + + def get_master_monitor_software(self): + """Return master monitor in software acquisition. + + :return: master monitor in software acquisition + :rtype: :class:`~sardana.pool.poolmeasurementgroup.ChannelConfiguration` # noqa + """ + return self._master_monitor_sw + + def get_master_timer_software_start(self): + """Return master timer in software start acquisition. + + :return: master timer in software start acquisition + :rtype: :class:`~sardana.pool.poolmeasurementgroup.ChannelConfiguration` # noqa + """ + return self._master_monitor_sw_start + + def get_master_monitor_software_start(self): + """Return master monitor in software start acquisition. + + :return: master monitor in software start acquisition + :rtype: :class:`~sardana.pool.poolmeasurementgroup.ChannelConfiguration` # noqa + """ + return self._master_timer_sw_start + + def get_configuration_for_user(self): + """Return measurement configuration serializable data structure.""" + return self._user_confg + + def set_configuration_from_user(self, cfg, to_fqdn=True): + """Load measurement configuration from serializable data structure.""" + user_elements = self._parent.get_user_elements() + if len(user_elements) == 0: + # All channels were disabled + raise ValueError('The configuration has all the channels disabled') + + pool = self._parent.pool + + label = cfg.get('label', self._parent.name) + description = cfg.get('description', self.DFT_DESC) + + timerable_ctrls = {AcqSynch.HardwareGate: [], + AcqSynch.HardwareStart: [], + AcqSynch.HardwareTrigger: [], + AcqSynch.SoftwareStart: [], + AcqSynch.SoftwareTrigger: [], + AcqSynch.SoftwareGate: []} + zerod_ctrls = [] + synch_ctrls = [] + other_ctrls = [] + master_timer_sw = None + master_monitor_sw = None + master_timer_sw_start = None + master_monitor_sw_start = None + master_timer_idx_sw = float("+inf") + master_monitor_idx_sw = float("+inf") + master_timer_idx_sw_start = float("+inf") + master_monitor_idx_sw_start = float("+inf") + user_elem_ids = {} + channel_acq_synch = {} + ctrl_acq_synch = {} + user_config = {} + + user_config['controllers'] = {} + user_config['label'] = label + user_config['description'] = description + + for ctrl_name, ctrl_data in cfg['controllers'].items(): + # backwards compatibility for measurement groups created before + # implementing feature-372: + # https://sourceforge.net/p/sardana/tickets/372/ + # WARNING: this is one direction backwards compatibility - it just + # reads channels from the units, but does not write channels to the + # units back + if 'units' in ctrl_data: + ctrl_data = ctrl_data['units']['0'] + # discard controllers which don't have items (garbage) + ch_count = len(ctrl_data['channels']) + if ch_count == 0: + continue + + external = ctrl_name.startswith('__') + if external: + ctrl = ctrl_name + else: + if to_fqdn: + ctrl_name = _to_fqdn(ctrl_name, logger=self._parent) + ctrl = pool.get_element_by_full_name(ctrl_name) + assert ctrl.get_type() == ElementType.Controller + + user_config['controllers'][ctrl_name] = user_config_ctrl = {} + ctrl_conf = {} + + synchronizer = ctrl_data.get('synchronizer', 'software') + conf_synch = None + if synchronizer is None or synchronizer == 'software': + ctrl_conf['synchronizer'] = 'software' + user_config_ctrl['synchronizer'] = 'software' + else: + if to_fqdn: + synchronizer = _to_fqdn(synchronizer, + logger=self._parent) + + user_config_ctrl['synchronizer'] = synchronizer + pool_synch = pool.get_element_by_full_name(synchronizer) + pool_synch_ctrl = pool_synch.controller + conf_synch = SynchronizerConfiguration(pool_synch) + conf_synch_ctrl = None + if len(synch_ctrls) > 0: + conf_synch_ctrl = None + for conf_ctrl in synch_ctrls: + if pool_synch_ctrl == conf_ctrl.element: + conf_synch_ctrl = conf_ctrl + if conf_synch_ctrl is None: + conf_synch_ctrl = ControllerConfiguration(pool_synch_ctrl) + conf_synch_ctrl.add_channel(conf_synch) + synch_ctrls.append(conf_synch_ctrl) + ctrl_conf['synchronizer'] = conf_synch + + try: + synchronization = ctrl_data['synchronization'] + except KeyError: + # backwards compatibility for configurations before SEP6 + try: + synchronization = ctrl_data['trigger_type'] + msg = ("trigger_type configuration parameter is deprecated" + " in favor of synchronization. Re-apply " + "configuration in order to upgrade.") + self._parent.warning(msg) + except KeyError: + synchronization = AcqSynchType.Trigger + + ctrl_conf['synchronization'] = synchronization + user_config_ctrl['synchronization'] = synchronization + + acq_synch = None + if external: + ctrl_item = ExternalControllerConfiguration(ctrl) + elif ctrl.is_timerable(): + is_software = synchronizer == 'software' + acq_synch = AcqSynch.from_synch_type(is_software, + synchronization) + ctrl_acq_synch[ctrl] = acq_synch + ctrl_item = TimerableControllerConfiguration(ctrl, ctrl_conf) + else: + ctrl_item = ControllerConfiguration(ctrl, ctrl_conf) + + ctrl_enabled = False + if 'channels' in ctrl_data: + user_config_ctrl['channels'] = user_config_channel = {} + for ch_name, ch_data in ctrl_data['channels'].items(): + if external: + validator = TangoAttributeNameValidator() + full_name = ch_data.get('full_name', ch_name) + params = validator.getParams(full_name) + params['pool'] = pool + channel = PoolExternalObject(**params) + else: + if to_fqdn: + ch_name = _to_fqdn(ch_name, logger=self._parent) + channel = pool.get_element_by_full_name(ch_name) + ch_data = self._fill_channel_data(channel, ch_data) + user_config_channel[ch_name] = ch_data + ch_item = ChannelConfiguration(channel, ch_data) + ch_item.controller = ctrl_item + ctrl_item.add_channel(ch_item) + if ch_item.enabled: + if external: + id_ = channel.full_name + else: + id_ = channel.id + user_elem_ids[ch_item.index] = id_ + + if ch_item.enabled: + ctrl_enabled = True + + if acq_synch is not None: + channel_acq_synch[channel] = acq_synch + if not external and ctrl.is_timerable(): + ctrl_item.update_timer() + ctrl_item.update_monitor() + user_config_ctrl['timer'] = ctrl_item.timer.full_name + user_config_ctrl['monitor'] = ctrl_item.monitor.full_name + # Update synchronizer state + if conf_synch is not None: + conf_synch.enabled = ctrl_enabled + + ctrl_item.validate() + + if external: + other_ctrls.append(ctrl_item) + elif ctrl.is_timerable(): + timerable_ctrls[acq_synch].append(ctrl_item) + # Find master timer/monitor the system take the channel with + # less index + if acq_synch in (AcqSynch.SoftwareTrigger, + AcqSynch.SoftwareGate): + if ctrl_item.timer.index < master_timer_idx_sw: + master_timer_sw = ctrl_item.timer + master_timer_idx_sw = ctrl_item.timer.index + if ctrl_item.monitor.index < master_monitor_idx_sw: + master_monitor_sw = ctrl_item.monitor + master_monitor_idx_sw = ctrl_item.monitor.index + elif acq_synch == AcqSynch.SoftwareStart: + if ctrl_item.timer.index < master_timer_idx_sw_start: + master_timer_sw_start = ctrl_item.timer + master_timer_idx_sw_start = ctrl_item.timer.index + if ctrl_item.monitor.index < master_monitor_idx_sw_start: + master_monitor_sw_start = ctrl_item.monitor + master_monitor_idx_sw_start = ctrl_item.monitor.index + elif ctrl.get_ctrl_types()[0] == ElementType.ZeroDExpChannel: + zerod_ctrls.append(ctrl_item) + + # Update synchronizer controller states + for conf_synch_ctrl in synch_ctrls: + conf_synch_ctrl.update_state() + + # Fill user configuration with measurement group's timer & monitor + # This is a backwards compatibility cause the measurement group's + # timer & monitor are not used + if master_timer_sw is not None: + user_config['timer'] = master_timer_sw.full_name + elif master_timer_sw_start is not None: + user_config['timer'] = master_timer_sw_start.full_name + else: + user_config['timer'] = cfg['timer'] + + if master_monitor_sw is not None: + user_config['monitor'] = master_monitor_sw.full_name + elif master_monitor_sw_start is not None: + user_config['monitor'] = master_monitor_sw_start.full_name + else: + user_config['monitor'] = cfg['monitor'] + + # Update internals values + self._label = label + self._description = description + self._timerable_ctrls = timerable_ctrls + self._zerod_ctrls = zerod_ctrls + self._synch_ctrls = synch_ctrls + self._other_ctrls = other_ctrls + self._master_timer_sw = master_timer_sw + self._master_monitor_sw = master_monitor_sw + self._master_timer_sw_start = master_timer_sw_start + self._master_monitor_sw_start = master_monitor_sw_start + self._user_confg = user_config + self._channel_acq_synch = channel_acq_synch + self._ctrl_acq_synch = ctrl_acq_synch + + # sorted ids may not be consecutive (if a channel is disabled) + indexes = sorted(user_elem_ids.keys()) + user_elem_ids_list = [user_elem_ids[idx] for idx in indexes] + for conf_synch_ctrl in synch_ctrls: + for conf_synch in conf_synch_ctrl.get_channels(enabled=True): + user_elem_ids_list.append(conf_synch.id) + self._parent.set_user_element_ids(user_elem_ids_list) + + self.changed = True + + def _fill_channel_data(self, channel, channel_data): + """Fill channel default values for the given channel dictionary""" + name = channel.name + full_name = channel.full_name + source = channel.get_source() + ndim = None + ctype = channel.get_type() + if ctype == ElementType.CTExpChannel: + ndim = 0 + elif ctype == ElementType.PseudoCounter: + ndim = 0 + elif ctype == ElementType.ZeroDExpChannel: + ndim = 0 + elif ctype == ElementType.OneDExpChannel: + ndim = 1 + elif ctype == ElementType.TwoDExpChannel: + ndim = 2 + elif ctype == ElementType.External: + config = channel.get_config() + if config is not None: + ndim = int(config.data_format) + elif ctype == ElementType.IORegister: + ndim = 0 + + # Definitively should be initialized by measurement group + # index MUST be here already (asserting this in the following line) + channel_data['index'] = channel_data['index'] + channel_data['name'] = channel_data.get('name', name) + channel_data['full_name'] = channel_data.get('full_name', full_name) + channel_data['source'] = channel_data.get('source', source) + channel_data['enabled'] = channel_data.get('enabled', True) + channel_data['label'] = channel_data.get('label', channel_data['name']) + channel_data['ndim'] = ndim + # Probably should be initialized by measurement group + channel_data['output'] = channel_data.get('output', True) + + # Perhaps should NOT be initialized by measurement group + channel_data['plot_type'] = channel_data.get('plot_type', PlotType.No) + channel_data['plot_axes'] = channel_data.get('plot_axes', []) + channel_data['conditioning'] = channel_data.get('conditioning', '') + channel_data['normalization'] = channel_data.get('normalization', + Normalization.No) + # TODO: think of filling other keys: data_type, data_units, nexus_path + # shape here instead of feeling them on the Taurus extension level + + if ctype != ElementType.External: + ctrl_name = channel.controller.full_name + channel_data['_controller_name'] = channel_data.get( + '_controller_name', ctrl_name) + return channel_data - DFT_DESC = 'General purpose measurement group' + +class PoolMeasurementGroup(PoolGroupElement): def __init__(self, **kwargs): self._state_lock = threading.Lock() self._monitor_count = None - self._repetitions = 1 + self._nb_starts = 1 + self._pending_starts = 0 self._acquisition_mode = AcqMode.Timer - self._config = None + self._config = MeasurementConfiguration(self) self._config_dirty = True self._moveable = None self._moveable_obj = None # by default software synchronizer initial domain is set to Position self._sw_synch_initial_domain = SynchDomain.Position - self._synchronization = [] - # dict with channel and its acquisition synchronization - # key: PoolBaseChannel; value: AcqSynch - self._channel_to_acq_synch = {} - # dict with controller and its acquisition synchronization - # key: PoolController; value: AcqSynch - self._ctrl_to_acq_synch = {} - # list of controller with channels enabled. - self._enabled_ctrls = [] + self._synchronization = SynchronizationDescription() + kwargs['elem_type'] = ElementType.MeasurementGroup PoolGroupElement.__init__(self, **kwargs) configuration = kwargs.get("configuration") - self.set_configuration(configuration) - # if the configuration was never "really" written e.g. newly created MG - # just sets it now so the _channe_to_acq_synch and _ctrl_to_acq_synch - # are properly populated - # TODO: make it more elegant if configuration is None: - configuration = self.get_configuration() - self.set_configuration(configuration, propagate=0, to_fqdn=False) + user_elements = self.get_user_elements() + configuration = build_measurement_configuration(user_elements) + self.set_configuration_from_user(configuration) def _create_action_cache(self): acq_name = "%s.Acquisition" % self._name @@ -212,367 +946,33 @@ def _is_managed_element(self, element): return (element_type in TYPE_EXP_CHANNEL_ELEMENTS or element_type is ElementType.TriggerGate) - """Fills the channel default values for the given channel dictionary""" - - def _build_channel_defaults(self, channel_data, channel): - - external_from_name = isinstance(channel, (str, unicode)) - ndim = None - if external_from_name: - name = full_name = source = channel - ndim = 0 # TODO: this should somehow verify the dimension - else: - name = channel.name - full_name = channel.full_name - source = channel.get_source() - ndim = None - ctype = channel.get_type() - if ctype == ElementType.CTExpChannel: - ndim = 0 - elif ctype == ElementType.PseudoCounter: - ndim = 0 - elif ctype == ElementType.ZeroDExpChannel: - ndim = 0 - elif ctype == ElementType.OneDExpChannel: - ndim = 1 - elif ctype == ElementType.TwoDExpChannel: - ndim = 2 - elif ctype == ElementType.External: - config = channel.get_config() - if config is not None: - ndim = int(config.data_format) - elif ctype == ElementType.IORegister: - ndim = 0 - - # Definitively should be initialized by measurement group - # index MUST be here already (asserting this in the following line) - channel_data['index'] = channel_data['index'] - channel_data['name'] = channel_data.get('name', name) - channel_data['full_name'] = channel_data.get('full_name', full_name) - channel_data['source'] = channel_data.get('source', source) - channel_data['enabled'] = channel_data.get('enabled', True) - channel_data['label'] = channel_data.get('label', channel_data['name']) - channel_data['ndim'] = ndim - # Probably should be initialized by measurement group - channel_data['output'] = channel_data.get('output', True) - - # Perhaps should NOT be initialized by measurement group - channel_data['plot_type'] = channel_data.get('plot_type', PlotType.No) - channel_data['plot_axes'] = channel_data.get('plot_axes', []) - channel_data['conditioning'] = channel_data.get('conditioning', '') - channel_data['normalization'] = channel_data.get( - 'normalization', Normalization.No) - - return channel_data - - def _build_configuration(self): - """Builds a configuration object from the list of elements""" - config = {} - user_elements = self.get_user_elements() - ctrls = self.get_pool_controllers() - - # find the first CT - first_timerable = None - for elem in user_elements: - if elem.get_type() in TYPE_TIMERABLE_ELEMENTS: - first_timerable = elem - break - if first_timerable is None: - raise Exception("It is not possible to construct a measurement " - "group without at least one timer able channel " - "(Counter/timer, 1D or 2D)") - g_timer = g_monitor = first_timerable - config['timer'] = g_timer - config['monitor'] = g_monitor - config['controllers'] = controllers = {} - - external_user_elements = [] - self._enabled_ctrls = [] - for index, element in enumerate(user_elements): - elem_type = element.get_type() - if elem_type == ElementType.External: - external_user_elements.append((index, element)) - continue - - ctrl = element.controller - ctrl_data = controllers.get(ctrl) - # include all controller in the enabled list - self._enabled_ctrls.append(ctrl) - if ctrl_data is None: - controllers[ctrl] = ctrl_data = {} - ctrl_data['channels'] = channels = {} - if elem_type in TYPE_TIMERABLE_ELEMENTS: - elements = ctrls[ctrl] - if g_timer in elements: - ctrl_data['timer'] = g_timer - else: - ctrl_data['timer'] = elements[0] - if g_monitor in elements: - ctrl_data['monitor'] = g_monitor - else: - ctrl_data['monitor'] = elements[0] - ctrl_data['synchronization'] = AcqSynchType.Trigger - ctrl_data['synchronizer'] = 'software' - self._ctrl_to_acq_synch[ctrl] = AcqSynch.SoftwareTrigger - self._channel_to_acq_synch[ - element] = AcqSynch.SoftwareTrigger - else: - channels = ctrl_data['channels'] - channels[element] = channel_data = {} - channel_data['index'] = user_elements.index(element) - channel_data = self._build_channel_defaults(channel_data, element) - config['label'] = self.name - config['description'] = self.DFT_DESC - - if len(external_user_elements) > 0: - controllers['__tango__'] = ctrl_data = {} - ctrl_data['channels'] = channels = {} - for index, element in external_user_elements: - channels[element] = channel_data = {} - channel_data['index'] = index - channel_data = self._build_channel_defaults( - channel_data, element) - return config + @property + def configuration(self): + return self._config + # TODO: Check if it needed def set_configuration(self, config=None, propagate=1, to_fqdn=True): - self._enabled_ctrls = [] - if config is None: - config = self._build_configuration() - else: - # create a configuration based on a new configuration - user_elem_ids = {} - tg_elem_ids = [] - pool = self.pool - for c, c_data in config['controllers'].items(): - synchronizer = c_data.get('synchronizer') - acq_synch_type = c_data.get('synchronization') - software = synchronizer == 'software' - external = isinstance(c, (str, unicode)) - # only timerable elements are configured with acq_synch - acq_synch = None - ctrl_enabled = False - ctrl_to_acq_synch = False - if not external and c.is_timerable(): - acq_synch = AcqSynch.from_synch_type( - software, acq_synch_type) - for channel_data in c_data['channels'].values(): - if external: - element = _id = channel_data['full_name'] - channel_data['source'] = _id - else: - full_name = channel_data['full_name'] - if to_fqdn: - full_name = _to_fqdn(full_name, logger=self) - element = pool.get_element_by_full_name(full_name) - _id = element.id - channel_data = self._build_channel_defaults( - channel_data, element) - if channel_data["enabled"]: - ctrl_enabled = True - if acq_synch is not None: - ctrl_to_acq_synch = True - self._channel_to_acq_synch[element] = acq_synch - if not software: - tg_elem_ids.append(synchronizer.id) - user_elem_ids[channel_data['index']] = _id - - if ctrl_to_acq_synch: - self._ctrl_to_acq_synch[c] = acq_synch - if ctrl_enabled: - self._enabled_ctrls.append(c) - - # sorted ids may not be consecutive (if a channel is disabled) - indexes = sorted(user_elem_ids.keys()) - user_elem_ids_list = [user_elem_ids[idx] for idx in indexes] - user_elem_ids_list.extend(tg_elem_ids) - self.set_user_element_ids(user_elem_ids_list) - - g_timer, g_monitor = config['timer'], config['monitor'] - - timer_ctrl_data = config['controllers'][g_timer.controller] - if timer_ctrl_data['timer'] != g_timer: - self.warning('controller timer and global timer mismatch. ' - 'Using global timer') - self.debug('For controller %s, timer is defined as channel %s. ' - 'The global timer is set to channel %s which belongs ' - 'to the same controller', g_timer.controller.name, - timer_ctrl_data['timer'].name, g_timer.name) - timer_ctrl_data['timer'] = g_timer - - monitor_ctrl_data = config['controllers'][g_monitor.controller] - if monitor_ctrl_data['monitor'] != g_monitor: - self.warning('controller monitor and global monitor mismatch. ' - 'Using global monitor') - self.debug('For controller %s, monitor is defined as channel %s. ' - 'The global timer is set to channel %s which belongs ' - 'to the same controller', g_monitor.controller.name, - monitor_ctrl_data['monitor'].name, g_monitor.name) - monitor_ctrl_data['monitor'] != g_monitor - - self._config = config + self._config._use_fqdn = to_fqdn + self._config.configuration = config self._config_dirty = True if not propagate: return self.fire_event(EventType("configuration", priority=propagate), config) def set_configuration_from_user(self, cfg, propagate=1, to_fqdn=True): - config = {} - user_elements = self.get_user_elements() - pool = self.pool - timer_name = cfg.get('timer', user_elements[0].full_name) - monitor_name = cfg.get('monitor', user_elements[0].full_name) - if to_fqdn: - timer_name = _to_fqdn(timer_name, logger=self) - config['timer'] = pool.get_element_by_full_name(timer_name) - if to_fqdn: - monitor_name = _to_fqdn(monitor_name, logger=self) - config['monitor'] = pool.get_element_by_full_name(monitor_name) - config['controllers'] = controllers = {} - - for c_name, c_data in cfg['controllers'].items(): - # backwards compatibility for measurement groups created before - # implementing feature-372: - # https://sourceforge.net/p/sardana/tickets/372/ - # WARNING: this is one direction backwards compatibility - it just - # reads channels from the units, but does not write channels to the - # units back - if 'units' in c_data: - c_data = c_data['units']['0'] - # discard controllers which don't have items (garbage) - ch_count = len(c_data['channels']) - if ch_count == 0: - continue - - external = c_name.startswith('__') - if external: - ctrl = c_name - else: - if to_fqdn: - c_name = _to_fqdn(c_name, logger=self) - ctrl = pool.get_element_by_full_name(c_name) - assert ctrl.get_type() == ElementType.Controller - controllers[ctrl] = ctrl_data = {} - - # exclude external and not timerable elements - if not external and ctrl.is_timerable(): - timer_name = c_data['timer'] - if to_fqdn: - timer_name = _to_fqdn(timer_name, logger=self) - timer = pool.get_element_by_full_name(timer_name) - ctrl_data['timer'] = timer - monitor_name = c_data['monitor'] - if to_fqdn: - monitor_name = _to_fqdn(monitor_name, logger=self) - monitor = pool.get_element_by_full_name(monitor_name) - ctrl_data['monitor'] = monitor - synchronizer = c_data.get('synchronizer') - # for backwards compatibility purposes - # protect measurement groups without synchronizer defined - if synchronizer is None: - synchronizer = 'software' - elif synchronizer != 'software': - if to_fqdn: - synchronizer = _to_fqdn(synchronizer, logger=self) - synchronizer = pool.get_element_by_full_name(synchronizer) - ctrl_data['synchronizer'] = synchronizer - try: - synchronization = c_data['synchronization'] - except KeyError: - # backwards compatibility for configurations before SEP6 - synchronization = c_data['trigger_type'] - msg = ("trigger_type configuration parameter is deprecated" - " in favor of synchronization. Re-apply " - "configuration in order to upgrade.") - self.warning(msg) - ctrl_data['synchronization'] = synchronization - ctrl_data['channels'] = channels = {} - for ch_name, ch_data in c_data['channels'].items(): - if external: - validator = TangoAttributeNameValidator() - params = validator.getParams(ch_data['full_name']) - params['pool'] = self.pool - channel = PoolExternalObject(**params) - else: - if to_fqdn: - ch_name = _to_fqdn(ch_name, logger=self) - channel = pool.get_element_by_full_name(ch_name) - channels[channel] = dict(ch_data) - - config['label'] = cfg.get('label', self.name) - config['description'] = cfg.get('description', self.DFT_DESC) - - self.set_configuration(config, propagate=propagate, to_fqdn=to_fqdn) - - def get_configuration(self): - return self._config + self._config.set_configuration_from_user(cfg, to_fqdn) + self._config_dirty = True + if not propagate: + return + self.fire_event(EventType("configuration", priority=propagate), + self._config.get_configuration_for_user()) def get_user_configuration(self): - cfg = self.get_configuration() - config = {} - - config['timer'] = cfg['timer'].full_name - config['monitor'] = cfg['monitor'].full_name - config['controllers'] = controllers = {} - - for c, c_data in cfg['controllers'].items(): - ctrl_name = c - if not isinstance(c, (str, unicode)): - ctrl_name = c.full_name - external = ctrl_name.startswith('__') - controllers[ctrl_name] = ctrl_data = {} - if not external and c.is_timerable(): - if 'timer' in c_data: - ctrl_data['timer'] = c_data['timer'].full_name - if 'monitor' in c_data: - ctrl_data['monitor'] = c_data['monitor'].full_name - if 'synchronizer' in c_data: - synchronizer = c_data['synchronizer'] - if synchronizer != 'software': - synchronizer = synchronizer.full_name - ctrl_data['synchronizer'] = synchronizer - if 'synchronization' in c_data: - ctrl_data['synchronization'] = c_data['synchronization'] - ctrl_data['channels'] = channels = {} - for ch, ch_data in c_data['channels'].items(): - channels[ch.full_name] = dict(ch_data) - - config['label'] = cfg['label'] - config['description'] = cfg['description'] - return config - - def load_configuration(self, force=False): - """Loads the current configuration to all involved controllers""" - cfg = self.get_configuration() - # g_timer, g_monitor = cfg['timer'], cfg['monitor'] - for ctrl, ctrl_data in cfg['controllers'].items(): - if isinstance(ctrl, str): # skip external channels - continue - if not ctrl.is_online(): - continue - if ctrl not in self._enabled_ctrls: - continue - - ctrl.set_ctrl_par('acquisition_mode', self.acquisition_mode) - # @TODO: fix optimization and enable it again - if ctrl.operator == self and not force and not self._config_dirty: - continue - ctrl.operator = self - if ctrl.is_timerable(): - # if ctrl == g_timer.controller: - # ctrl.set_ctrl_par('timer', g_timer.axis) - # if ctrl == g_monitor.controller: - # ctrl.set_ctrl_par('monitor', g_monitor.axis) - ctrl.set_ctrl_par('timer', ctrl_data['timer'].axis) - ctrl.set_ctrl_par('monitor', ctrl_data['monitor'].axis) - synchronization = self._ctrl_to_acq_synch.get(ctrl) - self.debug('load_configuration: setting trigger_type: %s ' - 'to ctrl: %s' % (synchronization, ctrl)) - ctrl.set_ctrl_par('synchronization', synchronization) - - self._config_dirty = False + return self._config.get_configuration_for_user() def get_timer(self): - return self.get_configuration()['timer'] + # TODO: Adapt to the new future MeasurementConfiguration API + return self._config._master_timer timer = property(get_timer) @@ -581,14 +981,14 @@ def get_timer(self): # ------------------------------------------------------------------------- def get_integration_time(self): - if len(self._synchronization) == 0: + integration_time = self._synchronization.active_time + if type(integration_time) == float: + return integration_time + elif len(integration_time) == 0: raise Exception("The synchronization group has not been" " initialized") - elif len(self._synchronization) > 1: + elif len(integration_time) > 1: raise Exception("There are more than one synchronization groups") - else: - return self._synchronization[0][SynchParam.Active][ - SynchDomain.Time] def set_integration_time(self, integration_time, propagate=1): total_time = integration_time + self.latency_time @@ -597,7 +997,6 @@ def set_integration_time(self, integration_time, propagate=1): SynchParam.Total: {SynchDomain.Time: total_time}, SynchParam.Repeats: 1}] self.set_synchronization(synch) - self._integration_time = integration_time if not propagate: return self.fire_event(EventType("integration_time", priority=propagate), @@ -649,7 +1048,7 @@ def get_synchronization(self): return self._synchronization def set_synchronization(self, synchronization, propagate=1): - self._synchronization = synchronization + self._synchronization = SynchronizationDescription(synchronization) self._config_dirty = True # acquisition mode goes to configuration if not propagate: return @@ -668,7 +1067,7 @@ def get_moveable(self): def set_moveable(self, moveable, propagate=1, to_fqdn=True): self._moveable = moveable - if self._moveable != 'None' and self._moveable is not None: + if self._moveable is not None: if to_fqdn: moveable = _to_fqdn(moveable, logger=self) self._moveable_obj = self.pool.get_element_by_full_name(moveable) @@ -684,7 +1083,7 @@ def set_moveable(self, moveable, propagate=1, to_fqdn=True): def get_latency_time(self): latency_time = 0 - pool_ctrls = self.acquisition.get_pool_controllers() + pool_ctrls = self.get_pool_controllers() for pool_ctrl in pool_ctrls: if not pool_ctrl.is_timerable(): continue @@ -697,6 +1096,10 @@ def get_latency_time(self): doc="latency time between two consecutive " "acquisitions") + # ------------------------------------------------------------------------- + # software synchronizer initial domain + # ------------------------------------------------------------------------- + def get_sw_synch_initial_domain(self): return self._sw_synch_initial_domain @@ -710,27 +1113,79 @@ def set_sw_synch_initial_domain(self, domain): "or SynchDomain.Position)" ) + # ------------------------------------------------------------------------- + # number of starts + # ------------------------------------------------------------------------- + + def get_nb_starts(self): + return self._nb_starts + + def set_nb_starts(self, nb_starts, propagate=1): + self._nb_starts = nb_starts + if not propagate: + return + self.fire_event(EventType("nb_starts", priority=propagate), + nb_starts) + + nb_starts = property(get_nb_starts, set_nb_starts, + doc="current number of starts") + # ------------------------------------------------------------------------- # acquisition # ------------------------------------------------------------------------- + def prepare(self, multiple=1): + """Prepare for measurement. + + Delegate measurement preparation to the acquisition action. + + ..todo:: remove multiple argument + """ + value = self._get_value() + self._pending_starts = self.nb_starts + + kwargs = {'head': self, + 'multiple': multiple} + + self.acquisition.prepare(self.configuration, + self.acquisition_mode, + value, + self._synchronization, + self._moveable_obj, + self.sw_synch_initial_domain, + self.nb_starts, + **kwargs) + def start_acquisition(self, value=None, multiple=1): + """Start measurement. + + Delegate start measurement to the acquisition action. + Provide backwards compatibility for starts without previous prepare. + + ..todo:: remove value and multiple arguments. + """ + if self._pending_starts == 0: + msg = "starting acquisition without prior preparing is " \ + "deprecated since version Jan18." + self.warning(msg) + self.debug("Preparing with number_of_starts equal to 1") + nb_starts = self.nb_starts + self.set_nb_starts(1, propagate=0) + try: + self.prepare(multiple) + finally: + self.set_nb_starts(nb_starts, propagate=0) self._aborted = False + self._pending_starts -= 1 if not self._simulation_mode: - # load configuration into controller(s) if necessary - self.load_configuration() - # determining the acquisition parameters - kwargs = dict(head=self, config=self._config, multiple=multiple) - acquisition_mode = self.acquisition_mode - if acquisition_mode is AcqMode.Timer: - kwargs['integ_time'] = self.get_integration_time() - elif acquisition_mode is AcqMode.Monitor: - kwargs['monitor'] = self._monitor - kwargs['synchronization'] = self._synchronization - kwargs['moveable'] = self._moveable_obj - kwargs['sw_synch_initial_domain'] = self._sw_synch_initial_domain - # start acquisition - self.acquisition.run(**kwargs) + self.acquisition.run() + + def _get_value(self): + if self._acquisition_mode is AcqMode.Timer: + value = self.get_integration_time() + elif self.acquisition_mode is AcqMode.Monitor: + value = self._monitor_count + return value def set_acquisition(self, acq_cache): self.set_action_cache(acq_cache) @@ -741,5 +1196,10 @@ def get_acquisition(self): acquisition = property(get_acquisition, doc="acquisition object") def stop(self): + self._pending_starts = 0 self.acquisition._synch._synch_soft.stop() PoolGroupElement.stop(self) + + def abort(self): + self._pending_starts = 0 + PoolGroupElement.abort(self) diff --git a/src/sardana/pool/poolmetacontroller.py b/src/sardana/pool/poolmetacontroller.py index a6e7661643..242c737306 100644 --- a/src/sardana/pool/poolmetacontroller.py +++ b/src/sardana/pool/poolmetacontroller.py @@ -274,17 +274,17 @@ def __init__(self, **kwargs): + "sardana.pool.controller.Description constant instead.") for k, v in klass.class_prop.items(): # old member props[k] = DataInfo.toDataInfo(k, v) - try: + if Description in v: self.ctrl_properties_descriptions.append(v[Description]) - except KeyError: + elif 'Description' in v: self.warning(dep_msg) self.ctrl_properties_descriptions.append(v['Description']) for k, v in klass.ctrl_properties.items(): props[k] = DataInfo.toDataInfo(k, v) - try: + if Description in v: self.ctrl_properties_descriptions.append(v[Description]) - except KeyError: + elif 'Description' in v: self.warning(dep_msg) self.ctrl_properties_descriptions.append(v['Description']) diff --git a/src/sardana/pool/poolonedexpchannel.py b/src/sardana/pool/poolonedexpchannel.py index b40c53e625..e715fc4965 100644 --- a/src/sardana/pool/poolonedexpchannel.py +++ b/src/sardana/pool/poolonedexpchannel.py @@ -33,15 +33,16 @@ from sardana import ElementType from sardana.sardanaevent import EventType -from sardana.pool.poolbasechannel import PoolBaseChannel +from sardana.pool.poolbasechannel import PoolTimerableChannel -class Pool1DExpChannel(PoolBaseChannel): +class Pool1DExpChannel(PoolTimerableChannel): def __init__(self, **kwargs): self._data_source = None + self._timer = None kwargs['elem_type'] = ElementType.OneDExpChannel - PoolBaseChannel.__init__(self, **kwargs) + PoolTimerableChannel.__init__(self, **kwargs) # ------------------------------------------------------------------------- # data source diff --git a/src/sardana/pool/poolsynchronization.py b/src/sardana/pool/poolsynchronization.py index 11d536779a..aaf81832bc 100644 --- a/src/sardana/pool/poolsynchronization.py +++ b/src/sardana/pool/poolsynchronization.py @@ -24,16 +24,17 @@ ############################################################################## -"""This module is part of the Python Pool libray. It defines the class for the -trigger/gate generation""" +"""This module is part of the Python Pool library. It defines the classes +for the synchronization""" -__all__ = ["PoolSynchronization", "TGChannel"] +__all__ = ["PoolSynchronization", "SynchronizationDescription", "TGChannel"] import time from functools import partial from taurus.core.util.log import DebugIt from sardana import State from sardana.sardanathreadpool import get_thread_pool +from sardana.pool.pooldefs import SynchDomain, SynchParam from sardana.pool.poolaction import ActionContext, PoolActionItem, PoolAction from sardana.util.funcgenerator import FunctionGenerator @@ -60,9 +61,62 @@ def __getattr__(self, name): return getattr(self.element, name) +class SynchronizationDescription(list): + """Synchronization description. It is composed from groups - repetitions + of equidistant synchronization events. Each group is described by + :class:`~sardana.pool.pooldefs.SynchParam` parameters which may have + values in :class:`~sardana.pool.pooldefs.SynchDomain` domains. + """ + + @property + def repetitions(self): + repetitions = 0 + for group in self: + repetitions += group[SynchParam.Repeats] + return repetitions + + @property + def active_time(self): + return self._get_param(SynchParam.Active) + + @property + def total_time(self): + return self._get_param(SynchParam.Total) + + @property + def passive_time(self): + return self.total_time - self.active_time + + def _get_param(self, param, domain=SynchDomain.Time): + """ + Extract parameter from synchronization description and its groups. If + there is only one group in the synchronization then returns float + with the value. Otherwise a list of floats with different values. + + :param param: parameter type + :type param: :class:`~sardana.pool.pooldefs.SynchParam` + :param domain: domain + :type param: :class:`~sardana.pool.pooldefs.SynchDomain` + :return: parameter value(s) + :rtype float or [float] + """ + + if len(self) == 1: + return self[0][param][domain] + + values = [] + for group in self: + value = group[param][domain] + repeats = group[SynchParam.Repeats] + values += [value] * repeats + return values + + class PoolSynchronization(PoolAction): - '''Action class responsible for trigger/gate generation - ''' + """Synchronization action. + + It coordinates trigger/gate elements and software synchronizer. + """ def __init__(self, main_element, name="Synchronization"): PoolAction.__init__(self, main_element, name) @@ -79,53 +133,39 @@ def __init__(self, main_element, name="Synchronization"): def add_listener(self, listener): self._listener = listener - def start_action(self, *args, **kwargs): - '''Start action method. Expects the following kwargs: - - - config - dictionary containing measurement group configuration - - synchronization - list of dictionaries containing information about - the expected synchronization - - moveable (optional)- moveable object used as the synchronization - source in the Position domain - - monitor (optional) - counter/timer object used as the synchronization - source in the Monitor domain - - sw_synch_initial_domain (optional) - initial domain for software - synchronizer, can be either SynchDomain.Time or SynchDomain.Position - ''' - cfg = kwargs['config'] - synchronization = kwargs.get('synchronization') - moveable = kwargs.get('moveable') - sw_synch_initial_domain = kwargs.get('sw_synch_initial_domain', None) - ctrls_config = cfg.get('controllers') - pool_ctrls = ctrls_config.keys() - - # Prepare a dictionary with the involved channels - self._channels = channels = {} - for pool_ctrl in pool_ctrls: - pool_ctrl_data = ctrls_config[pool_ctrl] - elements = pool_ctrl_data['channels'] - - for element, element_info in elements.items(): - channel = TGChannel(element, info=element_info) - channels[element] = channel - + def start_action(self, ctrls, synchronization, moveable=None, + sw_synch_initial_domain=None, *args, **kwargs): + """Start synchronization action. + + :param ctrls: list of enabled trigger/gate controllers + :type ctrls: list + :param synchronization: synchronization description + :type synchronization: + :class:`~sardana.pool.poolsynchronization.SynchronizationDescription` + :param moveable: (optional) moveable object used as the + synchronization source in the Position domain + :type moveable: :class:`~sardna.pool.poolmotor.PoolMotor` or + :class:`~sardana.pool.poolpseudomotor.PoolPseudoMotor` + :param sw_synch_initial_domain: (optional) - initial domain for + software synchronizer, can be either + :obj:`~sardana.pool.pooldefs.SynchDomain.Time` or + :obj:`~sardana.pool.pooldefs.SynchDomain.Position` + """ with ActionContext(self): # loads synchronization description - for pool_ctrl in pool_ctrls: - ctrl = pool_ctrl.ctrl - pool_ctrl_data = ctrls_config[pool_ctrl] - ctrl.PreSynchAll() - elements = pool_ctrl_data['channels'] - for element in elements: - axis = element.axis - ret = ctrl.PreSynchOne(axis, synchronization) + for ctrl in ctrls: + pool_ctrl = ctrl.element + pool_ctrl.ctrl.PreSynchAll() + for channel in ctrl.get_channels(enabled=True): + axis = channel.axis + ret = pool_ctrl.ctrl.PreSynchOne(axis, synchronization) if not ret: msg = ("%s.PreSynchOne(%d) returns False" % - (pool_ctrl.name, axis)) + (ctrl.name, axis)) raise Exception(msg) - ctrl.SynchOne(axis, synchronization) - ctrl.SynchAll() + pool_ctrl.ctrl.SynchOne(axis, synchronization) + pool_ctrl.ctrl.SynchAll() # attaching listener (usually acquisition action) # to the software trigger gate generator @@ -157,41 +197,43 @@ def start_action(self, *args, **kwargs): get_thread_pool().add(self._synch_soft.run) # PreStartAll on all controllers - for pool_ctrl in pool_ctrls: + for ctrl in ctrls: + pool_ctrl = ctrl.element pool_ctrl.ctrl.PreStartAll() # PreStartOne & StartOne on all elements - for pool_ctrl in pool_ctrls: - ctrl = pool_ctrl.ctrl - pool_ctrl_data = ctrls_config[pool_ctrl] - elements = pool_ctrl_data['channels'] - for element in elements: - axis = element.axis - channel = channels[element] - ret = ctrl.PreStartOne(axis) + for ctrl in ctrls: + pool_ctrl = ctrl.element + for channel in ctrl.get_channels(enabled=True): + axis = channel.axis + ret = pool_ctrl.ctrl.PreStartOne(axis) if not ret: raise Exception("%s.PreStartOne(%d) returns False" % (pool_ctrl.name, axis)) - ctrl.StartOne(axis) + pool_ctrl.ctrl.StartOne(axis) # set the state of all elements to inform their listeners - for channel in channels: - channel.set_state(State.Moving, propagate=2) + self._channels = [] + for ctrl in ctrls: + for channel in ctrl.get_channels(enabled=True): + channel.set_state(State.Moving, propagate=2) + self._channels.append(channel) # StartAll on all controllers - for pool_ctrl in pool_ctrls: + for ctrl in ctrls: + pool_ctrl = ctrl.element pool_ctrl.ctrl.StartAll() def is_triggering(self, states): - """Determines if we are triggering or if the triggering has ended - based on the states returned by the controller(s) and the software - TG generation. + """Determines if we are synchronizing or not based on the states + returned by the controller(s) and the software synchronizer. :param states: a map containing state information as returned by read_state_info: ((state, status), exception_error) :type states: dict - :param ctrl_channels: sequence of the sequences of the channels - corresponding to the controllers - :type ctrl_channels: seq> - :return: a configuration dictionary - :rtype: dict<> - ''' - ctrls_configuration = {} - for ctrl, channels in zip(ctrls, ctrl_channels): - ctrl_data = createConfFromObj(ctrl) - ctrl_data['channels'] = {} - for channel in channels: - channel_conf = createConfFromObj(channel) - ctrl_data['channels'][channel] = channel_conf - ctrls_configuration[ctrl] = ctrl_data - configuration = {'controllers': ctrls_configuration} - return configuration +def createTimerableControllerConfiguration(pool_ctrl, pool_channels): + conf_ctrl = createControllerConfiguration(pool_ctrl, pool_channels) + channel = conf_ctrl.get_channels(enabled=True)[0] + conf_ctrl.timer = channel + conf_ctrl.monitor = channel + return conf_ctrl def createCTAcquisitionConfiguration(ctrls, ctrl_channels): @@ -195,7 +202,7 @@ def createCTAcquisitionConfiguration(ctrls, ctrl_channels): master_idx = 0 configuration = {} ctrls_configuration = {} - configuration['timer'] = ctrl_channels[master_ctrl_idx][master_idx] + configuration['timer'] = timer = ctrl_channels[master_ctrl_idx][master_idx] for ctrl, channels in zip(ctrls, ctrl_channels): ctrl_data = createConfFromObj(ctrl) ctrl_data['channels'] = {} @@ -205,7 +212,16 @@ def createCTAcquisitionConfiguration(ctrls, ctrl_channels): ctrl_data['timer'] = channels[master_idx] ctrls_configuration[ctrl] = ctrl_data configuration['controllers'] = ctrls_configuration - return configuration + mg_cfg = MeasurementConfiguration() + mg_cfg._config = configuration + mg_cfg.hw_sync_monitor = timer + mg_cfg.hw_sync_timer = timer + mg_cfg.sw_sync_timer = timer + mg_cfg.sw_sync_monitor = timer + mg_cfg.ctrl_hw_sync = ctrls_configuration + mg_cfg.ctrl_sw_sync = ctrls_configuration + + return mg_cfg def createMGUserConfiguration(pool, channels): @@ -255,91 +271,17 @@ def createMGUserConfiguration(pool, channels): channel_element = pool.get_element_by_full_name(channel_name_str) channel_ids.append(channel_element.id) one_channel_d = {} - one_channel_d.update({'plot_type': 1}) - one_channel_d.update({'plot_axes': ['']}) - one_channel_d.update({'data_type': 'float64'}) - one_channel_d.update({'index': index}) - one_channel_d.update({'enabled': True}) - one_channel_d.update({'nexus_path': ''}) - one_channel_d.update({'shape': []}) - ctrl_from_channel = channel_element.get_controller() - ctrl_name = ctrl_from_channel.full_name - one_channel_d.update({'_controller_name': ctrl_name}) - one_channel_d.update({'conditioning': ''}) one_channel_d.update({'full_name': channel_name_str}) - one_channel_d.update({'id': channel_element.id}) - one_channel_d.update({'normalization': 0}) - one_channel_d.update({'output': True}) - one_channel_d.update({'label': channel_element.name}) - one_channel_d.update({'data_units': 'No unit'}) - one_channel_d.update({'name': channel_element.name}) + one_channel_d.update({'index': index}) channels_d.update({channel_name_str: one_channel_d}) index += 1 - ctrl_data['channels'] = {} ctrl_data['channels'].update(channels_d) ctrl_d[ctrl_full_name] = ctrl_data all_ctrls_d.update(ctrl_d) MG_configuration.update({'controllers': all_ctrls_d}) - return (MG_configuration, channel_ids, channel_names) - - -def createMGConfiguration(ctrls, ctrls_conf, ctrl_channels, ctrl_channels_conf, - ctrl_trigger_elements, synchronizations): - '''Method to create general MeasurementGroup (and CT) configuration. - Order of the sequences is important. For all sequences, the element of a - given position refers the same controller. - - :param ctrls: sequence of the controllers used by the action - :type ctrls: seq - :param ctrls_conf: sequence of the controllers configuration dictionaries - :type ctrls_conf: dict - :param ctrl_channels: sequence of the sequences of the channels - corresponding to the controllers - :type ctrl_channels: seq> - :param ctrl_channels_conf: sequence of the sequences of the channels - configuration dictionaries - :type ctrl_channels_conf: seq> - :param trigger_elements: sequence of the sequences of the trigger elements - :type trigger_elements: seq> - :param synchronizations: sequence of the sequences of the synchronizations - :type synchronizations: seq> - :return: a configuration dictionary - :rtype: dict<> - ''' - - synchronizers = [] - master_ctrl_idx = 0 - master_idx = 0 - MG_configuration = {} - ctrls_configuration = {} - MG_configuration['timer'] = ctrl_channels[master_ctrl_idx][master_idx] - MG_configuration['monitor'] = ctrl_channels[master_ctrl_idx][master_idx] - for ctrl, ctrl_data, channels, channels_conf, trigger_elements, \ - trigger_types in zip(ctrls, ctrls_conf, ctrl_channels, - ctrl_channels_conf, ctrl_trigger_elements, synchronizations): - ctrl_data['channels'] = {} - index = 0 - for channel, channel_conf, synchronizer, synchronization in \ - zip(channels, channels_conf, trigger_elements, trigger_types): - ctrl_data['channels'][channel] = channel_conf - # this way we are forcing the synchronization of the last channel - ctrl_data['synchronization'] = synchronization - ctrl_data['synchronizer'] = synchronizer - # TODO: investigate why we need the index! - # adding a dummy index - ctrl_data['channels'][channel]['index'] = index - if synchronizer not in synchronizers: - synchronizers.append(synchronizer) - index += 1 - - ctrl_data['timer'] = channels[master_idx] - ctrl_data['monitor'] = channels[master_idx] - ctrls_configuration[ctrl] = ctrl_data - MG_configuration['controllers'] = ctrls_configuration - - return MG_configuration + return MG_configuration, channel_ids, channel_names def createConfbyCtrlKlass(pool, ctrl_klass, ctrl_name): diff --git a/src/sardana/pool/test/test_acquisition.py b/src/sardana/pool/test/test_acquisition.py index 6e09954ad6..be681a13ab 100644 --- a/src/sardana/pool/test/test_acquisition.py +++ b/src/sardana/pool/test/test_acquisition.py @@ -23,203 +23,64 @@ ## ############################################################################## import time -import datetime -import numpy import threading -from taurus.external import unittest +from taurus.external.unittest import TestCase from taurus.test import insertTest -from sardana.pool import AcqSynch +from sardana.pool import AcqSynch, AcqMode from sardana.pool.pooldefs import SynchDomain, SynchParam from sardana.pool.poolsynchronization import PoolSynchronization -from sardana.pool.poolacquisition import (PoolAcquisitionHardware, - PoolAcquisitionSoftware, - PoolCTAcquisition) -from sardana.sardanautils import is_non_str_seq +from sardana.pool.poolacquisition import PoolAcquisitionHardware, \ + PoolAcquisitionSoftware, PoolAcquisitionSoftwareStart, \ + get_acq_ctrls, get_timerable_ctrls from sardana.sardanathreadpool import get_thread_pool -from sardana.pool.test import (createPoolSynchronizationConfiguration, - createCTAcquisitionConfiguration, - BasePoolTestCase, FakeElement) - - -class AttributeListener(object): - - def __init__(self): - self.data = {} - self.data_lock = threading.RLock() - - def event_received(self, *args, **kwargs): - # s - type: sardana.sardanavalue.SardanaValue - # t - type: sardana.sardanaevent.EventType - # v - type: sardana.sardanaattribute.SardanaAttribute e.g. - # sardana.pool.poolbasechannel.Value - s, t, v = args - if t.name.lower() != "valuebuffer": - return - # obtaining sardana element e.g. exp. channel (the attribute owner) - obj_name = s.name - # obtaining the SardanaValue(s) either from the value_chunk (in case - # of buffered attributes) or from the value in case of normal - # attributes - chunk = v - idx = chunk.keys() - value = [sardana_value.value for sardana_value in chunk.values()] - # filling the measurement records - with self.data_lock: - channel_data = self.data.get(obj_name, []) - expected_idx = len(channel_data) - pad = [None] * (idx[0] - expected_idx) - channel_data.extend(pad + value) - self.data[obj_name] = channel_data - - def get_table(self): - '''Construct a table-like array with padded channel data as columns. - Return the ''' - with self.data_lock: - max_len = max([len(d) for d in self.data.values()]) - dtype_spec = [] - table = [] - for k in sorted(self.data.keys()): - v = self.data[k] - v.extend([None] * (max_len - len(v))) - table.append(v) - dtype_spec.append((k, 'float64')) - a = numpy.array(zip(*table), dtype=dtype_spec) - return a +from sardana.pool.test import createControllerConfiguration, \ + createTimerableControllerConfiguration, BasePoolTestCase, FakeElement, \ + AttributeListener class AcquisitionTestCase(BasePoolTestCase): def setUp(self): - """Create a Controller, TriggerGate and PoolSynchronization objects from - dummy configurations. - """ + """Create dummy controllers and elements.""" BasePoolTestCase.setUp(self) - self.l = AttributeListener() - self.channel_names = [] - - def createPoolSynchronization(self, tg_list): + self.acquisition = None + self.synchronization = None + self.data_listener = AttributeListener() self.main_element = FakeElement(self.pool) - self.tggeneration = PoolSynchronization(self.main_element) - for tg in tg_list: - self.tggeneration.add_element(tg) - self.tggeneration.add_listener(self) - - def hw_continuous_acquisition(self, offset, active_interval, - passive_interval, repetitions, integ_time): - """Executes measurement running the TGGeneration and Acquisition - actions according the test parameters. Checks the lengths of the - acquired data. - """ - # obtaining elements created in the BasePoolTestCase.setUp - tg = self.tgs[self.tg_elem_name] - tg_ctrl = tg.get_controller() - # crating configuration for TGGeneration - tg_cfg = createPoolSynchronizationConfiguration((tg_ctrl,), - ((tg,),)) - # creating PoolSynchronization action - self.createPoolSynchronization([tg]) - - channels = [] - for name in self.channel_names: - channels.append(self.cts[name]) - - ct_ctrl = self.ctrls[self.chn_ctrl_name] - - # add_listeners - self.addListeners(channels) - # creating acquisition configurations - self.hw_acq_cfg = createCTAcquisitionConfiguration((ct_ctrl,), - (channels,)) - # creating acquisition actions - self.hw_acq = PoolAcquisitionHardware(channels[0]) - for channel in channels: - self.hw_acq.add_element(channel) - - # get the current number of jobs - jobs_before = get_thread_pool().qsize - - ct_ctrl.set_ctrl_par('synchronization', AcqSynch.HardwareTrigger) - - hw_acq_args = () - hw_acq_kwargs = { - 'integ_time': integ_time, - 'repetitions': repetitions, - 'config': self.hw_acq_cfg, - } - self.hw_acq.run(hw_acq_args, **hw_acq_kwargs) - tg_args = () - tg_kwargs = { - 'offset': offset, - 'active_interval': active_interval, - 'passive_interval': passive_interval, - 'repetitions': repetitions, - 'config': tg_cfg - } - self.tggeneration.run(*tg_args, **tg_kwargs) - # waiting for acquisition and tggeneration to finish - while self.hw_acq.is_running() or self.tggeneration.is_running(): - time.sleep(1) - - self.do_asserts(self.channel_names, repetitions, jobs_before) - - def hw_step_acquisition(self, repetitions, integ_time): - """Executes measurement running the TGGeneration and Acquisition - actions according the test parameters. Checks the lengths of the - acquired data. - """ - - channels = [] - for name in self.channel_names: - channels.append(self.cts[name]) - - ct_ctrl = self.ctrls[self.chn_ctrl_name] - - # creating acquisition configurations - self.acq_cfg = createCTAcquisitionConfiguration((ct_ctrl,), - (channels,)) - # creating acquisition actions - main_element = FakeElement(self.pool) - self.ct_acq = PoolAcquisitionSoftware(main_element) - for channel in channels: - self.ct_acq.add_element(channel) - - ct_ctrl.set_ctrl_par('synchronization', AcqSynch.SoftwareTrigger) - - ct_acq_args = () - ct_acq_kwargs = { - 'integ_time': integ_time, - 'repetitions': repetitions, - 'config': self.acq_cfg, - } - self.ct_acq.run(ct_acq_args, **ct_acq_kwargs) - # waiting for acquisition - while self.ct_acq.is_running(): - time.sleep(0.02) - - for channel in channels: - name = channel.name - value = channel.value.value - print 'channel: %s = %s' % (name, value) - msg = ('Value for channel %s is of type %s, should be ' % - (name, type(value))) - self.assertIsInstance(value, float, msg) - - def addListeners(self, chn_list): + self.tg_1_1 = self.tgs['_test_tg_1_1'] + self.tg_ctrl_1 = self.tg_1_1.get_controller() + self.ct_1_1 = self.cts['_test_ct_1_1'] + self.ct_ctrl_1 = self.ct_1_1.get_controller() + self.channel_names = ['_test_ct_1_1'] + + def create_action(self, class_, elements): + action = class_(self.main_element) + for element in elements: + action.add_element(element) + return action + + def add_listeners(self, chn_list): for chn in chn_list: - chn.add_listener(self.l) + chn.add_listener(self.data_listener) - def do_asserts(self, channel_names, repetitions, jobs_before): + def wait_finish(self): + # waiting for acquisition and synchronization to finish + while (self.acquisition.is_running() + or self.synchronization.is_running()): + time.sleep(.1) + + def do_asserts(self, repetitions, jobs_before): # print acquisition records - table = self.l.get_table() + table = self.data_listener.get_table() header = table.dtype.names print header n_rows = table.shape[0] for row in xrange(n_rows): print row, table[row] # checking if all channels produced data - for channel in channel_names: + for channel in self.channel_names: msg = 'data from channel %s were not acquired' % channel self.assertIn(channel, header, msg) # checking if all the data were acquired @@ -236,8 +97,9 @@ def do_asserts(self, channel_names, repetitions, jobs_before): def tearDown(self): BasePoolTestCase.tearDown(self) - self.l = None + self.data_listener = None self.channel_names = None + self.main_element = None @insertTest(helper_name='continuous_acquisition', offset=0, active_interval=0.1, @@ -251,7 +113,7 @@ def tearDown(self): @insertTest(helper_name='continuous_acquisition', offset=0, active_interval=0.001, passive_interval=0.1, repetitions=10, integ_time=0.01) -class DummyAcquisitionTestCase(AcquisitionTestCase, unittest.TestCase): +class DummyAcquisitionTestCase(AcquisitionTestCase, TestCase): """Integration test of PoolSynchronization, PoolAcquisitionHardware and PoolAcquisitionSoftware actions. This test plays the role of the PoolAcquisition macro action (it aggregates the sub-actions and assign the @@ -263,12 +125,12 @@ def setUp(self): """Create a Controller, TriggerGate and PoolSynchronization objects from dummy configurations. """ - unittest.TestCase.setUp(self) + TestCase.setUp(self) AcquisitionTestCase.setUp(self) def event_received(self, *args, **kwargs): """Executes a single software triggered acquisition.""" - _, type_, value = args + _, type_, index = args name = type_.name if name == "active": if self.sw_acq_busy.is_set(): @@ -276,9 +138,9 @@ def event_received(self, *args, **kwargs): return else: self.sw_acq_busy.set() - args = dict(self.sw_acq_args) - kwargs = dict(self.sw_acq_kwargs) - kwargs['idx'] = value + args = self.sw_acq_args + kwargs = self.sw_acq_kwargs + kwargs['index'] = index get_thread_pool().add(self.sw_acq.run, None, *args, @@ -290,29 +152,36 @@ def continuous_acquisition(self, offset, active_interval, passive_interval, according the test parameters. Checks the lengths of the acquired data. """ # obtaining elements created in the BasePoolTestCase.setUp - tg_2_1 = self.tgs['_test_tg_1_1'] - tg_ctrl_2 = tg_2_1.get_controller() + tg_1_1 = self.tgs['_test_tg_1_1'] + tg_ctrl_1 = tg_1_1.get_controller() ct_1_1 = self.cts['_test_ct_1_1'] # hw synchronized ct_2_1 = self.cts['_test_ct_2_1'] # sw synchronized ct_ctrl_1 = ct_1_1.get_controller() + ct_ctrl_1.set_ctrl_par("synchronization", AcqSynch.HardwareTrigger) ct_ctrl_2 = ct_2_1.get_controller() self.channel_names.append('_test_ct_1_1') self.channel_names.append('_test_ct_2_1') - # crating configuration for TGGeneration - tg_cfg = createPoolSynchronizationConfiguration((tg_ctrl_2,), - ((tg_2_1,),)) - # creating TGGeneration action - self.createPoolSynchronization([tg_2_1]) + + conf_ct_ctrl_1 = createTimerableControllerConfiguration(ct_ctrl_1, + [ct_1_1]) + conf_ct_ctrl_2 = createTimerableControllerConfiguration(ct_ctrl_2, + [ct_2_1]) + hw_ctrls = get_timerable_ctrls([conf_ct_ctrl_1], + acq_mode=AcqMode.Timer) + sw_ctrls = get_timerable_ctrls([conf_ct_ctrl_2], + acq_mode=AcqMode.Timer) + sw_master = sw_ctrls[0].master + conf_tg_ctrl_1 = createControllerConfiguration(tg_ctrl_1, [tg_1_1]) + synch_ctrls = get_acq_ctrls([conf_tg_ctrl_1]) + # creating synchronization action + self.synchronization = self.create_action(PoolSynchronization, + [tg_1_1]) + self.synchronization.add_listener(self) # add_listeners - self.addListeners([ct_1_1, ct_2_1]) - # creating acquisition configurations - self.hw_acq_cfg = createCTAcquisitionConfiguration((ct_ctrl_1,), - ((ct_1_1,),)) - self.sw_acq_cfg = createCTAcquisitionConfiguration((ct_ctrl_2,), - ((ct_2_1,),)) + self.add_listeners([ct_1_1, ct_2_1]) # creating acquisition actions - self.hw_acq = PoolAcquisitionHardware(ct_1_1) - self.sw_acq = PoolAcquisitionSoftware(ct_2_1) + self.hw_acq = self.create_action(PoolAcquisitionHardware, [ct_1_1]) + self.sw_acq = self.create_action(PoolAcquisitionSoftware, [ct_2_1]) # Since we deposit the software acquisition action on the PoolThread's # queue we can not rely on the action's state - one may still wait # in the queue (its state has not changed to running yet) and we would @@ -323,46 +192,138 @@ def continuous_acquisition(self, offset, active_interval, passive_interval, # acquisition action pending. self.sw_acq_busy = threading.Event() self.sw_acq.add_finish_hook(self.sw_acq_busy.clear) + self.sw_acq_args = (sw_ctrls, integ_time, sw_master) + self.sw_acq_kwargs = {} - self.hw_acq.add_element(ct_1_1) - self.sw_acq.add_element(ct_2_1) - + total_interval = active_interval + passive_interval + group = { + SynchParam.Delay: {SynchDomain.Time: offset}, + SynchParam.Active: {SynchDomain.Time: active_interval}, + SynchParam.Total: {SynchDomain.Time: total_interval}, + SynchParam.Repeats: repetitions + } + synchronization = [group] # get the current number of jobs jobs_before = get_thread_pool().qsize + self.hw_acq.run(hw_ctrls, integ_time, repetitions, 0) + self.synchronization.run(synch_ctrls, synchronization) + # waiting for acquisition and synchronization to finish + while (self.hw_acq.is_running() + or self.sw_acq.is_running() + or self.synchronization.is_running()): + time.sleep(.1) + self.do_asserts(repetitions, jobs_before) - self.sw_acq_args = () - self.sw_acq_kwargs = { - 'synch': True, - 'integ_time': integ_time, - 'repetitions': 1, - 'config': self.sw_acq_cfg - } - ct_ctrl_1.set_ctrl_par('synchronization', AcqSynch.HardwareTrigger) - hw_acq_args = () - hw_acq_kwargs = { - 'integ_time': integ_time, - 'repetitions': repetitions, - 'config': self.hw_acq_cfg, + def tearDown(self): + AcquisitionTestCase.tearDown(self) + TestCase.tearDown(self) + + +@insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, + latency_time=0.02) +class AcquisitionSoftwareStartTestCase(AcquisitionTestCase, TestCase): + """Integration test of PoolSynchronization and PoolAcquisitionHardware""" + + def setUp(self): + """Create test actors (controllers and elements)""" + TestCase.setUp(self) + AcquisitionTestCase.setUp(self) + + def event_received(self, *args, **kwargs): + """Callback to execute software start acquisition.""" + _, type_, value = args + name = type_.name + if name != "start": + return + args = self.acq_args + kwargs = self.acq_kwargs + get_thread_pool().add(self.acquisition.run, None, *args, **kwargs) + + def acquire(self, integ_time, repetitions, latency_time): + """Acquire with a dummy C/T synchronized by a hardware start + trigger from a dummy T/G.""" + self.ct_ctrl_1.set_ctrl_par("synchronization", AcqSynch.SoftwareStart) + + conf_ct_ctrl_1 = createTimerableControllerConfiguration(self.ct_ctrl_1, + [self.ct_1_1]) + ctrls = get_timerable_ctrls([conf_ct_ctrl_1], AcqMode.Timer) + master = ctrls[0].master + # creating synchronization action + self.synchronization = self.create_action(PoolSynchronization, + [self.tg_1_1]) + self.synchronization.add_listener(self) + # add_listeners + self.add_listeners([self.ct_1_1]) + # creating acquisition actions + self.acquisition = self.create_action(PoolAcquisitionSoftwareStart, + [self.ct_1_1]) + + self.acq_args = (ctrls, integ_time, master, repetitions, 0) + self.acq_kwargs = {} + + total_interval = integ_time + latency_time + group = { + SynchParam.Delay: {SynchDomain.Time: 0}, + SynchParam.Active: {SynchDomain.Time: integ_time}, + SynchParam.Total: {SynchDomain.Time: total_interval}, + SynchParam.Repeats: repetitions } - self.hw_acq.run(*hw_acq_args, **hw_acq_kwargs) - tg_args = () - total_interval = active_interval + passive_interval - synchronization = [{SynchParam.Delay: {SynchDomain.Time: offset}, - SynchParam.Active: {SynchDomain.Time: active_interval}, - SynchParam.Total: {SynchDomain.Time: total_interval}, - SynchParam.Repeats: repetitions}] - tg_kwargs = { - 'config': tg_cfg, - 'synchronization': synchronization + synchronization = [group] + # get the current number of jobs + jobs_before = get_thread_pool().qsize + self.synchronization.run([], synchronization) + self.wait_finish() + self.do_asserts(repetitions, jobs_before) + + def tearDown(self): + AcquisitionTestCase.tearDown(self) + TestCase.tearDown(self) + + +@insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, + latency_time=0.02) +class AcquisitionHardwareStartTestCase(AcquisitionTestCase, TestCase): + """Integration test of PoolSynchronization and PoolAcquisitionHardware""" + + def setUp(self): + """Create test actors (controllers and elements)""" + TestCase.setUp(self) + AcquisitionTestCase.setUp(self) + + def acquire(self, integ_time, repetitions, latency_time): + """Acquire with a dummy C/T synchronized by a hardware start + trigger from a dummy T/G.""" + self.ct_ctrl_1.set_ctrl_par("synchronization", AcqSynch.HardwareStart) + conf_ct_ctrl_1 = createTimerableControllerConfiguration( + self.ct_ctrl_1, [self.ct_1_1]) + ctrls = get_timerable_ctrls([conf_ct_ctrl_1], AcqMode.Timer) + conf_tg_ctrl_1 = createControllerConfiguration(self.tg_ctrl_1, + [self.tg_1_1]) + synch_ctrls = get_acq_ctrls([conf_tg_ctrl_1]) + self.synchronization = self.create_action(PoolSynchronization, + [self.tg_1_1]) + # add data listeners + self.add_listeners([self.ct_1_1]) + # creating acquisition actions + self.acquisition = self.create_action(PoolAcquisitionHardware, + [self.ct_1_1]) + self.acq_args = ([conf_ct_ctrl_1], integ_time, repetitions) + # prepare synchronization description + total_interval = integ_time + latency_time + group = { + SynchParam.Delay: {SynchDomain.Time: 0}, + SynchParam.Active: {SynchDomain.Time: integ_time}, + SynchParam.Total: {SynchDomain.Time: total_interval}, + SynchParam.Repeats: repetitions } - self.tggeneration.run(*tg_args, **tg_kwargs) - # waiting for acquisition and tggeneration to finish - while (self.hw_acq.is_running() or - self.sw_acq.is_running() or - self.tggeneration.is_running()): - time.sleep(1) - self.do_asserts(self.channel_names, repetitions, jobs_before) + synchronization = [group] + # get the current number of jobs + jobs_before = get_thread_pool().qsize + self.acquisition.run(ctrls, integ_time, repetitions, 0) + self.synchronization.run(synch_ctrls, synchronization) + self.wait_finish() + self.do_asserts(repetitions, jobs_before) def tearDown(self): AcquisitionTestCase.tearDown(self) - unittest.TestCase.tearDown(self) + TestCase.tearDown(self) diff --git a/src/sardana/pool/test/test_ctacquisition.py b/src/sardana/pool/test/test_ctacquisition.py index 1f8295f046..37e6344150 100644 --- a/src/sardana/pool/test/test_ctacquisition.py +++ b/src/sardana/pool/test/test_ctacquisition.py @@ -50,7 +50,6 @@ def setUp(self): pc = createPoolController(pool, dummyPoolCTCtrlConf01) pct = createPoolCounterTimer(pool, pc, dummyCounterTimerConf01) - pc.add_element(pct) pool.add_element(pc) pool.add_element(pct) diff --git a/src/sardana/pool/test/test_measurementgroup.py b/src/sardana/pool/test/test_measurementgroup.py index c10bd51403..a2ea0944fa 100644 --- a/src/sardana/pool/test/test_measurementgroup.py +++ b/src/sardana/pool/test/test_measurementgroup.py @@ -32,25 +32,19 @@ from sardana.sardanathreadpool import get_thread_pool from sardana.pool import AcqSynchType, AcqMode from sardana.pool.pooldefs import SynchDomain, SynchParam -from sardana.pool.test import (BasePoolTestCase, createPoolMeasurementGroup, - dummyMeasurementGroupConf01, - createMGUserConfiguration) -# TODO Import AttributeListener from the right location. -from sardana.pool.test.test_acquisition import AttributeListener +from sardana.pool.test import BasePoolTestCase, createPoolMeasurementGroup,\ + dummyMeasurementGroupConf01, createMGUserConfiguration, AttributeListener class BaseAcquisition(object): def setUp(self, pool): - """ - """ self.pool = pool self.pmg = None self.attr_listener = None def prepare_meas(self, config): - """ Prepare the measurement group and returns the channel names - """ + """ Prepare measurement group and returns the channel names""" pool = self.pool # creating mg user configuration and obtaining channel ids mg_conf, channel_ids, channel_names = \ @@ -66,13 +60,13 @@ def prepare_meas(self, config): def prepare_attribute_listener(self): self.attr_listener = AttributeListener() - # Add listeners + # Add data listener attributes = self.pmg.get_user_elements() for attr in attributes: attr.add_listener(self.attr_listener) def remove_attribute_listener(self): - # Remove listeners + # Remove data listener attributes = self.pmg.get_user_elements() for attr in attributes: attr.remove_listener(self.attr_listener) @@ -82,9 +76,8 @@ def acquire(self): """ self.pmg.start_acquisition() acq = self.pmg.acquisition - # waiting for acquisition while acq.is_running(): - time.sleep(1) + time.sleep(.1) def acq_asserts(self, channel_names, repetitions): # printing acquisition records @@ -191,7 +184,7 @@ def meas_cont_acquisition(self, config, synchronization, moveable=None, '(before: %d)') % (jobs_after, jobs_before) self.assertEqual(jobs_before, jobs_after, msg) - def stopAcquisition(self): + def stop_acquisition(self): """Method used to abort a running acquisition""" self.pmg.stop() @@ -210,7 +203,7 @@ def meas_cont_stop_acquisition(self, config, synchronization, acq = self.pmg.acquisition # starting timer (0.05 s) which will stop the acquisiton - threading.Timer(0.2, self.stopAcquisition).start() + threading.Timer(0.2, self.stop_acquisition).start() # waiting for acquisition and tggeneration to be stoped by thread while acq.is_running(): time.sleep(0.05) @@ -343,7 +336,24 @@ def tearDown(self): [('_test_ct_2_1', 'software', AcqSynchType.Trigger), ], [('_test_0d_1_1', 'software', AcqSynchType.Gate), ]] +doc_14 = 'Acquisition using 2 controllers, with 2 channels in each '\ + + 'controller, using software and hardware start synchronization' + +config_14 = [[('_test_ct_1_1', 'software', AcqSynchType.Start), + ('_test_ct_1_2', 'software', AcqSynchType.Start)], + [('_test_ct_2_1', '_test_tg_1_1', AcqSynchType.Start), + ('_test_ct_2_2', '_test_tg_1_1', AcqSynchType.Start)]] + +doc_15 = 'Acquisition using with 1 2D channel using software synchronization' + +config_15 = [[('_test_2d_1_1', 'software', AcqSynchType.Trigger)]] + +# TODO: listener is not ready to handle 2D +# @insertTest(helper_name='meas_cont_acquisition', test_method_doc=doc_15, +# config=config_15, synchronization=synchronization1) +@insertTest(helper_name='meas_cont_acquisition', test_method_doc=doc_14, + config=config_14, synchronization=synchronization1) @insertTest(helper_name='meas_contpos_acquisition', test_method_doc=doc_12, config=config_12, synchronization=synchronization4, moveable="_test_mot_1_1") diff --git a/src/sardana/pool/test/test_poolcountertimer.py b/src/sardana/pool/test/test_poolcountertimer.py index 14017643e8..dec8347bc3 100644 --- a/src/sardana/pool/test/test_poolcountertimer.py +++ b/src/sardana/pool/test/test_poolcountertimer.py @@ -23,7 +23,10 @@ ## ############################################################################## +import time + from taurus.external import unittest + from sardana.pool.poolcountertimer import PoolCounterTimer from sardana.pool.test import (FakePool, createPoolController, createPoolCounterTimer, dummyCounterTimerConf01, @@ -47,6 +50,14 @@ def test_init(self): 'PoolCounterTimer instance' self.assertIsInstance(self.pct, PoolCounterTimer, msg) + def test_acquisition(self): + self.pct.integration_time = 0.1 + self.pct.start_acquisition() + while self.pct.acquisition.is_running(): + time.sleep(0.01) + msg = "wrong value after acquisition" + self.assertEqual(self.pct.value.value, 0.1, msg) + def tearDown(self): unittest.TestCase.tearDown(self) self.pct = None diff --git a/src/sardana/pool/test/test_poolsynchronization.py b/src/sardana/pool/test/test_poolsynchronization.py index 9e4fcb0014..4401796561 100644 --- a/src/sardana/pool/test/test_poolsynchronization.py +++ b/src/sardana/pool/test/test_poolsynchronization.py @@ -27,11 +27,11 @@ from taurus.external import unittest from sardana.pool.poolsynchronization import PoolSynchronization +from sardana.pool.poolacquisition import get_acq_ctrls from sardana.sardanadefs import State -from sardana.pool.test import (FakePool, createPoolController, - createPoolTriggerGate, dummyPoolTGCtrlConf01, - dummyTriggerGateConf01, - createPoolSynchronizationConfiguration) +from sardana.pool.test import FakePool, createPoolController, \ + createPoolTriggerGate, dummyPoolTGCtrlConf01, dummyTriggerGateConf01, \ + createControllerConfiguration class PoolTriggerGateTestCase(unittest.TestCase): @@ -53,8 +53,12 @@ def setUp(self): dummy_tg_ctrl.add_element(self.dummy_tg) pool.add_element(dummy_tg_ctrl) pool.add_element(self.dummy_tg) - self.cfg = createPoolSynchronizationConfiguration((dummy_tg_ctrl,), - ((self.dummy_tg,),),) + self.conf_ctrl = createControllerConfiguration(dummy_tg_ctrl, + [self.dummy_tg]) + + self.ctrls = get_acq_ctrls([self.conf_ctrl]) + # self.cfg = createPoolSynchronizationConfiguration((dummy_tg_ctrl,), + # ((self.dummy_tg,),),) # Create mock and define its functions ctrl_methods = ['PreStartAll', 'StartAll', 'PreStartOne', 'StartOne', 'PreStateAll', 'StateAll', 'PreStateOne', 'StateOne', @@ -72,11 +76,10 @@ def stopGeneration(self): def test_tggeneration(self): """Verify trigger element states before and after action_loop.""" - from mock import call - args = () - kwargs = {'config': self.cfg} + from mock import call, MagicMock # starting action - self.tgaction.start_action(*args, **kwargs) + synchronization = MagicMock() + self.tgaction.start_action(self.ctrls, synchronization) # verifying that the action correctly started the involved controller self.mock_tg_ctrl.assert_has_calls([call.PreStartAll(), (call.PreStartOne(1,)), diff --git a/src/sardana/pool/test/test_synchronization.py b/src/sardana/pool/test/test_synchronization.py index 53f36cb8fc..061d19820e 100644 --- a/src/sardana/pool/test/test_synchronization.py +++ b/src/sardana/pool/test/test_synchronization.py @@ -34,11 +34,12 @@ from taurus.external import unittest from sardana.pool.poolsynchronization import PoolSynchronization +from sardana.pool.poolacquisition import get_acq_ctrls from sardana.sardanadefs import State from sardana.pool.pooldefs import SynchDomain, SynchParam -from sardana.pool.test import (FakePool, createCtrlConf, createElemConf, - createPoolController, createPoolTriggerGate, - createPoolSynchronizationConfiguration) +from sardana.pool.test import FakePool, createCtrlConf, createElemConf, \ + createPoolController, createPoolTriggerGate, \ + createControllerConfiguration class SynchronizationTestCase(object): @@ -60,8 +61,9 @@ def createElements(self, ctrl_klass, ctrl_lib, ctrl_props): self.pool.add_element(self.tg_ctrl) self.pool.add_element(self.tg_elem) # create Synchronization action and its configuration - self.tg_cfg = createPoolSynchronizationConfiguration((self.tg_ctrl,), - ((self.tg_elem,),),) + self.conf_ctrl = createControllerConfiguration(self.tg_ctrl, + [self.tg_elem]) + self.ctrls = get_acq_ctrls([self.conf_ctrl]) self.tgaction = PoolSynchronization(self.tg_elem) self.tgaction.add_element(self.tg_elem) @@ -83,7 +85,8 @@ def tggeneration(self, ctrl_lib, ctrl_klass, ctrl_props, :type offset: float :param active_interval: signal at which triggers will be generated :type active_interval: float - :param passive_interval: temporal passive period between two active periods + :param passive_interval: temporal passive period between two active + periods :type passive_interval: float :param repetitions: number of generated triggers :type repetitions: int @@ -93,10 +96,8 @@ def tggeneration(self, ctrl_lib, ctrl_klass, ctrl_props, self.createElements(ctrl_klass, ctrl_lib, ctrl_props) # create start_action arguments - args = () - kwargs = {'config': self.tg_cfg, - 'synchronization': synchronization - } + args = (self.ctrls, synchronization) + kwargs = {} # starting action self.tgaction.start_action(*args, **kwargs) # verifying that the elements involved in action changed its state @@ -128,11 +129,12 @@ def abort_tggeneration(self, ctrl_lib, ctrl_klass, ctrl_props, :type offset: float :param active_interval: signal at which triggers will be generated :type active_interval: float - :param passive_interval: temporal passive period between two active periods + :param passive_interval: temporal passive period between two active + periods :type passive_interval: float :param repetitions: number of generated triggers :type repetitions: int - :param abort_time: wait this time before stopping the trigger generation. + :param abort_time: wait this time before stopping the trigger generation :type abort_time: float """ @@ -140,10 +142,8 @@ def abort_tggeneration(self, ctrl_lib, ctrl_klass, ctrl_props, self.createElements(ctrl_klass, ctrl_lib, ctrl_props) # create start_action arguments - args = () - kwargs = {'config': self.tg_cfg, - 'synchronization': synchronization - } + args = (self.ctrls, synchronization) + kwargs = {} # starting action self.tgaction.start_action(*args, **kwargs) # verifying that the elements involved in action changed its state diff --git a/src/sardana/pool/test/util.py b/src/sardana/pool/test/util.py new file mode 100644 index 0000000000..1bc68e1c25 --- /dev/null +++ b/src/sardana/pool/test/util.py @@ -0,0 +1,75 @@ +#!/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 . +## +############################################################################## + +__all__ = ['AttributeListener'] + +import numpy +import threading + + +class AttributeListener(object): + + def __init__(self): + self.data = {} + self.data_lock = threading.RLock() + + def event_received(self, *args, **kwargs): + # s - type: sardana.sardanavalue.SardanaValue + # t - type: sardana.sardanaevent.EventType + # v - type: sardana.sardanaattribute.SardanaAttribute e.g. + # sardana.pool.poolbasechannel.Value + s, t, v = args + if t.name.lower() != "valuebuffer": + return + # obtaining sardana element e.g. exp. channel (the attribute owner) + obj_name = s.name + # obtaining the SardanaValue(s) either from the value_chunk (in case + # of buffered attributes) or from the value in case of normal + # attributes + chunk = v + idx = chunk.keys() + value = [sardana_value.value for sardana_value in chunk.values()] + # filling the measurement records + with self.data_lock: + channel_data = self.data.get(obj_name, []) + expected_idx = len(channel_data) + pad = [None] * (idx[0] - expected_idx) + channel_data.extend(pad + value) + self.data[obj_name] = channel_data + + def get_table(self): + '''Construct a table-like array with padded channel data as columns. + Return the ''' + with self.data_lock: + max_len = max([len(d) for d in self.data.values()]) + dtype_spec = [] + table = [] + for k in sorted(self.data.keys()): + v = self.data[k] + v.extend([None] * (max_len - len(v))) + table.append(v) + dtype_spec.append((k, 'float64')) + a = numpy.array(zip(*table), dtype=dtype_spec) + return a diff --git a/src/sardana/release.py b/src/sardana/release.py index df919e5d06..521b44bbb7 100644 --- a/src/sardana/release.py +++ b/src/sardana/release.py @@ -47,7 +47,7 @@ # we use semantic versioning (http://semver.org/) and we update it using the # bumpversion script (https://github.com/peritus/bumpversion) -version = '2.5.0' +version = '2.7.0' # generate version_info and revision (**deprecated** since v 2.1.2--alpha). if '-' in version: diff --git a/src/sardana/requirements.py b/src/sardana/requirements.py index 6b4a4df97d..8ed81ce2c6 100644 --- a/src/sardana/requirements.py +++ b/src/sardana/requirements.py @@ -37,7 +37,7 @@ # module minimum "Python": (2, 6, 0), "PyTango": (7, 2, 3), - "taurus.core": (3, 7, 5), + "taurus.core": (3, 10), } diff --git a/src/sardana/sardanabuffer.py b/src/sardana/sardanabuffer.py index ba747c3da2..1b93fc5539 100644 --- a/src/sardana/sardanabuffer.py +++ b/src/sardana/sardanabuffer.py @@ -128,7 +128,7 @@ def get_value_obj(self, idx): try: return self._buffer[idx] except KeyError: - msg = "value with %s index is not in buffer" + msg = "value with %s index is not in buffer" % idx if self.next_idx > idx: raise LateValueException(msg) else: @@ -191,7 +191,7 @@ def remove(self, idx): try: return self._buffer.pop(idx) except KeyError: - msg = "value with %s index is not in buffer" + msg = "value with %s index is not in buffer" % idx raise KeyError(msg) def fire_add_event(self, propagate=1): diff --git a/src/sardana/sardanameta.py b/src/sardana/sardanameta.py index 1324bb83d1..1ccda440ed 100644 --- a/src/sardana/sardanameta.py +++ b/src/sardana/sardanameta.py @@ -120,8 +120,9 @@ def __init__(self, **kwargs): name, _ = os.path.splitext(self.file_name) self.meta_classes = {} self.meta_functions = {} - if module is not None and module.__doc__: - self.description = module.__doc__ + if module is not None: + if module.__doc__ is not None: + self.description = module.__doc__ self._code = getsourcelines(module)[0] else: self.description = name + " in error!" diff --git a/src/sardana/sardanamodulemanager.py b/src/sardana/sardanamodulemanager.py old mode 100755 new mode 100644 diff --git a/src/sardana/spock/inputhandler.py b/src/sardana/spock/inputhandler.py index 4174582541..1a5f528bbf 100644 --- a/src/sardana/spock/inputhandler.py +++ b/src/sardana/spock/inputhandler.py @@ -71,14 +71,16 @@ def input_timeout(self, input_data): class MessageHandler(Qt.QObject): + messageArrived = Qt.pyqtSignal(object) + def __init__(self, conn, parent=None): Qt.QObject.__init__(self, parent) self._conn = conn self._dialog = None - self.connect(self, Qt.SIGNAL("messageArrived"), self.on_message) + self.messageArrived.connect(self.on_message) def handle_message(self, input_data): - self.emit(Qt.SIGNAL("messageArrived"), input_data) + self.messageArrived.emit(input_data) def on_message(self, input_data): msg_type = input_data['type'] diff --git a/src/sardana/spock/ipython_01_00/genutils.py b/src/sardana/spock/ipython_01_00/genutils.py index 181d725da5..080425497d 100644 --- a/src/sardana/spock/ipython_01_00/genutils.py +++ b/src/sardana/spock/ipython_01_00/genutils.py @@ -319,7 +319,7 @@ def get_device_from_user(expected_class, dft=None): prompt += "? " from_user = raw_input(prompt).strip() or dft - name = '' + name = None try: full_name, name, _ = from_name_to_tango(from_user) except: @@ -753,6 +753,8 @@ def _create_config_file(location, door_name=None): else: full_door_name, door_name, _ = from_name_to_tango(door_name) door_name = full_door_name + if door_name is None: + raise RuntimeError('unknown door name') # # Discover macro server name @@ -786,7 +788,18 @@ def create_spock_profile(userdir, profile, door_name=None): ipy_profile_dir = p_dir.location - _create_config_file(ipy_profile_dir) + try: + _create_config_file(ipy_profile_dir) + # catch BaseException in order to catch also KeyboardInterrupt + except BaseException: + import shutil + try: + shutil.rmtree(ipy_profile_dir) + except OSError: + msg = ('Could not remove spock profile directory {0}. ' + 'Remove it by hand e.g. rmdir {0}').format(ipy_profile_dir) + print(msg) + sys.exit(-1) def upgrade_spock_profile(ipy_profile_dir, door_name): diff --git a/src/sardana/spock/magic.py b/src/sardana/spock/magic.py index db0e0bf9b3..73a66b081a 100644 --- a/src/sardana/spock/magic.py +++ b/src/sardana/spock/magic.py @@ -62,9 +62,10 @@ def expconf(self, parameter_s=''): # https://sourceforge.net/p/sardana/tickets/10/ import subprocess import sys - fname = sys.modules[ExpDescriptionEditor.__module__].__file__ args = ['python', fname, doorname] + if parameter_s == '--auto-update': + args.insert(2, parameter_s) subprocess.Popen(args) # =========================================================================== diff --git a/src/sardana/spock/parser.py b/src/sardana/spock/parser.py deleted file mode 100644 index 7668b6b668..0000000000 --- a/src/sardana/spock/parser.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/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 . -## -############################################################################## - -"""This package contains macro parameters parsing utilities.""" - -__all__ = ["ParamParser"] - -import re -import collections - -# Token specification -PARAM = r"(?P[^\[\]\s]+)" -QUOTEDPARAM = r'"(?P.*?)(?.*?)'" -LPAREN = r"(?P\[)" -RPAREN = r"(?P\])" -WS = r"(?P\s+)" - -master_pat = re.compile("|".join([QUOTEDPARAM, SINGQUOTEDPARAM, PARAM, LPAREN, - RPAREN, WS])) - -# Tokenizer -Token = collections.namedtuple("Token", ["type", "value"]) - - -def generate_tokens(text): - scanner = master_pat.scanner(text) - for m in iter(scanner.match, None): - # quoted parameters must be returned without the quotes that's why we - # extract a given group, otherwise we would extract the whole match - group = (m.group("QUOTEDPARAM") or - m.group("SINGQUOTEDPARAM") or - m.group()) - tok = Token(m.lastgroup, group) - if tok.type != "WS": - yield tok - - -class ParamParser: - """Implementation of a recursive descent parser. Use the ._accept() method - to test and accept the current lookahead token. Use the ._expect() - method to exactly match and discard the next token on the input - (or raise a SyntaxError if it doesn't match). - - Inspired on Python Cookbook 3 (chapter 2.19) - """ - - def parse(self, text): - self.tokens = generate_tokens(text) - self.tok = None # Last symbol consumed - self.nexttok = None # Next symbol tokenized - self._advance() # Load first lookahead token - return self.param() - - def _advance(self): - """Advance one token ahead""" - self.tok, self.nexttok = self.nexttok, next(self.tokens, None) - - def _accept(self, toktype): - """Test and consume the next token if it matches toktype""" - if self.nexttok and self.nexttok.type == toktype: - self._advance() - return True - else: - return False - - def _expect(self, toktype): - """Consume next token if it matches toktype or raise SyntaxError""" - if not self._accept(toktype): - raise SyntaxError("Expected " + toktype) - - # Grammar rules follow - - def param(self): - """Interpret parameters by iterating over generated tokens. Respect - quotes for string parameters and parenthesis for repeat parameters. - """ - params = [] - while True: - if self._accept("QUOTEDPARAM"): - # quoted parameters allows using quotes escaped by \\ - string = self.tok.value - string = string.replace('\\"', '"') - params.append(string) - elif self._accept("SINGQUOTEDPARAM"): - params.append(self.tok.value) - elif self._accept("PARAM"): - params.append(self.tok.value) - elif self._accept("LPAREN"): - params.append(self.param()) - self._expect("RPAREN") - else: - break - return params diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py old mode 100755 new mode 100644 index 934563f6d6..0fb4c91d6b --- a/src/sardana/spock/spockms.py +++ b/src/sardana/spock/spockms.py @@ -37,7 +37,7 @@ from sardana.sardanautils import is_pure_str, is_non_str_seq from sardana.spock import genutils -from sardana.spock.parser import ParamParser +from sardana.util.parser import ParamParser from sardana.spock.inputhandler import SpockInputHandler, InputHandler from sardana import sardanacustomsettings @@ -516,8 +516,7 @@ class QSpockDoor(SpockBaseDoor): def __init__(self, name, **kw): self.call__init__(SpockBaseDoor, name, **kw) - Qt.QObject.connect(self, Qt.SIGNAL('recordDataUpdated'), - self.processRecordData) + self.recordDataUpdated.connect(self.processRecordData) def recordDataReceived(self, s, t, v): if genutils.get_pylab_mode() == "inline": @@ -589,16 +588,20 @@ def _addMacro(self, macro_info): # IPython < 1 magic commands have different API if genutils.get_ipython_version_list() < [1, 0]: def macro_fn(shell, parameter_s='', name=macro_name): - parameters = split_macro_parameters(parameter_s) door = genutils.get_door() + ms = genutils.get_macro_server() + params_def = ms.getMacroInfoObj(name).parameters + parameters = split_macro_parameters(parameter_s, params_def) door.runMacro(macro_name, parameters, synch=True) macro = door.getLastRunningMacro() if macro is not None: # maybe none if macro was aborted return macro.getResult() else: def macro_fn(parameter_s='', name=macro_name): - parameters = split_macro_parameters(parameter_s) door = genutils.get_door() + ms = genutils.get_macro_server() + params_def = ms.getMacroInfoObj(name).parameters + parameters = split_macro_parameters(parameter_s, params_def) door.runMacro(macro_name, parameters, synch=True) macro = door.getLastRunningMacro() if macro is not None: # maybe none if macro was aborted @@ -620,19 +623,18 @@ def _removeMacro(self, macro_info): del self._local_magic[macro_name] -def split_macro_parameters(parameters_s): +def split_macro_parameters(parameters_s, params_def): """Split string with macro parameters into a list with macro parameters. Whitespaces are the separators between the parameters. - When the input string contains square brackets it indicates an advanced - syntax for representing repeat parameters. Repeat parameters are encapsulated - in square brackets and its internal repetitions, if composed from more than - one item are also encapsulated in brackets. In this case the output list - contains lists internally. + Repeat parameters are encapsulated in square brackets and its internal + repetitions, if composed from more than one item are also encapsulated + in brackets. In this case the output list contains lists internally. - :param parameters_s (string): input string containing parameters - :returns (list): parameters represented as a list (may contain internal - lists) + :param parameters_s: input string containing parameters + :type parameters_s: string + :return: parameters represented as a list (may contain internal lists + :rtype: list """ - parser = ParamParser() + parser = ParamParser(params_def) return parser.parse(parameters_s) diff --git a/src/sardana/spock/test/test_parser.py b/src/sardana/spock/test/test_parser.py deleted file mode 100644 index 6f2d6449df..0000000000 --- a/src/sardana/spock/test/test_parser.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/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 . -## -############################################################################## - -"""Tests for parser utilities.""" - -from taurus.external import unittest -from taurus.test import insertTest -from sardana.spock.parser import ParamParser - - -@insertTest(helper_name="parse", - params_str='ScanFile "[\\"file.nxs\\", \\"file.dat\\"]"', - params=["ScanFile", '["file.nxs", "file.dat"]']) -@insertTest(helper_name="parse", params_str="[1 [] 3]", - params=[["1", [], "3"]]) -@insertTest(helper_name="parse", - params_str="2 3 ['Hello world!' 'How are you?']", - params=["2", "3", ["Hello world!", "How are you?"]]) -@insertTest(helper_name="parse", params_str="ScanFile file.dat", - params=["ScanFile", "file.dat"]) -@insertTest(helper_name="parse", params_str="'2 3'", params=["2 3"]) -@insertTest(helper_name="parse", params_str='"2 3"', params=["2 3"]) -@insertTest(helper_name="parse", params_str="[[mot01 3][mot02 5]] ct01 999", - params=[[["mot01", "3"], ["mot02", "5"]], "ct01", "999"]) -@insertTest(helper_name="parse", params_str="[[2 3][4 5]]", - params=[[["2", "3"], ["4", "5"]]]) -@insertTest(helper_name="parse", params_str="1 [2 3]", - params=["1", ["2", "3"]]) -@insertTest(helper_name="parse", params_str="2 3", params=["2", "3"]) -class ParamParserTestCase(unittest.TestCase): - """Unit tests for ParamParser class.""" - - def parse(self, params_str, params): - """Helper method to test parameters parsing. To be used with insertTest - decorator. - """ - p = ParamParser() - result = p.parse(params_str) - msg = "Parsing failed (result: %r; expected: %r)" %\ - (result, params) - self.assertListEqual(result, params, msg) diff --git a/src/sardana/tango/core/util.py b/src/sardana/tango/core/util.py index a2df3865f2..7c42ff98cf 100644 --- a/src/sardana/tango/core/util.py +++ b/src/sardana/tango/core/util.py @@ -1000,9 +1000,9 @@ def prepare_logstash(args): log_messages = [] try: - import logstash + from logstash_async.handler import AsynchronousLogstashHandler except ImportError: - msg = ("Unable to import logstash. Skipping logstash " + msg = ("Unable to import logstash_async. Skipping logstash " + "configuration...", ) log_messages.append(msg,) return log_messages @@ -1017,8 +1017,13 @@ def get_logstash_conf(dev_name): props = db.get_device_property(dev_name, "LogstashPort") port = int(props["LogstashPort"][0]) except IndexError: - port = 12345 - return host, port + port = None + try: + props = db.get_device_property(dev_name, "LogstashCacheDbPath") + cache_db_path = props["LogstashCacheDbPath"][0] + except IndexError: + cache_db_path = None + return host, port, cache_db_path db = Database() @@ -1034,18 +1039,21 @@ def get_logstash_conf(dev_name): if bin_name in ["Pool", "MacroServer"]: class_name = bin_name dev_name = get_dev_from_class_server(db, class_name, server_name)[0] - host, port = get_logstash_conf(dev_name) + host, port, cache = get_logstash_conf(dev_name) else: dev_name = get_dev_from_class_server(db, "Pool", server_name)[0] - host, port = get_logstash_conf(dev_name) + host, port, cache = get_logstash_conf(dev_name) if host is None: dev_name = get_dev_from_class_server(db, "MacroServer", server_name)[0] - host, port = get_logstash_conf(dev_name) + host, port, cache = get_logstash_conf(dev_name) if host is not None: root = Logger.getRootLog() - handler = logstash.TCPLogstashHandler(host, port, version=1) + handler = AsynchronousLogstashHandler(host, port, database_path=cache) + # don't use full path for program_name + handler._create_formatter_if_necessary() + _, handler.formatter._program_name = os.path.split(handler.formatter._program_name) root.addHandler(handler) msg = ("Log is being sent to logstash listening on %s:%d", host, port) diff --git a/src/sardana/tango/macroserver/MacroServer.py b/src/sardana/tango/macroserver/MacroServer.py index 553f9e66c0..dd80efb168 100644 --- a/src/sardana/tango/macroserver/MacroServer.py +++ b/src/sardana/tango/macroserver/MacroServer.py @@ -396,7 +396,17 @@ class MacroServerClass(SardanaDeviceClass): None], 'LogstashPort': [DevLong, - "Port on which Logstash will listen on events. " + "Port on which Logstash will listen on events [default: 12345]. " + "This property has been included in Sardana on a provisional " + "basis. Backwards incompatible changes (up to and including " + "its removal) may occur if deemed necessary by the " + "core developers.", + 12345], + 'LogstashCacheDbPath': + [DevString, + "Path to the Logstash cache database [default: None]. " + "It is advised not to use the database cache, as it may " + "have negative effects on logging performance. See #895. " "This property has been included in Sardana on a provisional " "basis. Backwards incompatible changes (up to and including " "its removal) may occur if deemed necessary by the " diff --git a/src/sardana/tango/macroserver/test/base.py b/src/sardana/tango/macroserver/test/base.py old mode 100755 new mode 100644 index 9580b0b85a..ee8b7cd40c --- a/src/sardana/tango/macroserver/test/base.py +++ b/src/sardana/tango/macroserver/test/base.py @@ -27,11 +27,15 @@ __all__ = ['BaseMacroServerTestCase'] +import os + import PyTango +from taurus.core.util import whichexecutable from taurus.core.tango.starter import ProcessStarter + from sardana import sardanacustomsettings from sardana.tango.core.util import (get_free_server, get_free_device) -from taurus.core.util import whichexecutable +from sardana.tango.macroserver.MacroServer import MacroServerClass class BaseMacroServerTestCase(object): @@ -44,17 +48,21 @@ class BaseMacroServerTestCase(object): door_name = getattr(sardanacustomsettings, 'UNITTEST_DOOR_NAME', "door/demo1/1") - def setUp(self, pool_name): - """Start MacroServer DS. + def setUp(self, properties=None): + """ + Start MacroServer DS. + + :param properties: dictionary with the macroserver properies. + """ try: db = PyTango.Database() # Discover the MS launcher script msExec = whichexecutable.whichfile("MacroServer") # register MS server - ms_ds_name = "MacroServer/" + self.ms_ds_name - ms_free_ds_name = get_free_server(db, ms_ds_name) - self._msstarter = ProcessStarter(msExec, ms_free_ds_name) + ms_ds_name_base = "MacroServer/" + self.ms_ds_name + self.ms_ds_name = get_free_server(db, ms_ds_name_base) + self._msstarter = ProcessStarter(msExec, self.ms_ds_name) # register MS device dev_name_parts = self.ms_name.split('/') prefix = '/'.join(dev_name_parts[0:2]) @@ -68,7 +76,11 @@ def setUp(self, pool_name): start_from = int(dev_name_parts[2]) self.door_name = get_free_device(db, prefix, start_from) self._msstarter.addNewDevice(self.door_name, klass='Door') - db.put_device_property(self.ms_name, {'PoolNames': pool_name}) + # Add properties + if properties: + for key, values in properties.items(): + db.put_device_property(self.ms_name, + {key: values}) # start MS server self._msstarter.startDs() self.door = PyTango.DeviceProxy(self.door_name) @@ -78,13 +90,34 @@ def setUp(self, pool_name): self.tearDown() def tearDown(self): - """Remove the Pool instance. + """Remove the MacroServer instance and its properties file. """ + self._msstarter.cleanDb(force=True) self._msstarter = None self.macroserver = None self.door = None + db = PyTango.Database() + prop = db.get_device_property(self.ms_name, "EnvironmentDb") + ms_properties = prop["EnvironmentDb"] + if not ms_properties: + dft_ms_properties = os.path.join( + MacroServerClass.DefaultEnvBaseDir, + MacroServerClass.DefaultEnvRelDir) + ds_inst_name = self.ms_ds_name.split("/")[1] + ms_properties = dft_ms_properties % { + "ds_exec_name": "MacroServer", + "ds_inst_name": ds_inst_name} + ms_properties = os.path.normpath(ms_properties) + try: + os.remove(ms_properties) + except Exception, e: + msg = "Not possible to remove macroserver environment file" + print(msg) + print("Details: %s" % e) + + if __name__ == '__main__': bms = BaseMacroServerTestCase() bms.setUp() diff --git a/src/sardana/tango/pool/CTExpChannel.py b/src/sardana/tango/pool/CTExpChannel.py index 654a7b296c..5ba6f48dc4 100644 --- a/src/sardana/tango/pool/CTExpChannel.py +++ b/src/sardana/tango/pool/CTExpChannel.py @@ -33,7 +33,7 @@ import time from PyTango import DevFailed, DevVoid, DevDouble, DevState, AttrQuality, \ - DevString, Except, READ, SCALAR + DevString, Except, READ, SCALAR, READ_WRITE from taurus.core.util.log import DebugIt @@ -41,18 +41,18 @@ from sardana.sardanaattribute import SardanaAttribute from sardana.tango.core.util import to_tango_type_format, exception_str -from sardana.tango.pool.PoolDevice import PoolExpChannelDevice, \ - PoolExpChannelDeviceClass +from sardana.tango.pool.PoolDevice import PoolTimerableDevice, \ + PoolTimerableDeviceClass -class CTExpChannel(PoolExpChannelDevice): +class CTExpChannel(PoolTimerableDevice): def __init__(self, dclass, name): - PoolExpChannelDevice.__init__(self, dclass, name) + PoolTimerableDevice.__init__(self, dclass, name) self._first_read_cache = False def init(self, name): - PoolExpChannelDevice.init(self, name) + PoolTimerableDevice.init(self, name) def get_ct(self): return self.element @@ -64,14 +64,14 @@ def set_ct(self, ct): @DebugIt() def delete_device(self): - PoolExpChannelDevice.delete_device(self) + PoolTimerableDevice.delete_device(self) ct = self.ct if ct is not None: ct.remove_listener(self.on_ct_changed) @DebugIt() def init_device(self): - PoolExpChannelDevice.init_device(self) + PoolTimerableDevice.init_device(self) ct = self.ct if ct is None: @@ -107,6 +107,7 @@ def _on_ct_changed(self, event_source, event_type, event_value): timestamp = time.time() name = event_type.name.lower() + name = name.replace('_', '') # for integration_time events attr_name = name # TODO: remove this condition when Data attribute will be substituted # by ValueBuffer @@ -138,7 +139,9 @@ def _on_ct_changed(self, event_source, event_type, event_value): timestamp = event_value.timestamp else: value = event_value - if name == "value": + if name == "timer" and value is None: + value = "None" + elif name == "value": w_value = event_source.get_value_attribute().w_value state = self.ct.get_state() if state == State.Moving: @@ -159,7 +162,7 @@ def get_dynamic_attributes(self): cache_built = hasattr(self, "_dynamic_attributes_cache") std_attrs, dyn_attrs = \ - PoolExpChannelDevice.get_dynamic_attributes(self) + PoolTimerableDevice.get_dynamic_attributes(self) if not cache_built: # For value attribute, listen to what the controller says for data @@ -172,7 +175,7 @@ def get_dynamic_attributes(self): return std_attrs, dyn_attrs def initialize_dynamic_attributes(self): - attrs = PoolExpChannelDevice.initialize_dynamic_attributes(self) + attrs = PoolTimerableDevice.initialize_dynamic_attributes(self) detect_evts = "value", non_detect_evts = "data", @@ -219,33 +222,34 @@ def Start(self): self.ct.start_acquisition() -class CTExpChannelClass(PoolExpChannelDeviceClass): +class CTExpChannelClass(PoolTimerableDeviceClass): # Class Properties class_property_list = {} # Device Properties device_property_list = {} - device_property_list.update(PoolExpChannelDeviceClass.device_property_list) + device_property_list.update(PoolTimerableDeviceClass.device_property_list) # Command definitions cmd_list = { 'Start': [[DevVoid, ""], [DevVoid, ""]], } - cmd_list.update(PoolExpChannelDeviceClass.cmd_list) + cmd_list.update(PoolTimerableDeviceClass.cmd_list) # Attribute definitions attr_list = {} - attr_list.update(PoolExpChannelDeviceClass.attr_list) + attr_list.update(PoolTimerableDeviceClass.attr_list) standard_attr_list = { 'Value': [[DevDouble, SCALAR, READ], {'abs_change': '1.0', }] } - standard_attr_list.update(PoolExpChannelDeviceClass.standard_attr_list) + + standard_attr_list.update(PoolTimerableDeviceClass.standard_attr_list) def _get_class_properties(self): - ret = PoolExpChannelDeviceClass._get_class_properties(self) + ret = PoolTimerableDeviceClass._get_class_properties(self) ret['Description'] = "Counter/Timer device class" - ret['InheritedFrom'].insert(0, 'PoolExpChannelDevice') + ret['InheritedFrom'].insert(0, 'PoolTimerableDevice') return ret diff --git a/src/sardana/tango/pool/MeasurementGroup.py b/src/sardana/tango/pool/MeasurementGroup.py index 027236e7f4..7daadbe3be 100644 --- a/src/sardana/tango/pool/MeasurementGroup.py +++ b/src/sardana/tango/pool/MeasurementGroup.py @@ -74,9 +74,9 @@ def init_device(self): PoolGroupDevice.init_device(self) # state and status are already set by the super class detect_evts = "latencytime", "moveable", "synchronization", \ - "softwaresynchronizerinitialdomain" + "softwaresynchronizerinitialdomain", "nbstarts" non_detect_evts = "configuration", "integrationtime", "monitorcount", \ - "acquisitionmode", "elementlist", "repetitions" + "acquisitionmode", "elementlist" self.set_change_events(detect_evts, non_detect_evts) self.Elements = list(self.Elements) @@ -141,6 +141,8 @@ def _on_measurement_group_changed(self, event_source, event_type, elif name == "synchronization": codec = CodecFactory().getCodec('json') _, event_value = codec.encode(('', event_value)) + elif name == "moveable" and event_value is None: + event_value = 'None' else: if isinstance(event_value, SardanaAttribute): if event_value.error: @@ -220,14 +222,14 @@ def write_Configuration(self, attr): cfg = CodecFactory().decode(('json', data), ensure_ascii=True) self.measurement_group.set_configuration_from_user(cfg) - def read_Repetitions(self, attr): - repetitions = self.measurement_group.repetitions - if repetitions is None: - repetitions = int('nan') - attr.set_value(repetitions) + def read_NbStarts(self, attr): + nb_starts = self.measurement_group.nb_starts + if nb_starts is None: + nb_starts = int('nan') + attr.set_value(nb_starts) - def write_Repetitions(self, attr): - self.measurement_group.repetitions = attr.get_write_value() + def write_NbStarts(self, attr): + self.measurement_group.nb_starts = attr.get_write_value() def read_Moveable(self, attr): moveable = self.measurement_group.moveable @@ -236,7 +238,10 @@ def read_Moveable(self, attr): attr.set_value(moveable) def write_Moveable(self, attr): - self.measurement_group.moveable = attr.get_write_value() + moveable = attr.get_write_value() + if moveable == 'None': + moveable = None + self.measurement_group.moveable = moveable def read_Synchronization(self, attr): synchronization = self.measurement_group.synchronization @@ -269,6 +274,9 @@ def write_SoftwareSynchronizerInitialDomain(self, attr): raise Exception("Invalid domain (can be either Position or Time)") self.measurement_group.sw_synch_initial_domain = domain + def Prepare(self): + self.measurement_group.prepare() + def Start(self): try: self.wait_for_operation() @@ -300,6 +308,7 @@ class MeasurementGroupClass(PoolGroupDeviceClass): # Command definitions cmd_list = { + 'Prepare': [[DevVoid, ""], [DevVoid, ""]], 'Start': [[DevVoid, ""], [DevVoid, ""]], 'StartMultiple': [[DevLong, ""], [DevVoid, ""]], } @@ -319,9 +328,9 @@ class MeasurementGroupClass(PoolGroupDeviceClass): 'Configuration': [[DevString, SCALAR, READ_WRITE], {'Memorized': "true", 'Display level': DispLevel.EXPERT}], - 'Repetitions': [[DevLong, SCALAR, READ_WRITE], - {'Memorized': "true", - 'Display level': DispLevel.OPERATOR}], + 'NbStarts': [[DevLong, SCALAR, READ_WRITE], + {'Memorized': "true", + 'Display level': DispLevel.OPERATOR}], 'Moveable': [[DevString, SCALAR, READ_WRITE], {'Memorized': "true", 'Display level': DispLevel.EXPERT}], diff --git a/src/sardana/tango/pool/OneDExpChannel.py b/src/sardana/tango/pool/OneDExpChannel.py index f4c879f8d6..e491d0d96b 100644 --- a/src/sardana/tango/pool/OneDExpChannel.py +++ b/src/sardana/tango/pool/OneDExpChannel.py @@ -33,7 +33,7 @@ import time from PyTango import DevFailed, DevVoid, DevString, DevState, AttrQuality, \ - Except, READ, SCALAR + Except, READ, SCALAR, READ_WRITE from taurus.core.util.log import DebugIt @@ -42,18 +42,18 @@ from sardana.pool.controller import OneDController, MaxDimSize, Type from sardana.tango.core.util import to_tango_type_format, exception_str -from sardana.tango.pool.PoolDevice import PoolExpChannelDevice, \ - PoolExpChannelDeviceClass +from sardana.tango.pool.PoolDevice import PoolTimerableDevice, \ + PoolTimerableDeviceClass -class OneDExpChannel(PoolExpChannelDevice): +class OneDExpChannel(PoolTimerableDevice): def __init__(self, dclass, name): - PoolExpChannelDevice.__init__(self, dclass, name) + PoolTimerableDevice.__init__(self, dclass, name) self._first_read_cache = False def init(self, name): - PoolExpChannelDevice.init(self, name) + PoolTimerableDevice.init(self, name) def get_oned(self): return self.element @@ -65,14 +65,14 @@ def set_oned(self, oned): @DebugIt() def delete_device(self): - PoolExpChannelDevice.delete_device(self) + PoolTimerableDevice.delete_device(self) oned = self.oned if oned is not None: oned.remove_listener(self.on_oned_changed) @DebugIt() def init_device(self): - PoolExpChannelDevice.init_device(self) + PoolTimerableDevice.init_device(self) oned = self.oned if oned is None: full_name = self.get_full_name() @@ -107,6 +107,7 @@ def _on_oned_changed(self, event_source, event_type, event_value): timestamp = time.time() name = event_type.name.lower() + name = name.replace('_', '') # for integration_time events attr_name = name # TODO: remove this condition when Data attribute will be substituted # by ValueBuffer @@ -138,7 +139,9 @@ def _on_oned_changed(self, event_source, event_type, event_value): timestamp = event_value.timestamp else: value = event_value - if name == "value": + if name == "timer" and value is None: + value = "None" + elif name == "value": w_value = event_source.get_value_attribute().w_value state = self.oned.get_state() if state == State.Moving: @@ -159,7 +162,7 @@ def get_dynamic_attributes(self): cache_built = hasattr(self, "_dynamic_attributes_cache") std_attrs, dyn_attrs = \ - PoolExpChannelDevice.get_dynamic_attributes(self) + PoolTimerableDevice.get_dynamic_attributes(self) if not cache_built: # For value attribute, listen to what the controller says for data @@ -174,7 +177,7 @@ def get_dynamic_attributes(self): return std_attrs, dyn_attrs def initialize_dynamic_attributes(self): - attrs = PoolExpChannelDevice.initialize_dynamic_attributes(self) + attrs = PoolTimerableDevice.initialize_dynamic_attributes(self) non_detect_evts = "data", @@ -233,7 +236,7 @@ def Start(self): _DFT_VALUE_INFO[Type], DataFormat.OneD) -class OneDExpChannelClass(PoolExpChannelDeviceClass): +class OneDExpChannelClass(PoolTimerableDeviceClass): # Class Properties class_property_list = { @@ -242,29 +245,29 @@ class OneDExpChannelClass(PoolExpChannelDeviceClass): # Device Properties device_property_list = { } - device_property_list.update(PoolExpChannelDeviceClass.device_property_list) + device_property_list.update(PoolTimerableDeviceClass.device_property_list) # Command definitions cmd_list = { 'Start': [[DevVoid, ""], [DevVoid, ""]], } - cmd_list.update(PoolExpChannelDeviceClass.cmd_list) + cmd_list.update(PoolTimerableDeviceClass.cmd_list) # Attribute definitions attr_list = { 'DataSource': [[DevString, SCALAR, READ]], } - attr_list.update(PoolExpChannelDeviceClass.attr_list) + attr_list.update(PoolTimerableDeviceClass.attr_list) standard_attr_list = { 'Value': [[_DFT_VALUE_TYPE, _DFT_VALUE_FORMAT, READ, _DFT_VALUE_MAX_SHAPE[0]], {'abs_change': '1.0', }] } - standard_attr_list.update(PoolExpChannelDeviceClass.standard_attr_list) + standard_attr_list.update(PoolTimerableDeviceClass.standard_attr_list) def _get_class_properties(self): - ret = PoolExpChannelDeviceClass._get_class_properties(self) + ret = PoolTimerableDeviceClass._get_class_properties(self) ret['Description'] = "1D device class" - ret['InheritedFrom'].insert(0, 'PoolExpChannelDevice') + ret['InheritedFrom'].insert(0, 'PoolTimerableDevice') return ret diff --git a/src/sardana/tango/pool/Pool.py b/src/sardana/tango/pool/Pool.py index e35fa54ab6..3fae33c04f 100644 --- a/src/sardana/tango/pool/Pool.py +++ b/src/sardana/tango/pool/Pool.py @@ -1377,7 +1377,17 @@ class PoolClass(PyTango.DeviceClass): None], 'LogstashPort': [PyTango.DevLong, - "Port on which Logstash will listen on events. " + "Port on which Logstash will listen on events [default: 12345]. " + "This property has been included in Sardana on a provisional " + "basis. Backwards incompatible changes (up to and including " + "its removal) may occur if deemed necessary by the " + "core developers.", + 12345], + 'LogstashCacheDbPath': + [PyTango.DevString, + "Path to the Logstash cache database [default: None]. " + "It is advised not to use the database cache, as it may " + "have negative effects on logging performance. See #895. " "This property has been included in Sardana on a provisional " "basis. Backwards incompatible changes (up to and including " "its removal) may occur if deemed necessary by the " diff --git a/src/sardana/tango/pool/PoolDevice.py b/src/sardana/tango/pool/PoolDevice.py index 5a0d315f22..1b1872da50 100644 --- a/src/sardana/tango/pool/PoolDevice.py +++ b/src/sardana/tango/pool/PoolDevice.py @@ -35,8 +35,8 @@ import time import numpy as np -from PyTango import Util, DevVoid, DevLong64, DevBoolean, DevString, \ - DevVarStringArray, DispLevel, DevState, SCALAR, SPECTRUM, \ +from PyTango import Util, DevVoid, DevLong64, DevBoolean, DevString,\ + DevDouble, DevVarStringArray, DispLevel, DevState, SCALAR, SPECTRUM, \ IMAGE, READ_WRITE, READ, AttrData, CmdArgType, DevFailed, seqStr_2_obj, \ Except, ErrSeverity @@ -647,7 +647,14 @@ def get_dynamic_attributes(self): """ if hasattr(self, "_dynamic_attributes_cache"): - return self._standard_attributes_cache, self._dynamic_attributes_cache + return self._standard_attributes_cache, \ + self._dynamic_attributes_cache + std_attrs, dyn_attrs = self._get_dynamic_attributes() + self._standard_attributes_cache = std_attrs + self._dynamic_attributes_cache = dyn_attrs + return std_attrs, dyn_attrs + + def _get_dynamic_attributes(self): ctrl = self.ctrl if ctrl is None: self.warning("no controller: dynamic attributes NOT created") @@ -656,8 +663,8 @@ def get_dynamic_attributes(self): self.warning("controller offline: dynamic attributes NOT created") return PoolDevice.get_dynamic_attributes(self) - self._dynamic_attributes_cache = dyn_attrs = CaselessDict() - self._standard_attributes_cache = std_attrs = CaselessDict() + dyn_attrs = CaselessDict() + std_attrs = CaselessDict() dev_class = self.get_device_class() axis_attrs = ctrl.get_axis_attributes(self.element.axis) @@ -859,6 +866,16 @@ def _encode_value_chunk(self, value_chunk): _, encoded_data = self._codec.encode(('', data)) return encoded_data + def initialize_dynamic_attributes(self): + attrs = PoolElementDevice.initialize_dynamic_attributes(self) + + non_detect_evts = "integrationtime", + + for attr_name in non_detect_evts: + if attr_name in attrs: + self.set_change_event(attr_name, True, False) + return attrs + def read_Data(self, attr): desc = "Data attribute is not foreseen for reading. It is used only "\ "as the communication channel for the continuous acquisitions." @@ -867,10 +884,90 @@ def read_Data(self, attr): "PoolExpChannelDevice.read_Data", ErrSeverity.WARN) + def read_IntegrationTime(self, attr): + """Reads the integration time. + + :param attr: tango attribute + :type attr: :class:`~PyTango.Attribute`""" + attr.set_value(self.element.integration_time) + + def write_IntegrationTime(self, attr): + """Sets the integration time. + + :param attr: tango attribute + :type attr: :class:`~PyTango.Attribute`""" + self.element.integration_time = attr.get_write_value() + class PoolExpChannelDeviceClass(PoolElementDeviceClass): + #: + #: Sardana device attribute definition + #: + #: .. seealso:: :ref:`server` + #: + attr_list = { + 'IntegrationTime': [[DevDouble, SCALAR, READ_WRITE]] + } + attr_list.update(PoolElementDeviceClass.attr_list) + standard_attr_list = { - 'Data': [[DevString, SCALAR, READ]] # TODO: think about DevEncoded + 'Data': [[DevString, SCALAR, READ]], # TODO: think about DevEncoded } standard_attr_list.update(PoolElementDeviceClass.standard_attr_list) + + +class PoolTimerableDevice(PoolExpChannelDevice): + + def __init__(self, dclass, name): + """Constructor""" + PoolExpChannelDevice.__init__(self, dclass, name) + + def initialize_dynamic_attributes(self): + attrs = PoolExpChannelDevice.initialize_dynamic_attributes(self) + + detect_evts = "timer", + + for attr_name in detect_evts: + if attr_name in attrs: + self.set_change_event(attr_name, True, True) + return attrs + + def read_Timer(self, attr): + """Reads the timer for this channel. + + :param attr: tango attribute + :type attr: :class:`~PyTango.Attribute`""" + timer = self.element.timer + if timer is None: + timer = 'None' + attr.set_value(timer) + + def write_Timer(self, attr): + """Sets the timer for this channel. + + :param attr: tango attribute + :type attr: :class:`~PyTango.Attribute`""" + timer = attr.get_write_value() + if timer == 'None': + timer = None + self.element.timer = timer + + +class PoolTimerableDeviceClass(PoolExpChannelDeviceClass): + + #: + #: Sardana device attribute definition + #: + #: .. seealso:: :ref:`server` + #: + + # Attribute definitions + attr_list = { + 'Timer': [[DevString, SCALAR, READ_WRITE], + {'Memorized': "true", }] + } + attr_list.update(PoolExpChannelDeviceClass.attr_list) + + standard_attr_list = {} + standard_attr_list.update(PoolExpChannelDeviceClass.standard_attr_list) diff --git a/src/sardana/tango/pool/PseudoCounter.py b/src/sardana/tango/pool/PseudoCounter.py index 694a9df5ed..1fe70eb200 100644 --- a/src/sardana/tango/pool/PseudoCounter.py +++ b/src/sardana/tango/pool/PseudoCounter.py @@ -32,7 +32,7 @@ import sys import time -from PyTango import Except, READ, SCALAR, DevDouble, \ +from PyTango import Except, READ, SCALAR, SPECTRUM, IMAGE, DevDouble, \ DevVarStringArray, DevVarDoubleArray, DevState, AttrQuality, DevFailed from taurus.core.util.log import DebugIt @@ -176,12 +176,22 @@ def get_dynamic_attributes(self): if not cache_built: # For value attribute, listen to what the controller says for data - # type (between long and float) + # type (between long and float) and data format (scalar, spectrum + # or image) value = std_attrs.get('value') if value is not None: _, data_info, attr_info = value - ttype, _ = to_tango_type_format(attr_info.dtype) + ttype, tformat = to_tango_type_format(attr_info.dtype, + attr_info.dformat) data_info[0][0] = ttype + data_info[0][1] = tformat + if tformat == SPECTRUM: + shape = attr_info.maxdimsize + data_info[0].append(shape[0]) + elif tformat == IMAGE: + shape = attr_info.maxdimsize + data_info[0].append(shape[0]) + data_info[0].append(shape[1]) return std_attrs, dyn_attrs def initialize_dynamic_attributes(self): @@ -214,7 +224,7 @@ def read_Value(self, attr): state = pseudo_counter.get_state(cache=use_cache, propagate=0) if state == State.Moving: quality = AttrQuality.ATTR_CHANGING - timestamp = value_attr.value + timestamp = value_attr.timestamp self.set_attribute(attr, value=value, quality=quality, priority=0, timestamp=timestamp) diff --git a/src/sardana/tango/pool/TwoDExpChannel.py b/src/sardana/tango/pool/TwoDExpChannel.py index 8fdd54b29d..17e5fec83b 100644 --- a/src/sardana/tango/pool/TwoDExpChannel.py +++ b/src/sardana/tango/pool/TwoDExpChannel.py @@ -33,7 +33,7 @@ import time from PyTango import DevFailed, DevVoid, DevString, DevState, AttrQuality, \ - Except, READ, SCALAR + Except, READ, SCALAR, READ_WRITE from taurus.core.util.log import DebugIt @@ -42,17 +42,17 @@ from sardana.pool.controller import TwoDController, MaxDimSize, Type from sardana.tango.core.util import to_tango_type_format, exception_str -from sardana.tango.pool.PoolDevice import PoolElementDevice, \ - PoolElementDeviceClass +from sardana.tango.pool.PoolDevice import PoolTimerableDevice, \ + PoolTimerableDeviceClass -class TwoDExpChannel(PoolElementDevice): +class TwoDExpChannel(PoolTimerableDevice): def __init__(self, dclass, name): - PoolElementDevice.__init__(self, dclass, name) + PoolTimerableDevice.__init__(self, dclass, name) def init(self, name): - PoolElementDevice.init(self, name) + PoolTimerableDevice.init(self, name) def get_twod(self): return self.element @@ -64,14 +64,14 @@ def set_twod(self, twod): @DebugIt() def delete_device(self): - PoolElementDevice.delete_device(self) + PoolTimerableDevice.delete_device(self) twod = self.twod if twod is not None: twod.remove_listener(self.on_twod_changed) @DebugIt() def init_device(self): - PoolElementDevice.init_device(self) + PoolTimerableDevice.init_device(self) twod = self.twod if twod is None: full_name = self.get_full_name() @@ -106,6 +106,7 @@ def _on_twod_changed(self, event_source, event_type, event_value): timestamp = time.time() name = event_type.name.lower() + name = name.replace('_', '') # for integration_time events try: attr = self.get_attribute_by_name(name) @@ -127,8 +128,12 @@ def _on_twod_changed(self, event_source, event_type, event_value): else: value = event_value.value timestamp = event_value.timestamp + else: + value = event_value - if name == "value": + if name == "timer" and value is None: + value = "None" + elif name == "value": state = self.twod.get_state() if state == State.Moving: quality = AttrQuality.ATTR_CHANGING @@ -147,7 +152,7 @@ def get_dynamic_attributes(self): cache_built = hasattr(self, "_dynamic_attributes_cache") std_attrs, dyn_attrs = \ - PoolElementDevice.get_dynamic_attributes(self) + PoolTimerableDevice.get_dynamic_attributes(self) if not cache_built: # For value attribute, listen to what the controller says for data @@ -204,7 +209,7 @@ def Start(self): _DFT_VALUE_INFO[Type], DataFormat.TwoD) -class TwoDExpChannelClass(PoolElementDeviceClass): +class TwoDExpChannelClass(PoolTimerableDeviceClass): # Class Properties class_property_list = { @@ -213,29 +218,29 @@ class TwoDExpChannelClass(PoolElementDeviceClass): # Device Properties device_property_list = { } - device_property_list.update(PoolElementDeviceClass.device_property_list) + device_property_list.update(PoolTimerableDeviceClass.device_property_list) # Command definitions cmd_list = { 'Start': [[DevVoid, ""], [DevVoid, ""]], } - cmd_list.update(PoolElementDeviceClass.cmd_list) + cmd_list.update(PoolTimerableDeviceClass.cmd_list) # Attribute definitions attr_list = { 'DataSource': [[DevString, SCALAR, READ]], } - attr_list.update(PoolElementDeviceClass.attr_list) + attr_list.update(PoolTimerableDeviceClass.attr_list) standard_attr_list = { 'Value': [[_DFT_VALUE_TYPE, _DFT_VALUE_FORMAT, READ, _DFT_VALUE_MAX_SHAPE[0], _DFT_VALUE_MAX_SHAPE[1]], {'abs_change': '1.0', }], } - standard_attr_list.update(PoolElementDeviceClass.standard_attr_list) + standard_attr_list.update(PoolTimerableDeviceClass.standard_attr_list) def _get_class_properties(self): - ret = PoolElementDeviceClass._get_class_properties(self) + ret = PoolTimerableDeviceClass._get_class_properties(self) ret['Description'] = "2D device class" - ret['InheritedFrom'].insert(0, 'PoolElementDevice') + ret['InheritedFrom'].insert(0, 'PoolTimerableDevice') return ret diff --git a/src/sardana/tango/pool/test/base_sartest.py b/src/sardana/tango/pool/test/base_sartest.py index 8a4a996968..8fd8b3e65d 100644 --- a/src/sardana/tango/pool/test/base_sartest.py +++ b/src/sardana/tango/pool/test/base_sartest.py @@ -23,9 +23,10 @@ ## ############################################################################## -import PyTango +import taurus + from sardana.tango.pool.test import BasePoolTestCase -from sardana.tango.core.util import get_free_alias + __all__ = ['SarTestTestCase'] @@ -87,7 +88,7 @@ def setUp(self): except Exception, e: print e msg = 'Impossible to create ctrl: "%s"' % (ctrl_name) - raise Exception('Aborting SartestTesCase: %s' % (msg)) + raise Exception('Aborting SartestTestCase: %s' % (msg)) self.ctrl_list.append(ctrl_name) # create elements for axis in range(1, nelem + 1): @@ -99,7 +100,7 @@ def setUp(self): print e msg = 'Impossible to create element: "%s"' % ( elem_name) - raise Exception('Aborting SartestTesCase: %s' % (msg)) + raise Exception('Aborting SartestTestCase: %s' % (msg)) self.elem_list.append(elem_name) # pseudo controllers and elements for pseudo in self.pseudo_cls_list: @@ -114,7 +115,7 @@ def setUp(self): except Exception, e: print e msg = 'Impossible to create ctrl: "%s"' % (ctrl_name) - raise Exception('Aborting SartestTesCase: %s' % (msg)) + raise Exception('Aborting SartestTestCase: %s' % (msg)) self.ctrl_list.append(ctrl_name) for role in roles: elem = role.split("=")[1] @@ -130,13 +131,28 @@ def tearDown(self): """ dirty_elems = [] dirty_ctrls = [] + f = taurus.Factory() for elem_name in self.elem_list: + # Cleanup eventual taurus devices. This is especially important + # if the sardana-taurus extensions are in use since this + # devices are created and destroyed within the testsuite. + # Persisting taurus device may react on API_EventTimeouts, enabled + # polling, etc. + if elem_name in f.tango_alias_devs: + taurus.Device(elem_name).cleanUp() try: self.pool.DeleteElement(elem_name) except: dirty_elems.append(elem_name) for ctrl_name in self.ctrl_list: + # Cleanup eventual taurus devices. This is especially important + # if the sardana-taurus extensions are in use since this + # devices are created and destroyed within the testsuite. + # Persisting taurus device may react on API_EventTimeouts, enabled + # polling, etc. + if elem_name in f.tango_alias_devs: + taurus.Device(elem_name).cleanUp() try: self.pool.DeleteElement(ctrl_name) except: diff --git a/src/sardana/taurus/core/tango/sardana/macro.py b/src/sardana/taurus/core/tango/sardana/macro.py old mode 100755 new mode 100644 index 9b4a4ba3f5..238f6eac5e --- a/src/sardana/taurus/core/tango/sardana/macro.py +++ b/src/sardana/taurus/core/tango/sardana/macro.py @@ -1,5 +1,5 @@ #!/usr/bin/env python - +from __future__ import absolute_import ############################################################################## ## # This file is part of Sardana @@ -41,6 +41,7 @@ from taurus.core.util.user import USER_NAME from taurus.core.util.codecs import CodecFactory +from sardana.macroserver.msparameter import Optional class MacroRunException(Exception): @@ -484,13 +485,14 @@ class SingleParamNode(ParamNode): def __init__(self, parent=None, param=None): ParamNode.__init__(self, parent, param) + self._defValue = None if param is None: return self.setType(str(param.get('type'))) - self.setDefValue(str(param.get('default_value', ''))) + self.setDefValue(param.get('default_value', None)) if self.type() == "User": - self.setDefValue(str(USER_NAME)) - self.setValue(self.defValue()) + self.setDefValue(USER_NAME) + self._value = None def __len__(self): return 0 @@ -507,7 +509,9 @@ def setValue(self, value): self._value = value def defValue(self): - return self._defValue + if self._defValue is None: + return None + return str(self._defValue) def setDefValue(self, defValue): if defValue == "None": @@ -523,8 +527,11 @@ def setType(self, type): def toXml(self): value = self.value() paramElement = etree.Element("param", name=self.name()) - if not value is None: - paramElement.set("value", value) + # set value attribute only if it is different than the default value + # the server will assign the default value anyway. + if value is not None or str(value).lower() != 'none': + if value != self.defValue(): + paramElement.set("value", value) return paramElement def fromXml(self, xmlElement): @@ -542,9 +549,14 @@ def allMotors(self): def toRun(self): val = self.value() - if val is None or val == "None" or val == "": - alert = "Parameter " + self.name() + " is missing.
" - return ([val], alert) + if val is None or val == "None": + if self.defValue() is None: + alert = "Parameter " + self.name() + " is missing.
" + return ([val], alert) + elif self._defValue == Optional: + val = '' + else: + val = self.defValue() return ([val], "") def toList(self): @@ -1263,8 +1275,9 @@ def fromPlainText(self, plainText): def ParamFactory(paramInfo): - """Factory method returning param element, depends of the paramInfo argument.""" - + """Factory method returning param element, depends of the paramInfo + argument. + """ if isinstance(paramInfo.get('type'), list): param = RepeatParamNode(param=paramInfo) if param.min() > 0: @@ -1275,71 +1288,15 @@ def ParamFactory(paramInfo): def createMacroNode(macro_name, params_def, macro_params): - """The best effort creation of the macro XML object. It tries to - convert flat list of string parameter values to the correct macro XML - object. - - Default values allow in ParamRepeat parameters or the last single ones + """Create of the macro node object. :param macro_name: (str) macro name :param params_def: (list) list of param definitions - :param macro_params: (sequence[str]) list of parameter values - - :return (lxml.etree._Element) macro XML element + :param macro_params: (sequence[str]) list of parameter values, if repeat + parameters are used parameter values may be sequences itself. - .. todo:: This method implements exactly the same logic as :meth: - `sardana.taurus.core.tango.sardana.macroserver.BaseDoor._createMacroXmlFromStr` - unify them and place in some common location. + :return (MacroNode) macro node object """ macro_node = MacroNode(name=macro_name, params_def=params_def) - # Check if ParamRepeat used in advanced interface - new_interface = False - for param in macro_params: - if isinstance(param, list): - new_interface = True - break - - if not new_interface: - param_nodes = macro_node.params() - contain_param_repeat = False - len_param_nodes = len(param_nodes) - for i, param_node in enumerate(param_nodes): - if isinstance(param_node, RepeatParamNode): - if contain_param_repeat: - msg = "Only one repeat parameter is allowed" - raise Exception(msg) - if i < len_param_nodes - 1: - msg = "Repeat parameter must be the last one" - raise Exception(msg) - # If ParamRepeat only one and as last parameter - # this ignores raw_parameters which exceeds the param_def - for param_node, param_raw in zip(param_nodes, macro_params): - if isinstance(param_node, SingleParamNode): - param_node.setValue(param_raw) - # the rest of the values are interpreted as repeat parameter - elif isinstance(param_node, RepeatParamNode): - params_info = param_node.paramsInfo() - params_info_len = len(params_info) - rep = 0 - mem = 0 - rest_raw = macro_params[i:] - for member_raw in rest_raw: - repeat_node = param_node.child(rep) - # add a new repeat node (this is needed when the raw values - # fill more repeat nodes that the minimum number of - # repetitions e.g. min=0 - if repeat_node is None: - repeat_node = param_node.addRepeat() - member_node = repeat_node.child(mem) - if isinstance(member_node, RepeatParamNode): - msg = ("Nested repeat parameters are not allowed") - raise Exception(msg) - member_node.setValue(member_raw) - mem += 1 - mem %= params_info_len - if mem == 0: - rep += 1 - break - else: - macro_node.fromList(macro_params) + macro_node.fromList(macro_params) return macro_node diff --git a/src/sardana/taurus/core/tango/sardana/macroserver.py b/src/sardana/taurus/core/tango/sardana/macroserver.py old mode 100755 new mode 100644 index ea381f2c9f..ae400c53a4 --- a/src/sardana/taurus/core/tango/sardana/macroserver.py +++ b/src/sardana/taurus/core/tango/sardana/macroserver.py @@ -490,16 +490,12 @@ def _clearRunMacro(self): self._block_lines = 0 def _createMacroXml(self, macro_name, macro_params): - """The best effort creation of the macro XML object. It tries to - convert flat list of string parameter values to the correct macro XML - object. The cases that can not be converted are: - * repeat parameter containing repeat parameters - * two repeat parameters - * repeat parameter that is not the last parameter + """Creation of the macro XML object. :param macro_name: (str) macro name - :param macro_params: (sequence[str]) list of parameter values - + :param macro_params: (sequence[str]) list of parameter values, + if repeat parameters are used parameter values may be sequences + itself. :return (lxml.etree._Element) macro XML element """ macro_info = self.macro_server.getMacroInfoObj(macro_name) @@ -559,6 +555,11 @@ def _runMacro(self, xml, synch=False): evt_wait.lock() try: evt_wait.waitEvent(self.Running, equal=False, timeout=timeout) + # Clear event set to not confuse the value coming from the + # connection with the event of of end of the macro execution + # in the next wait event. This was observed on Windows where + # the time stamp resolution is not better than 1 ms. + evt_wait.clearEventSet() ts = time.time() result = self.command_inout("RunMacro", [etree.tostring(xml)]) evt_wait.waitEvent(self.Running, after=ts, timeout=timeout) @@ -1118,8 +1119,11 @@ def fillMacroNodeAdditionalInfos(self, macroNode): macroName = macroNode.name() macroInfoObj = self.getMacroInfoObj(macroName) if macroInfoObj is None: - raise Exception( - "It was not possible to get information about %s macro.\nCheck if MacroServer is alive and if this macro exist." % macroName) + msg = "It was not possible to get information about {0} " \ + "macro. Check if MacroServer is alive and if this macro " \ + "exist.".format(macroName) + self.info(msg) + raise Exception("no info about macro {0}".format(macroName)) allowedHookPlaces = [] hints = macroInfoObj.hints or {} for hook in hints.get("allowsHooks", []): diff --git a/src/sardana/taurus/core/tango/sardana/motion.py b/src/sardana/taurus/core/tango/sardana/motion.py index 1d33b5512a..099c83fcdb 100644 --- a/src/sardana/taurus/core/tango/sardana/motion.py +++ b/src/sardana/taurus/core/tango/sardana/motion.py @@ -34,6 +34,26 @@ from taurus.core.util.containers import CaselessDict +def _get_tango_devstate_match(states): + """ + Retrieve PyTango.DevState match + :param states: + :return: + """ + + import PyTango + state = PyTango.DevState.ON + if PyTango.DevState.FAULT in states: + state = PyTango.DevState.FAULT + elif PyTango.DevState.ALARM in states: + state = PyTango.DevState.ALARM + elif PyTango.DevState.UNKNOWN in states: + state = PyTango.DevState.UNKNOWN + elif PyTango.DevState.MOVING in states: + state = PyTango.DevState.MOVING + return state + + class Moveable: """ An item that can 'move'. In order to move it you need to provide a list of values (normally interpreted as motor positions). @@ -181,16 +201,7 @@ def move(self, new_pos, timeout=None): res = moveable.move(pos, timeout=timeout) states.append(res[0]) positions.extend(res[1]) - import PyTango - state = PyTango.DevState.ON - if PyTango.DevState.FAULT in states: - state = PyTango.DevState.FAULT - elif PyTango.DevState.ALARM in states: - state = PyTango.DevState.ALARM - elif PyTango.DevState.UNKNOWN in states: - state = PyTango.DevState.UNKNOWN - elif PyTango.DevState.MOVING in states: - state = PyTango.DevState.MOVING + state = _get_tango_devstate_match(states) self.__total_motion_time = time.time() - start_time return state, positions @@ -388,18 +399,9 @@ def move(self, new_pos, timeout=None): for moveable, id in zip(self.moveable_list, ids): moveable.waitMove(id=id, timeout=timeout) states, positions = self.readState(), self.readPosition() - import PyTango - state = PyTango.DevState.ON - if PyTango.DevState.FAULT in states: - state = PyTango.DevState.FAULT - elif PyTango.DevState.ALARM in states: - state = PyTango.DevState.ALARM - elif PyTango.DevState.UNKNOWN in states: - state = PyTango.DevState.UNKNOWN - elif PyTango.DevState.MOVING in states: - state = PyTango.DevState.MOVING + state = _get_tango_devstate_match(states) ret = state, positions - self.__total_motion_time = time.time() + self.__total_motion_time = time.time() - start_time return ret def iterMove(self, new_pos, timeout=None): diff --git a/src/sardana/taurus/core/tango/sardana/pool.py b/src/sardana/taurus/core/tango/sardana/pool.py index 4ecbcd193f..2c43502c60 100644 --- a/src/sardana/taurus/core/tango/sardana/pool.py +++ b/src/sardana/taurus/core/tango/sardana/pool.py @@ -672,8 +672,24 @@ class ExpChannel(PoolElement): def __init__(self, name, **kw): """ExpChannel initialization.""" self.call__init__(PoolElement, name, **kw) + self._last_integ_time = None self._value_buffer = {} + def getIntegrationTime(self): + return self._getAttrValue('IntegrationTime') + + def getIntegrationTimeObj(self): + return self._getAttrEG('IntegrationTime') + + def setIntegrationTime(self, ctime): + self.getIntegrationTimeObj().write(ctime) + + def putIntegrationTime(self, ctime): + if self._last_integ_time == ctime: + return + self._last_integ_time = ctime + self.getIntegrationTimeObj().write(ctime) + def getValueObj_(self): """Retrurns Value attribute event generator object. @@ -704,27 +720,62 @@ def valueBufferChanged(self, value_buffer): for index, value in zip(indexes, values): self._value_buffer[index] = value + def _start(self, *args, **kwargs): + self.Start() + + def go(self, *args, **kwargs): + start_time = time.time() + integration_time = args[0] + if integration_time is None or integration_time == 0: + return self.getStateEG().readValue(), self.getValues() + self.putIntegrationTime(integration_time) + PoolElement.go(self) + state = self.getStateEG().readValue() + values = self.getValue() + ret = state, values + self._total_go_time = time.time() - start_time + return ret + + count = go + + +class TimerableExpChannel(ExpChannel): + + def getTimer(self): + return self._getAttrValue('Timer') + + def getTimerObj(self): + return self._getAttrEG('Timer') -class CTExpChannel(ExpChannel): + def setTimer(self, timer): + self.getTimerObj().write(timer) + + +class CTExpChannel(TimerableExpChannel): """ Class encapsulating CTExpChannel functionality.""" pass + class ZeroDExpChannel(ExpChannel): """ Class encapsulating ZeroDExpChannel functionality.""" pass -class OneDExpChannel(ExpChannel): + +class OneDExpChannel(TimerableExpChannel): """ Class encapsulating OneDExpChannel functionality.""" pass -class TwoDExpChannel(ExpChannel): + +class TwoDExpChannel(TimerableExpChannel): """ Class encapsulating TwoDExpChannel functionality.""" pass + class PseudoCounter(ExpChannel): """ Class encapsulating PseudoCounter functionality.""" pass + class TriggerGate(PoolElement): """ Class encapsulating TriggerGate functionality.""" pass @@ -1528,6 +1579,11 @@ def __init__(self, name, **kw): self._value_buffer_cb = None self._codec = CodecFactory().getCodec("json") + def cleanUp(self): + PoolElement.cleanUp(self) + f = self.factory() + f.removeExistingAttribute(self.__cfg_attr) + def _create_str_tuple(self): channel_names = ", ".join(self.getChannelNames()) return self.getName(), self.getTimerName(), channel_names @@ -1670,6 +1726,16 @@ def setSynchronization(self, synchronization): self.getSynchronizationObj().write(data) self._last_integ_time = None + # NbStarts Methods + def getNbStartsObj(self): + return self._getAttrEG('NbStarts') + + def setNbStarts(self, starts): + self.getNbStartsObj().write(starts) + + def getNbStarts(self): + return self._getAttrValue('NbStarts') + def getMoveableObj(self): return self._getAttrEG('Moveable') @@ -1784,18 +1850,24 @@ def _enableChannels(self, channels, state): self.setConfiguration(cfg.raw_data) def _start(self, *args, **kwargs): - self.Start() + try: + self.Start() + except DevFailed as e: + # TODO: Workaround for CORBA timeout on measurement group start + # remove it whenever sardana-org/sardana#93 gets implemented + if e[-1].reason == "API_DeviceTimedOut": + self.error("start timed out, trying to stop") + self.stop() + self.debug("stopped") + raise e - def go(self, *args, **kwargs): - start_time = time.time() - cfg = self.getConfiguration() - cfg.prepare() - duration = args[0] - if duration is None or duration == 0: - return self.getStateEG().readValue(), self.getValues() - self.putIntegrationTime(duration) - self.setMoveable(None) - PoolElement.go(self, *args, **kwargs) + def prepare(self): + self.command_inout("Prepare") + + def count_raw(self, start_time=None): + PoolElement.go(self) + if start_time is None: + start_time = time.time() state = self.getStateEG().readValue() if state == Fault: msg = "Measurement group ended acquisition with Fault state" @@ -1805,7 +1877,20 @@ def go(self, *args, **kwargs): self._total_go_time = time.time() - start_time return ret - def measure(self, synchronization, value_buffer_cb=None): + def go(self, *args, **kwargs): + start_time = time.time() + cfg = self.getConfiguration() + cfg.prepare() + integration_time = args[0] + if integration_time is None or integration_time == 0: + return self.getStateEG().readValue(), self.getValues() + self.putIntegrationTime(integration_time) + self.setMoveable(None) + self.setNbStarts(1) + self.prepare() + return self.count_raw(start_time) + + def count_continuous(self, synchronization, value_buffer_cb=None): """Execute measurement process according to the given synchronization description. @@ -1828,7 +1913,7 @@ class on a provisional basis. Backwards incompatible changes cfg.prepare() self.setSynchronization(synchronization) self.subscribeValueBuffer(value_buffer_cb) - PoolElement.go(self) + self.count_raw(start_time) self.unsubscribeValueBuffer(value_buffer_cb) state = self.getStateEG().readValue() if state == Fault: diff --git a/src/sardana/taurus/core/tango/sardana/test/__init__.py b/src/sardana/taurus/core/tango/sardana/test/__init__.py old mode 100755 new mode 100644 diff --git a/src/sardana/taurus/core/tango/sardana/test/paramdef.py b/src/sardana/taurus/core/tango/sardana/test/paramdef.py old mode 100755 new mode 100644 diff --git a/src/sardana/taurus/core/tango/sardana/test/test_macro.py b/src/sardana/taurus/core/tango/sardana/test/test_macro.py old mode 100755 new mode 100644 index 65ba237852..0f6090537f --- a/src/sardana/taurus/core/tango/sardana/test/test_macro.py +++ b/src/sardana/taurus/core/tango/sardana/test/test_macro.py @@ -41,6 +41,7 @@ pt14d_param_def) # TODO: Use unittest.mock instead of this fake class. from sardana.macroserver.mstypemanager import TypeManager +from sardana.util.parser import ParamParser class FakeMacroServer(object): @@ -72,7 +73,7 @@ class FakeMacroServer(object): "max": None } ] -pt8_params_value = ["mot73", "5.0", "mot74", "8.0"] +pt8_params_str = "mot73 5.0 mot74 8.0" # pt8_xml = \ @@ -91,7 +92,7 @@ class FakeMacroServer(object): @insertTest(helper_name='verifyXML', macro_name="pt8", param_def=pt8_params_def, - param_value=pt8_params_value, expected_xml_rep=pt8_xml) + param_str=pt8_params_str, expected_xml_rep=pt8_xml) class MacroNodeTestCase(unittest.TestCase): def _validateXML(self, macronode_xml, expected_xml): @@ -106,7 +107,7 @@ def _validateXML(self, macronode_xml, expected_xml): # at the end. strips should not be necessary self.assertEquals(expected_str.strip(), macronode_str.strip(), msg) - def verifyXML(self, macro_name, param_def, param_value, expected_xml_rep): + def verifyXML(self, macro_name, param_def, param_str, expected_xml_rep): """ Helper to verify the generated XML of a macroNode :param macro_name: (str) name of the macro @@ -116,6 +117,8 @@ def verifyXML(self, macro_name, param_def, param_value, expected_xml_rep): :param expected_xml_rep: "pretty print" string representation of a XML macroNode """ + param_parser = ParamParser(param_def) + param_value = param_parser.parse(param_str) # Create the MacroNide with the inputs macronode = createMacroNode(macro_name, param_def, param_value) # Get the MacroNode equivalent XML tree diff --git a/src/sardana/taurus/core/tango/sardana/test/test_pool.py b/src/sardana/taurus/core/tango/sardana/test/test_pool.py index e99cd2a5cd..a53f3840e8 100644 --- a/src/sardana/taurus/core/tango/sardana/test/test_pool.py +++ b/src/sardana/taurus/core/tango/sardana/test/test_pool.py @@ -63,20 +63,35 @@ class TestMeasurementGroup(SarTestTestCase, TestCase): def setUp(self): SarTestTestCase.setUp(self) - self.mg_name = str(uuid.uuid1()) registerExtensions() def count(self, elements): - argin = [self.mg_name] + elements + mg_name = str(uuid.uuid1()) + argin = [mg_name] + elements self.pool.CreateMeasurementGroup(argin) try: - self.mg = Device(self.mg_name) - _, values = self.mg.count(1) + mg = Device(mg_name) + _, values = mg.count(1) for channel, value in values.iteritems(): msg = "Value for %s is not numerical" % channel self.assertTrue(is_numerical(value), msg) finally: - self.pool.DeleteElement(self.mg_name) + mg.cleanUp() + self.pool.DeleteElement(mg_name) + + def tearDown(self): + SarTestTestCase.tearDown(self) + + +class TestMotor(SarTestTestCase, TestCase): + + def setUp(self): + SarTestTestCase.setUp(self) + registerExtensions() + + def test_move(self): + mot = Device("_test_mt_1_1") + _, values = mot.move(1) def tearDown(self): SarTestTestCase.tearDown(self) diff --git a/src/sardana/taurus/qt/qtcore/tango/sardana/macroserver.py b/src/sardana/taurus/qt/qtcore/tango/sardana/macroserver.py old mode 100644 new mode 100755 index 5907fa046b..af882eddc1 --- a/src/sardana/taurus/qt/qtcore/tango/sardana/macroserver.py +++ b/src/sardana/taurus/qt/qtcore/tango/sardana/macroserver.py @@ -28,10 +28,12 @@ __all__ = ["QDoor", "QMacroServer", "MacroServerMessageErrorHandler", "registerExtensions"] +import copy from taurus.core.taurusbasetypes import TaurusEventType from taurus.external.qt import Qt -from sardana.taurus.core.tango.sardana.macroserver import BaseMacroServer, BaseDoor +from sardana.taurus.core.tango.sardana.macroserver import BaseMacroServer, \ + BaseDoor CHANGE_EVTS = TaurusEventType.Change, TaurusEventType.Periodic @@ -41,40 +43,41 @@ class QDoor(BaseDoor, Qt.QObject): __pyqtSignals__ = ["resultUpdated", "recordDataUpdated", "macroStatusUpdated"] __pyqtSignals__ += ["%sUpdated" % l.lower() for l in BaseDoor.log_streams] - # TODO: For Taurus 4 compatibility - try: - # sometimes we emit None hence the type is object - # (but most of the data are passed with type list) - resultUpdated = Qt.pyqtSignal(object) - recordDataUpdated = Qt.pyqtSignal(object) - errorUpdated = Qt.pyqtSignal(object) - warningUpdated = Qt.pyqtSignal(object) - infoUpdated = Qt.pyqtSignal(object) - outputUpdated = Qt.pyqtSignal(object) - debugUpdated = Qt.pyqtSignal(object) - except AttributeError: - pass + + EXP_DESC_ENV_VARS = ['ActiveMntGrp', 'ScanDir', 'ScanFile', + 'DataCompressionRank', 'PreScanSnapshot'] + + # sometimes we emit None hence the type is object + # (but most of the data are passed with type list) + resultUpdated = Qt.pyqtSignal(object) + recordDataUpdated = Qt.pyqtSignal(object) + macroStatusUpdated = Qt.pyqtSignal(object) + errorUpdated = Qt.pyqtSignal(object) + warningUpdated = Qt.pyqtSignal(object) + infoUpdated = Qt.pyqtSignal(object) + outputUpdated = Qt.pyqtSignal(object) + debugUpdated = Qt.pyqtSignal(object) + experimentConfigurationChanged = Qt.pyqtSignal(object) + elementsChanged = Qt.pyqtSignal() + environmentChanged = Qt.pyqtSignal() def __init__(self, name, qt_parent=None, **kw): self.call__init__wo_kw(Qt.QObject, qt_parent) self.call__init__(BaseDoor, name, **kw) + self._mntgrps_connected = [] + self._use_experiment_configuration = False + self._connections_prepared = False def resultReceived(self, log_name, result): res = BaseDoor.resultReceived(self, log_name, result) - self.emit(Qt.SIGNAL("resultUpdated"), res) - # TODO: For Taurus 4 compatibility - if hasattr(self, "resultUpdated"): - self.resultUpdated.emit(res) + self.resultUpdated.emit(res) return res def recordDataReceived(self, s, t, v): if t not in CHANGE_EVTS: return res = BaseDoor.recordDataReceived(self, s, t, v) - self.emit(Qt.SIGNAL("recordDataUpdated"), res) - # TODO: For Taurus 4 compatibility - if hasattr(self, "recordDataUpdated"): - self.recordDataUpdated.emit(res) + self.recordDataUpdated.emit(res) return res def macroStatusReceived(self, s, t, v): @@ -85,45 +88,113 @@ def macroStatusReceived(self, s, t, v): macro = self.getRunningMacro() if macro is None: return - self.emit(Qt.SIGNAL("macroStatusUpdated"), (macro, res)) - # TODO: For Taurus 4 compatibility - if hasattr(self, "macroStatusUpdated"): - self.macroStatusUpdated.emit(res) + + self.macroStatusUpdated.emit((macro, res)) return res def logReceived(self, log_name, output): res = BaseDoor.logReceived(self, log_name, output) log_name = log_name.lower() - self.emit(Qt.SIGNAL("%sUpdated" % log_name), output) - # TODO: For Taurus 4 compatibility - try: - recordDataUpdated = getattr(self, "%sUpdated" % log_name) - recordDataUpdated.emit(output) - except AttributeError: - pass + recordDataUpdated = getattr(self, "%sUpdated" % log_name) + recordDataUpdated.emit(output) return res + def _prepare_connections(self): + if not self._use_experiment_configuration and \ + not self._connections_prepared: + self.macro_server.environmentChanged.connect( + self._onEnvironmentChanged) + self.macro_server.elementsChanged.connect(self._elementsChanged) + self._elementsChanged() + self._connections_prepared = True + + def _elementsChanged(self): + mntgrps = self.macro_server.getElementsOfType("MeasurementGroup") + # one or more measurement group was deleted + mntgrp_changed = len(self._mntgrps_connected) > len(mntgrps) + new_mntgrp_connected = [] + for name, mg in mntgrps.items(): + if name not in self._mntgrps_connected: + mntgrp_changed = True # this measurement group is new + obj = mg.getObj() + obj.configurationChanged.connect( + self._onExperimentConfigurationChanged) + new_mntgrp_connected.append(name) + self._mntgrp_connected = new_mntgrp_connected + + if mntgrp_changed: + self._onExperimentConfigurationChanged() + + def _onEnvironmentChanged(self, env_changes): + """ + Filter environment changes that affect to the experiment + configuration. + + :param env_changes: tuple with three elements in the following order: + + * added (environment variables added) + * removed (environment variables removed) + * changed (environment variables changed) + + :type env_changes: :obj:`tuple` + """ + env_exp_changed = False + # Filter only the Environment added/removed/changes related with + # the experiment, not for all. + for envs in env_changes: + val = envs.intersection(self.EXP_DESC_ENV_VARS) + if len(val) > 0: + env_exp_changed = True + break + if not env_exp_changed: + return + self._onExperimentConfigurationChanged() + + def _onExperimentConfigurationChanged(self, *args): + conf = copy.deepcopy(BaseDoor.getExperimentConfiguration(self)) + self.experimentConfigurationChanged.emit(conf) + + def getExperimentConfigurationObj(self): + self._prepare_connections() + return BaseDoor.getExperimentConfigurationObj(self) + + def getExperimentConfiguration(self): + self._prepare_connections() + return BaseDoor.getExperimentConfiguration(self) + class QMacroServer(BaseMacroServer, Qt.QObject): + # TODO: Choose and homogenize signals named ...Updated and ...Changed. + # e.g: there should exist only one signal for elementsUpdated + # and elementsChanged. + typesUpdated = Qt.pyqtSignal() + elementsUpdated = Qt.pyqtSignal() + elementsChanged = Qt.pyqtSignal() + macrosUpdated = Qt.pyqtSignal() + environmentChanged = Qt.pyqtSignal(object) + def __init__(self, name, qt_parent=None, **kw): self.call__init__wo_kw(Qt.QObject, qt_parent) self.call__init__(BaseMacroServer, name, **kw) - def typesChanged(self, s, t, v): - res = BaseMacroServer.typesChanged(self, s, t, v) - self.emit(Qt.SIGNAL("typesUpdated")) - return res - - def elementsChanged(self, s, t, v): - res = BaseMacroServer.elementsChanged(self, s, t, v) - self.emit(Qt.SIGNAL("elementsUpdated")) - return res - - def macrosChanged(self, s, t, v): - res = BaseMacroServer.macrosChanged(self, s, t, v) - self.emit(Qt.SIGNAL("macrosUpdated")) - return res + # TODO: The following three methods are not used, and are not + # implemented in the base class 'BaseMacroServer': Implement them. + # (Now commented because they give conflicts with new style PyQt signals). + # def typesChanged(self, s, t, v): + # res = BaseMacroServer.typesChanged(self, s, t, v) + # self.typesUpdated.emit() + # return res + # + # def elementsChanged(self, s, t, v): + # res = BaseMacroServer.elementsChanged(self, s, t, v) + # self.elementsUpdated.emit() + # return res + # + # def macrosChanged(self, s, t, v): + # res = BaseMacroServer.macrosChanged(self, s, t, v) + # self.macrosUpdated.emit() + # return res def on_elements_changed(self, s, t, v): ret = added, removed, changed = \ @@ -137,17 +208,16 @@ def on_elements_changed(self, s, t, v): if elements and macros: break if elements: - self.emit(Qt.SIGNAL("elementsChanged")) + self.elementsChanged.emit() if macros: - self.emit(Qt.SIGNAL("macrosUpdated")) + self.macrosUpdated.emit() return ret def on_environment_changed(self, s, t, v): ret = added, removed, changed = \ BaseMacroServer.on_environment_changed(self, s, t, v) - if added or removed or changed: - self.emit(Qt.SIGNAL("environmentChanged"), ret) + self.environmentChanged.emit(ret) return ret @@ -171,13 +241,14 @@ def setError(self, err_type=None, err_value=None, err_traceback=None): msg = "
%s
" % err_value msgbox.setDetailedHtml(msg) - html_orig = """""" + html_orig = """""" exc_info = "".join(err_traceback) style = "" try: import pygments.formatters import pygments.lexers - except: + except Exception: pygments = None if pygments is not None: formatter = pygments.formatters.HtmlFormatter() @@ -188,13 +259,16 @@ def setError(self, err_type=None, err_value=None, err_traceback=None): else: formatter = pygments.formatters.HtmlFormatter() html += pygments.highlight(exc_info, - pygments.lexers.PythonTracebackLexer(), formatter) + pygments.lexers.PythonTracebackLexer( + + ), formatter) html += "" msgbox.setOriginHtml(html) def registerExtensions(): - """Registers the macroserver extensions in the :class:`taurus.core.tango.TangoFactory`""" + """Registers the macroserver extensions in the + :class:taurus.core.tango.TangoFactory`""" import taurus factory = taurus.Factory() factory.registerDeviceClass('MacroServer', QMacroServer) @@ -204,8 +278,8 @@ def registerExtensions(): # handlers, maybe in TangoFactory & TaurusManager import sardana.taurus.core.tango.sardana.macro import taurus.qt.qtgui.panel - MacroRunException = sardana.taurus.core.tango.sardana.macro.MacroRunException + MacroRunExcep = sardana.taurus.core.tango.sardana.macro.MacroRunException TaurusMessagePanel = taurus.qt.qtgui.panel.TaurusMessagePanel - TaurusMessagePanel.registerErrorHandler( - MacroRunException, MacroServerMessageErrorHandler) + TaurusMessagePanel.registerErrorHandler(MacroRunExcep, + MacroServerMessageErrorHandler) diff --git a/src/sardana/taurus/qt/qtcore/tango/sardana/model.py b/src/sardana/taurus/qt/qtcore/tango/sardana/model.py index 0e3a529db0..0719bea8c1 100644 --- a/src/sardana/taurus/qt/qtcore/tango/sardana/model.py +++ b/src/sardana/taurus/qt/qtcore/tango/sardana/model.py @@ -175,6 +175,8 @@ def icon(self, index): class SardanaBaseElementModel(TaurusBaseModel): + elementsChanged = Qt.pyqtSignal() + ColumnNames = ["Elements", "Controller/Module/Parent"] ColumnRoles = ('Root', 'type', 'name', 'name'), "parent" @@ -185,11 +187,9 @@ def __init__(self, parent=None, data=None): def setDataSource(self, data_source): old_ds = self.dataSource() if old_ds is not None: - Qt.QObject.disconnect(old_ds, Qt.SIGNAL('elementsChanged'), - self.on_elements_changed) + old_ds.elementsChanged.disconnect(self.on_elements_changed) if data_source is not None: - Qt.QObject.connect(data_source, Qt.SIGNAL('elementsChanged'), - self.on_elements_changed) + data_source.elementsChanged.connect(self.on_elements_changed) TaurusBaseModel.setDataSource(self, data_source) def on_elements_changed(self): @@ -379,6 +379,8 @@ def icon(self, index): class SardanaEnvironmentModel(TaurusBaseModel): + environmentChanged = Qt.pyqtSignal() + ColumnNames = ["Environment", "Value", "Data Type"] ColumnRoles = ('Root', 'key'), 'value', 'datatype' @@ -389,11 +391,9 @@ def __init__(self, parent=None, data=None): def setDataSource(self, data_source): old_ds = self.dataSource() if old_ds is not None: - Qt.QObject.disconnect(old_ds, Qt.SIGNAL('environmentChanged'), - self.on_environment_changed) + old_ds.environmentChanged.disconnect(self.on_environment_changed) if data_source is not None: - Qt.QObject.connect(data_source, Qt.SIGNAL('environmentChanged'), - self.on_environment_changed) + data_source.environmentChanged.connect(self.on_environment_changed) TaurusBaseModel.setDataSource(self, data_source) def on_environment_changed(self): diff --git a/src/sardana/taurus/qt/qtcore/tango/sardana/pool.py b/src/sardana/taurus/qt/qtcore/tango/sardana/pool.py index 9b0ca17fc4..cabe8c9775 100644 --- a/src/sardana/taurus/qt/qtcore/tango/sardana/pool.py +++ b/src/sardana/taurus/qt/qtcore/tango/sardana/pool.py @@ -40,20 +40,22 @@ class QPool(Qt.QObject, TangoDevice): - def __init__(self, name, qt_parent=None, **kw): - self.call__init__wo_kw(Qt.QObject, qt_parent) + def __init__(self, name='', qt_parent=None, **kw): self.call__init__(TangoDevice, name, **kw) + self.call__init__wo_kw(Qt.QObject, qt_parent) class QMeasurementGroup(Qt.QObject, TangoDevice): - def __init__(self, name, qt_parent=None, **kw): - self.call__init__wo_kw(Qt.QObject, qt_parent) + configurationChanged = Qt.pyqtSignal() + + def __init__(self, name='', qt_parent=None, **kw): self.call__init__(TangoDevice, name, **kw) + self.call__init__wo_kw(Qt.QObject, qt_parent) self._config = None - configuration = self.getAttribute("Configuration") - configuration.addListener(self._configurationChanged) + self.__configuration = self.getAttribute("Configuration") + self.__configuration.addListener(self._configurationChanged) def __getattr__(self, name): try: @@ -68,7 +70,7 @@ def _configurationChanged(self, s, t, v): self._config = None else: self._config = json.loads(v.value) - self.emit(Qt.SIGNAL("configurationChanged")) + self.configurationChanged.emit() def getConfiguration(self, cache=True): if self._config is None or not cache: diff --git a/src/sardana/taurus/qt/qtgui/extra_hkl/computeu.py b/src/sardana/taurus/qt/qtgui/extra_hkl/computeu.py index efd984cd04..3c4046ecbf 100644 --- a/src/sardana/taurus/qt/qtgui/extra_hkl/computeu.py +++ b/src/sardana/taurus/qt/qtgui/extra_hkl/computeu.py @@ -42,8 +42,7 @@ def __init__(self, parent=None, designMode=False): self.loadUi(filename="computeu.ui") - self.connect(self._ui.ComputeButton, Qt.SIGNAL( - "clicked()"), self.compute_u) + self._ui.ComputeButton.clicked.connect(self.compute_u) @classmethod def getQtDesignerPluginInfo(cls): diff --git a/src/sardana/taurus/qt/qtgui/extra_hkl/diffractometeralignment.py b/src/sardana/taurus/qt/qtgui/extra_hkl/diffractometeralignment.py index 3b86002ffc..560e4986ba 100644 --- a/src/sardana/taurus/qt/qtgui/extra_hkl/diffractometeralignment.py +++ b/src/sardana/taurus/qt/qtgui/extra_hkl/diffractometeralignment.py @@ -79,16 +79,15 @@ def __init__(self, parent=None, designMode=False): self.selectsignal = SelectSignal() - self.connect(self._ui.AlignmentStopButton, - Qt.SIGNAL("clicked()"), self.stop_movements) - self.connect(self._ui.AlignmentStoreReflectionButton, - Qt.SIGNAL("clicked()"), self.store_reflection) + self._ui.AlignmentStopButton.clicked.connect(self.stop_movements) + self._ui.AlignmentStoreReflectionButton.clicked.connect( + self.store_reflection) - self.connect(self._ui.MacroServerConnectionButton, Qt.SIGNAL( - "clicked()"), self.open_macroserver_connection_panel) + self._ui.MacroServerConnectionButton.clicked.connect( + self.open_macroserver_connection_panel) - self.connect(self._ui.SelectSignalButton, Qt.SIGNAL( - "clicked()"), self.open_selectsignal_panel) + self._ui.SelectSignalButton.clicked.connect( + self.open_selectsignal_panel) # Create a global SharedDataManager Qt.qApp.SDM = SharedDataManager(self) @@ -173,8 +172,7 @@ def setModel(self, model): alname = "angleslabel" + str(i) angles_labels[i].setObjectName(alname) angles_labels[i].setText(QtGui.QApplication.translate( - "HKLScan", self.angles_names[i], None, - QtGui.QApplication.UnicodeUTF8)) + "HKLScan", self.angles_names[i], None)) angles_taurus_label.append(TaurusLabel(self)) angles_taurus_label[i].setGeometry( @@ -203,8 +201,8 @@ def setModel(self, model): self.enginemodescombobox.loadEngineModeNames(self.device.hklmodelist) - self.connect(self.enginemodescombobox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onModeChanged) + self.enginemodescombobox.currentIndexChanged['QString'].connect( + self.onModeChanged) # Add dynamically the scan buttons, range inputs and 'to max' buttons @@ -227,10 +225,8 @@ def setModel(self, model): wname = "scanbutton" + str(i) scan_buttons[i].setObjectName(wname) scan_buttons[i].setText(QtGui.QApplication.translate( - "DiffractometerAlignment", self.angles_names[i], None, - QtGui.QApplication.UnicodeUTF8)) - self.connect(scan_buttons[i], Qt.SIGNAL( - "clicked()"), exec_functions[i]) + "DiffractometerAlignment", self.angles_names[i], None)) + scan_buttons[i].clicked.connect(exec_functions[i]) self.range_inputs.append(QtGui.QLineEdit(self)) self.range_inputs[i].setGeometry( @@ -245,10 +241,8 @@ def setModel(self, model): wname = "tomaxbutton" + str(i) self.tomax_buttons[i].setObjectName(wname) self.tomax_buttons[i].setText(QtGui.QApplication.translate( - "DiffractometerAlignment", 'n.n.', None, - QtGui.QApplication.UnicodeUTF8)) - self.connect(self.tomax_buttons[i], Qt.SIGNAL( - "clicked()"), tomax_functions[i]) + "DiffractometerAlignment", 'n.n.', None)) + self.tomax_buttons[i].clicked.connect(tomax_functions[i]) def exec_scan1(self): self.exec_scan(0) @@ -293,7 +287,7 @@ def exec_scan(self, imot): if output_values[i] == "Position to move": self.tomax_buttons[imot].setText(QtGui.QApplication.translate( "DiffractometerAlignment", str(output_values[i + 1]), - None, QtGui.QApplication.UnicodeUTF8)) + None)) def tomax_scan1(self): self.tomax_scan(0) @@ -319,6 +313,7 @@ def tomax_scan(self, imot): macro_command = ["mv", motor, position] self.door_device.RunMacro(macro_command) + @Qt.pyqtSlot('QString') def onModeChanged(self, modename): if self.device.engine != "hkl": self.device.write_attribute("engine", "hkl") diff --git a/src/sardana/taurus/qt/qtgui/extra_hkl/hklscan.py b/src/sardana/taurus/qt/qtgui/extra_hkl/hklscan.py index 0d6519d986..3300f8f36e 100644 --- a/src/sardana/taurus/qt/qtgui/extra_hkl/hklscan.py +++ b/src/sardana/taurus/qt/qtgui/extra_hkl/hklscan.py @@ -75,14 +75,11 @@ def __init__(self, parent=None, designMode=False): self.loadUi(filename="hklscan.ui") - self.connect(self._ui.hklStartScanButton, - Qt.SIGNAL("clicked()"), self.start_hklscan) - self.connect(self._ui.hklStopScanButton, - Qt.SIGNAL("clicked()"), self.stop_hklscan) - self.connect(self._ui.hklDisplayAnglesButton, - Qt.SIGNAL("clicked()"), self.display_angles) - self.connect(self._ui.MacroServerConnectionButton, Qt.SIGNAL( - "clicked()"), self.open_macroserver_connection_panel) + self._ui.hklStartScanButton.clicked.connect(self.start_hklscan) + self._ui.hklStopScanButton.clicked.connect(self.stop_hklscan) + self._ui.hklDisplayAnglesButton.clicked.connect(self.display_angles) + self._ui.MacroServerConnectionButton.clicked.connect( + self.open_macroserver_connection_panel) # Create a global SharedDataManager Qt.qApp.SDM = SharedDataManager(self) @@ -149,7 +146,7 @@ def setModel(self, model): alname = "angleslabel" + str(i) angles_labels[i].setObjectName(alname) angles_labels[i].setText(QtGui.QApplication.translate( - "HKLScan", angles_names[i], None, QtGui.QApplication.UnicodeUTF8)) + "HKLScan", angles_names[i], None)) angles_taurus_label.append(TaurusLabel(self)) angles_taurus_label[i].setGeometry( QtCore.QRect(50 + gap_x * i, 320, 81, 19)) @@ -182,9 +179,10 @@ def setModel(self, model): self.enginemodescombobox.loadEngineModeNames(self.device.hklmodelist) - self.connect(self.enginemodescombobox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onModeChanged) + self.enginemodescombobox.currentIndexChanged['QString'].connect( + self.onModeChanged) + @Qt.pyqtSlot('QString') def onModeChanged(self, modename): if self.device.engine != "hkl": self.device.write_attribute("engine", "hkl") @@ -259,7 +257,7 @@ def display_angles(self): label_name = "dsa_label_" + str(i) dsa_label[i].setObjectName(label_name) dsa_label[i].setText(QtGui.QApplication.translate( - "Form", angles_names[i], None, QtGui.QApplication.UnicodeUTF8)) + "Form", angles_names[i], None)) start_hkl = [] stop_hkl = [] diff --git a/src/sardana/taurus/qt/qtgui/extra_hkl/reflectionseditor.py b/src/sardana/taurus/qt/qtgui/extra_hkl/reflectionseditor.py index 3ffbd97951..5dd213e0fc 100644 --- a/src/sardana/taurus/qt/qtgui/extra_hkl/reflectionseditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_hkl/reflectionseditor.py @@ -44,8 +44,8 @@ def __init__(self, parent=None, designMode=False): self.loadUi(filename="reflectionseditor.ui") - self.connect(self._ui.ApplyButton, Qt.SIGNAL("clicked()"), self.apply) - self.connect(self._ui.ClearButton, Qt.SIGNAL("clicked()"), self.clear) + self._ui.ApplyButton.clicked.connect(self.apply) + self._ui.ClearButton.clicked.connect(self.clear) @classmethod def getQtDesignerPluginInfo(cls): @@ -128,7 +128,7 @@ def setModel(self, model): object_name = "anglelabel" + str(i) self.angle_labels[i].setObjectName(object_name) self.angle_labels[i].setText(QtGui.QApplication.translate( - "Form", angle_names[i], None, QtGui.QApplication.UnicodeUTF8)) + "Form", angle_names[i], None)) self.angle_values.append([]) for jref in range(0, 10): self.angle_values[i].append(QtGui.QLineEdit(self)) diff --git a/src/sardana/taurus/qt/qtgui/extra_hkl/selectsignal.py b/src/sardana/taurus/qt/qtgui/extra_hkl/selectsignal.py index d87ef5da6d..4ac80a56b5 100644 --- a/src/sardana/taurus/qt/qtgui/extra_hkl/selectsignal.py +++ b/src/sardana/taurus/qt/qtgui/extra_hkl/selectsignal.py @@ -47,7 +47,7 @@ class SignalComboBox(Qt.QComboBox, TaurusBaseWidget): def __init__(self, parent=None): name = self.__class__.__name__ self.call__init__wo_kw(Qt.QComboBox, parent) - self.call__init__(TaurusBaseWidget, name) + self.call__init__(TaurusBaseWidget, name='') self.setSizeAdjustPolicy(Qt.QComboBox.AdjustToContentsOnFirstShow) self.setToolTip("Choose a signal ...") QtCore.QMetaObject.connectSlotsByName(self) @@ -61,7 +61,7 @@ def loadSignals(self, signals): class SelectSignal(TaurusWidget): def __init__(self, parent=None, designMode=False): - TaurusWidget.__init__(self, parent, designMode=designMode) + TaurusWidget.__init__(self, parent=None, designMode=designMode) self.loadUi(filename="selectsignal.ui") @@ -69,8 +69,8 @@ def __init__(self, parent=None, designMode=False): self.signalComboBox.setGeometry(QtCore.QRect(70, 50, 161, 27)) self.signalComboBox.setObjectName("SignalcomboBox") - self.connect(self.signalComboBox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onSignalChanged) + self.signalComboBox.currentIndexChanged['QString'].connect( + self.onSignalChanged) self.doorName = None self.door_device = None @@ -86,7 +86,7 @@ def getQtDesignerPluginInfo(cls): ret['container'] = False return ret - def update_signals(self, doorname): + def update_signals(self, doorname=''): if self.doorName != doorname: self.doorName = doorname self.door_device = taurus.Device(self.doorName) @@ -98,7 +98,6 @@ def update_signals(self, doorname): mg_name = conf['ActiveMntGrp'] mg = taurus.Device(mg_name) signals = mg.ElementList - self.signalComboBox.loadSignals(signals) def onSignalChanged(self, signalname): diff --git a/src/sardana/taurus/qt/qtgui/extra_hkl/ubmatrix.py b/src/sardana/taurus/qt/qtgui/extra_hkl/ubmatrix.py index 8887fcec3a..bdb8a85d3e 100644 --- a/src/sardana/taurus/qt/qtgui/extra_hkl/ubmatrix.py +++ b/src/sardana/taurus/qt/qtgui/extra_hkl/ubmatrix.py @@ -74,19 +74,15 @@ def __init__(self, parent=None, designMode=False): self.loadUi(filename="ubmatrix.ui") - self.connect(self._ui.UpdateButton, Qt.SIGNAL( - "clicked()"), self.update_values) - self.connect(self._ui.ComputeUButton, - Qt.SIGNAL("clicked()"), self.compute_ub) - self.connect(self._ui.ReflectionsListButton, Qt.SIGNAL( - "clicked()"), self.reflections_list_window) - self.connect(self._ui.EditReflectionsButton, Qt.SIGNAL( - "clicked()"), self.edit_reflections_window) - self.connect(self._ui.AffineButton, - Qt.SIGNAL("clicked()"), self.affine) - self.connect(self._ui.AddCrystalButton, Qt.SIGNAL( - "clicked()"), self.add_select_crystal) -# self.connect(self._ui.alattice_value, Qt.SIGNAL("textEdited()"), self.on_alattice_value_textEdited) + self._ui.UpdateButton.clicked.connect(self.update_values) + self._ui.ComputeUButton.clicked.connect(self.compute_ub) + self._ui.ReflectionsListButton.clicked.connect( + self.reflections_list_window) + self._ui.EditReflectionsButton.clicked.connect( + self.edit_reflections_window) + self._ui.AffineButton.clicked.connect(self.affine) + self._ui.AddCrystalButton.clicked.connect(self.add_select_crystal) +# self._ui.alattice_value.textEdited.connect(self.on_alattice_value_textEdited) # Funciona con puro QEditValue pero no con TaurusQEdit ... @classmethod @@ -157,8 +153,8 @@ def setModel(self, model): self.enginescombobox.loadItems(self.device.enginelist) - self.connect(self.enginescombobox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onEngineChanged) + self.enginescombobox.currentIndexChanged['QString'].connect( + self.onEngineChanged) enginemodemodel = model + '/enginemode' self._ui.taurusLabelEngineMode.setModel(enginemodemodel) @@ -169,8 +165,8 @@ def setModel(self, model): self.enginemodescombobox.loadItems(self.device.enginemodelist) - self.connect(self.enginemodescombobox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onModeChanged) + self.enginemodescombobox.currentIndexChanged['QString'].connect( + self.onModeChanged) # Set model to crystal @@ -183,15 +179,18 @@ def setModel(self, model): self.crystalscombobox.loadItems(self.device.crystallist) - self.connect(self.crystalscombobox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onCrystalChanged) + self.crystalscombobox.currentIndexChanged['QString'].connect( + self.onCrystalChanged) + Qt.pyqtSlot('QString') def onEngineChanged(self, enginename): self.device.write_attribute("engine", str(enginename)) + Qt.pyqtSlot('QString') def onModeChanged(self, modename): self.device.write_attribute("enginemode", str(modename)) + Qt.pyqtSlot('QString') def onCrystalChanged(self, crystalname): if str(crystalname) != "": self.device.write_attribute("crystal", str(crystalname)) @@ -298,13 +297,13 @@ def reflections_list_window(self): # 4circles diffractometer if len(ref) == 10: self.rl_label1_7.setText(QtGui.QApplication.translate( - "Form", self.angle_names[0], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[0], None)) self.rl_label1_8.setText(QtGui.QApplication.translate( - "Form", self.angle_names[1], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[1], None)) self.rl_label1_9.setText(QtGui.QApplication.translate( - "Form", self.angle_names[2], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[2], None)) self.rl_label1_10.setText(QtGui.QApplication.translate( - "Form", self.angle_names[3], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[3], None)) # 6 circles diffractometer elif len(ref) == 12: self.rl_label1_11 = QtGui.QLabel(w) @@ -316,17 +315,17 @@ def reflections_list_window(self): QtCore.QRect(xangle6 + 20, 70, 41, 20)) self.rl_label1_12.setObjectName("rl_label1_12") self.rl_label1_7.setText(QtGui.QApplication.translate( - "Form", self.angle_names[0], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[0], None)) self.rl_label1_8.setText(QtGui.QApplication.translate( - "Form", self.angle_names[1], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[1], None)) self.rl_label1_9.setText(QtGui.QApplication.translate( - "Form", self.angle_names[2], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[2], None)) self.rl_label1_10.setText(QtGui.QApplication.translate( - "Form", self.angle_names[3], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[3], None)) self.rl_label1_11.setText(QtGui.QApplication.translate( - "Form", self.angle_names[4], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[4], None)) self.rl_label1_12.setText(QtGui.QApplication.translate( - "Form", self.angle_names[5], None, QtGui.QApplication.UnicodeUTF8)) + "Form", self.angle_names[5], None)) self.taurusValueIndex.append(TaurusValueLineEdit(w)) self.taurusValueIndex[nb_ref].setGeometry( @@ -441,7 +440,7 @@ def reflections_list_window(self): self.rl_label_nor.setFont(font) self.rl_label_nor.setObjectName("rl_label_nor") self.rl_label_nor.setText(QtGui.QApplication.translate( - "Form", "NO REFLECTIONS", None, QtGui.QApplication.UnicodeUTF8)) + "Form", "NO REFLECTIONS", None)) w.show() w.show() diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/common.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/common.py index 9ff779a4ff..aaa9fa771b 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/common.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/common.py @@ -119,25 +119,26 @@ def selectMacro(self, macroName): index = self.findText(macroName) self.setCurrentIndex(index) if currentIdx == index: - self.emit(Qt.SIGNAL("currentIndexChanged(QString)"), macroName) - + self.currentIndexChanged['QString'].emit(macroName) class TaurusMacroConfigurationDialog(Qt.QDialog): + macroserverNameChanged = Qt.pyqtSignal('QString') + doorNameChanged = Qt.pyqtSignal('QString') + def __init__(self, parent=None, initMacroServer=None, initDoor=None): Qt.QDialog.__init__(self, parent) self.initMacroServer = initMacroServer self.initDoor = initDoor configureAction = Qt.QAction(getThemeIcon( "folder-open"), "Change custom macro editors paths", self) - self.connect(configureAction, Qt.SIGNAL( - "triggered()"), self.onReloadMacroServers) + configureAction.triggered.connect(self.onReloadMacroServers) configureAction.setToolTip("Change custom macro editors paths") configureAction.setShortcut("F11") self.refreshMacroServersAction = Qt.QAction( getThemeIcon("view-refresh"), "Reload macroservers", self) - self.connect(self.refreshMacroServersAction, Qt.SIGNAL( - "triggered()"), self.onReloadMacroServers) + self.refreshMacroServersAction.triggered.connect( + self.onReloadMacroServers) self.refreshMacroServersAction.setToolTip( "This will reload list of all macroservers from Tango DB") self.refreshMacroServersAction.setShortcut("F5") @@ -171,20 +172,18 @@ def initComponents(self): self.layout().addWidget(self.buttonBox) self.adjustSize() - self.connect(self.buttonBox, Qt.SIGNAL( - "accepted()"), self, Qt.SLOT("accept()")) - self.connect(self.buttonBox, Qt.SIGNAL( - "rejected()"), self, Qt.SLOT("reject()")) - self.connect(self.macroServerComboBox, Qt.SIGNAL( - "currentIndexChanged(const QString&)"), self.onMacroServerComboBoxChanged) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + self.macroServerComboBox.currentIndexChanged['QString'].connect( + self.onMacroServerComboBoxChanged) self.selectMacroServer(self.initMacroServer) self.selectDoor(self.initDoor) def accept(self): - self.emit(Qt.SIGNAL("macroserverNameChanged"), str( - self.macroServerComboBox.currentText())) - self.emit(Qt.SIGNAL("doorNameChanged"), str( - self.doorComboBox.currentText())) + self.macroserverNameChanged.emit( + str(self.macroServerComboBox.currentText())) + self.doorNameChanged.emit( + str(self.doorComboBox.currentText())) Qt.QDialog.accept(self) def __retriveMacroServersFromDB(self): @@ -252,6 +251,8 @@ def selectMacroServer(self, macroServerName): class MacroExecutionWindow(TaurusMainWindow): + doorChanged = Qt.pyqtSignal('QString') + def __init__(self, parent=None, designMode=False): TaurusMainWindow.__init__(self, parent, designMode) self.statusBar().showMessage("") @@ -273,7 +274,7 @@ def __init__(self, parent=None, designMode=False): self.addToolBar(toolBar) self.initComponents() self.splashScreen().finish(self) - self.connect(self, Qt.SIGNAL("doorChanged"), self.onDoorChanged) + self.doorChanged.connect(self.onDoorChanged) def doorName(self): return self._doorName @@ -310,8 +311,7 @@ def setModel(self, model): def createConfigureAction(self): configureAction = Qt.QAction(getThemeIcon( "preferences-system-session"), "Change configuration", self) - self.connect(configureAction, Qt.SIGNAL( - "triggered()"), self.changeConfiguration) + configureAction.triggered.connect(self.changeConfiguration) configureAction.setToolTip("Configuring MacroServer and Door") configureAction.setShortcut("F10") return configureAction @@ -319,8 +319,7 @@ def createConfigureAction(self): def createCustomMacroEditorPathsAction(self): configureAction = Qt.QAction(getThemeIcon( "folder-open"), "Change custom macro editors paths", self) - self.connect(configureAction, Qt.SIGNAL( - "triggered()"), self.onCustomMacroEditorPaths) + configureAction.triggered.connect(self.onCustomMacroEditorPaths) configureAction.setToolTip("Change custom macro editors paths") configureAction.setShortcut("F11") return configureAction @@ -333,8 +332,7 @@ def changeConfiguration(self): self, self.modelName, self.doorName()) if dialog.exec_(): self.setModel(str(dialog.macroServerComboBox.currentText())) - self.emit(Qt.SIGNAL("doorChanged"), str( - dialog.doorComboBox.currentText())) + self.doorChanged.emit(str(dialog.doorComboBox.currentText())) else: return diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/dooroutput.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/dooroutput.py index 330eb5b62a..1a9af58a31 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/dooroutput.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/dooroutput.py @@ -104,9 +104,8 @@ def contextMenuEvent(self, event): if not len(self.toPlainText()): clearAction.setEnabled(False) - Qt.QObject.connect(clearAction, Qt.SIGNAL("triggered()"), self.clear) - Qt.QObject.connect(self.stopAction, Qt.SIGNAL( - "toggled(bool)"), self.stopScrolling) + clearAction.triggered.connect(self.clear) + self.stopAction.toggled.connect(self.stopScrolling) menu.exec_(event.globalPos()) def stopScrolling(self, stop): @@ -143,9 +142,8 @@ def contextMenuEvent(self, event): if not len(self.toPlainText()): clearAction.setEnabled(False) - Qt.QObject.connect(clearAction, Qt.SIGNAL("triggered()"), self.clear) - Qt.QObject.connect(self.stopAction, Qt.SIGNAL( - "toggled(bool)"), self.stopScrolling) + clearAction.triggered.connect(self.clear) + self.stopAction.toggled.connect(self.stopScrolling) menu.exec_(event.globalPos()) def stopScrolling(self, stop): @@ -175,7 +173,7 @@ def contextMenuEvent(self, event): if not len(self.toPlainText()): clearAction.setEnabled(False) - Qt.QObject.connect(clearAction, Qt.SIGNAL("triggered()"), self.clear) + clearAction.triggered.connect(self.clear) menu.exec_(event.globalPos()) @@ -184,9 +182,12 @@ def contextMenuEvent(self, event): #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- class DoorAttrListener(Qt.QObject): + """Deprecated. Do not use""" def __init__(self, attrName): Qt.QObject.__init__(self) + from taurus.core.util.log import deprecated + deprecated(dep="DoorAttrListener", rel="2.5.1") self.attrName = attrName self.attrObj = None @@ -200,7 +201,14 @@ def eventReceived(self, src, type, value): if (type == taurus.core.taurusbasetypes.TaurusEventType.Error or type == taurus.core.taurusbasetypes.TaurusEventType.Config): return - self.emit(Qt.SIGNAL('door%sChanged' % self.attrName), value.value) + + # The old code (using old-style signals) emitted a signal called + # doorChanged . Emulating this with new-style signasl + # is problematic, and in this case it is not worth it since this class + # is unused, deprecated and will disappear soon + # self.emit(Qt.SIGNAL('door%sChanged' % self.attrName), value.value) + + if __name__ == "__main__": import sys @@ -213,13 +221,10 @@ def eventReceived(self, src, type, value): doorOutput = DoorOutput() if len(args) == 1: door = taurus.Device(args[0]) - Qt.QObject.connect(door, Qt.SIGNAL("outputUpdated"), - doorOutput.onDoorOutputChanged) - Qt.QObject.connect(door, Qt.SIGNAL("infoUpdated"), - doorOutput.onDoorInfoChanged) - Qt.QObject.connect(door, Qt.SIGNAL("warningUpdated"), - doorOutput.onDoorWarningChanged) - Qt.QObject.connect(door, Qt.SIGNAL("errorUpdated"), - doorOutput.onDoorErrorChanged) + door.outputUpdated.connect(doorOutput.onDoorOutputChanged) + door.infoUpdated.connect(doorOutput.onDoorInfoChanged) + door.warningUpdated.connect(doorOutput.onDoorWarningChanged) + door.errorUpdated.connect(doorOutput.onDoorErrorChanged) + doorOutput.show() sys.exit(app.exec_()) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/favouriteseditor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/favouriteseditor.py index 5b83faa398..d1d763811c 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/favouriteseditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/favouriteseditor.py @@ -103,38 +103,36 @@ def getQtDesignerPluginInfo(cls): class FavouritesMacrosList(Qt.QListView, BaseConfigurableClass): - def __init__(self, parent): + favouriteSelected = Qt.pyqtSignal(object) + + def __init__(self, parent=None): Qt.QListView.__init__(self, parent) self.setSelectionMode(Qt.QListView.ExtendedSelection) self.removeAction = Qt.QAction(getIcon(":/actions/list-remove.svg"), "Remove from favourites", self) - self.connect(self.removeAction, Qt.SIGNAL("triggered()"), - self.removeMacros) + self.removeAction.triggered.connect(self.removeMacros) self.removeAction.setToolTip( "Clicking this button will remove selected macros " "from favourites.") self.removeAllAction = Qt.QAction(getIcon(":/places/user-trash.svg"), "Remove all from favourites", self) - self.connect(self.removeAllAction, Qt.SIGNAL( - "triggered()"), self.removeAllMacros) + self.removeAllAction.triggered.connect(self.removeAllMacros) self.removeAllAction.setToolTip( "Clicking this button will remove all macros from favourites.") self.moveUpAction = Qt.QAction(getIcon(":/actions/go-up.svg"), "Move up", self) - self.connect(self.moveUpAction, Qt.SIGNAL( - "triggered()"), self.upMacro) + self.moveUpAction.triggered.connect(self.upMacro) self.moveUpAction.setToolTip( "Clicking this button will move the macro up " "in the favourites hierarchy.") self.moveDownAction = Qt.QAction(getIcon(":/actions/go-down.svg"), "Move down", self) - self.connect(self.moveDownAction, Qt.SIGNAL( - "triggered()"), self.downMacro) + self.moveDownAction.triggered.connect(self.downMacro) self.moveDownAction.setToolTip( "Clicking this button will move the macro down " "in the favourites hierarchy.") @@ -143,7 +141,7 @@ def __init__(self, parent): def currentChanged(self, current, previous): macro = copy.deepcopy(self.currentIndex().internalPointer()) - self.emit(Qt.SIGNAL("favouriteSelected"), macro) + self.favouriteSelected.emit(macro) Qt.QListView.currentChanged(self, current, previous) def selectionChanged(self, old, new): @@ -174,7 +172,7 @@ def mousePressEvent(self, e): clickedIndex = self.indexAt(e.pos()) if clickedIndex.isValid(): macro = copy.deepcopy(self.currentIndex().internalPointer()) - self.emit(Qt.SIGNAL("favouriteSelected"), macro) + self.favouriteSelected.emit(macro) Qt.QListView.mousePressEvent(self, e) def disableActions(self): diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/historyviewer.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/historyviewer.py index 96397f2538..4f2a3406ec 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/historyviewer.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/historyviewer.py @@ -114,28 +114,29 @@ def getQtDesignerPluginInfo(cls): class HistoryMacrosList(Qt.QListView, BaseConfigurableClass): - def __init__(self, parent): + historySelected = Qt.pyqtSignal(object) + + def __init__(self, parent=None): Qt.QListView.__init__(self, parent) self.setSelectionMode(Qt.QListView.SingleSelection) self.removeAllAction = Qt.QAction(getIcon(":/places/user-trash.svg"), "Remove all from history", self) - self.connect(self.removeAllAction, Qt.SIGNAL( - "triggered()"), self.removeAllMacros) + self.removeAllAction.triggered.connect(self.removeAllMacros) self.removeAllAction.setToolTip( "Clicking this button will remove all macros from history.") self.removeAllAction.setEnabled(False) def currentChanged(self, current, previous): macro = copy.deepcopy(self.currentIndex().internalPointer()) - self.emit(Qt.SIGNAL("historySelected"), macro) + self.historySelected.emit(macro) Qt.QListView.currentChanged(self, current, previous) def mousePressEvent(self, e): clickedIndex = self.indexAt(e.pos()) if clickedIndex.isValid(): macro = copy.deepcopy(self.currentIndex().internalPointer()) - self.emit(Qt.SIGNAL("historySelected"), macro) + self.historySelected.emit(macro) self.removeAllAction.setEnabled(True) Qt.QListView.mousePressEvent(self, e) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/model.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/model.py index fadf8ab422..449cb30681 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/model.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/favouriteseditor/model.py @@ -45,9 +45,9 @@ def rowCount(self, parent=Qt.QModelIndex()): def data(self, index, role): if index.isValid() and role == Qt.Qt.DisplayRole: macroNode = self.list[index.row()] - return Qt.QVariant(self.list[index.row()].toSpockCommand()) + return self.list[index.row()].toSpockCommand() else: - return Qt.QVariant() + return None def index(self, row, column=0, parent=Qt.QModelIndex()): if self.rowCount(): @@ -96,9 +96,10 @@ def toXmlString(self, pretty=False): return xmlString def fromXmlString(self, xmlString): + self.beginResetModel() listElement = etree.fromstring(xmlString) for childElement in listElement.iterchildren("macro"): macroNode = macro.MacroNode() macroNode.fromXml(childElement) self.list.append(macroNode) - self.reset() + self.endResetModel() diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macrobutton.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macrobutton.py old mode 100755 new mode 100644 index 4654aff27a..b10c89b1ec --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macrobutton.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macrobutton.py @@ -53,11 +53,13 @@ class DoorStateListener(Qt.QObject): __pyqtSignals__ = ["doorStateChanged"] + doorStateChanged = Qt.pyqtSignal(object) + def eventReceived(self, evt_src, evt_type, evt_value): if evt_type not in (TaurusEventType.Change, TaurusEventType.Periodic): return door_state = evt_value.value - self.emit(Qt.SIGNAL('doorStateChanged'), door_state) + self.doorStateChanged.emit(door_state) @UILoadable(with_ui='ui') @@ -72,6 +74,9 @@ class MacroButton(TaurusWidget): __pyqtSignals__ = ['statusUpdated', 'resultUpdated'] + statusUpdated = Qt.pyqtSignal(object) + resultUpdated = Qt.pyqtSignal(object) + def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) self.loadUi() @@ -85,8 +90,7 @@ def __init__(self, parent=None, designMode=False): self.ui.progress.setValue(0) self.ui.button.setCheckable(True) - self.connect(self.ui.button, Qt.SIGNAL('clicked()'), - self._onButtonClicked) + self.ui.button.clicked.connect(self._onButtonClicked) # Override default implementation of handleEvent from TaurusWidget # in order to avoid button's text being lost on the MS restart. @@ -113,16 +117,14 @@ def setModel(self, model): ''' TaurusWidget.setModel(self, model) if self.door is not None: - self.disconnect(self.door, Qt.SIGNAL( - 'macroStatusUpdated'), self._statusUpdated) - self.disconnect(self.door, Qt.SIGNAL( - 'resultUpdated'), self._resultUpdated) + self.door.macroStatusUpdated.disconnect(self._statusUpdated) + self.door.resultUpdated.disconnect(self._resultUpdated) # disable management of Door Tango States self.door.getAttribute('State').removeListener( self.door_state_listener) - self.disconnect(self.door_state_listener, Qt.SIGNAL( - 'doorStateChanged'), self._doorStateChanged) + self.door_state_listener.doorStateChanged.disconnect( + self._doorStateChanged) self.door_state_listener = None try: @@ -130,15 +132,13 @@ def setModel(self, model): except: return - self.connect(self.door, Qt.SIGNAL( - 'macroStatusUpdated'), self._statusUpdated) - self.connect(self.door, Qt.SIGNAL( - 'resultUpdated'), self._resultUpdated) + self.door.macroStatusUpdated.connect(self._statusUpdated) + self.door.resultUpdated.connect(self._resultUpdated) # Manage Door Tango States self.door_state_listener = DoorStateListener() - self.connect(self.door_state_listener, Qt.SIGNAL( - 'doorStateChanged'), self._doorStateChanged) + self.door_state_listener.doorStateChanged.connect( + self._doorStateChanged) self.door.getAttribute('State').addListener(self.door_state_listener) def _doorStateChanged(self, state): @@ -191,7 +191,7 @@ def _statusUpdated(self, *args): if state in ['stop', 'abort', 'finish', 'alarm']: self.ui.button.setChecked(False) - self.emit(Qt.SIGNAL('statusUpdated'), status_dict) + self.statusUpdated.emit(status_dict) def _resultUpdated(self, *args): '''slot called on result changes''' @@ -201,7 +201,7 @@ def _resultUpdated(self, *args): if self.running_macro is None: return result = self.running_macro.getResult() - self.emit(Qt.SIGNAL('resultUpdated'), result) + self.resultUpdated.emit(result) def setText(self, text): '''set the button text @@ -214,7 +214,7 @@ def setButtonText(self, text): '''same as :meth:`setText` ''' # SHOULD ALSO BE POSSIBLE TO SET AN ICON - self.ui.button.setText("Run/Abort:\n" + text) + self.ui.button.setText("Run/Stop:\n" + text) def setMacroName(self, name): '''set the name of the macro to be executed @@ -253,25 +253,36 @@ def updateMacroArgumentFromSignal(self, index, obj, signal): functools.partial(self.updateMacroArgument, index)) def connectArgEditors(self, signals): - '''Associate signals to argument changes. - - :param signals: (seq) An ordered sequence of (`obj`, `sig`) - tuples , where `obj` is a parameter editor object and - `sig` is a signature for a signal emitted by `obj` which - provides the value of a parameter as its argument. - Each (`obj`, `sig`) tuple is associated to parameter - corresponding to its position in the `signals` sequence. - ''' - - for i, (obj, sig) in enumerate(signals): - self.connect(obj, Qt.SIGNAL(sig), - functools.partial(self.updateMacroArgument, i)) + """ + Associate signals to argument changes. + + :param signals: (seq) An ordered sequence of signals + """ + + for i, signal in enumerate(signals): + if not self.__isSignal(signal): + # bck-compat: (sender, sig) tuples used instead of pyqtsignals + sender, sig = signal + self.deprecated(dep='Passing (sender, signature) tuples', + alt='pyqtSignal objects', rel='2.5.1') + signal = getattr(sender, sig.split('(')[0]) + signal.connect(functools.partial(self.updateMacroArgument, i)) + + @staticmethod + def __isSignal(obj): + if not hasattr(obj, 'emit'): + return False + if not hasattr(obj, 'connect'): + return False + if not hasattr(obj, 'disconnect'): + return False + return True def _onButtonClicked(self): if self.ui.button.isChecked(): self.runMacro() else: - self.abort() + self.stop() @ProtectTaurusMessageBox(msg='Error while executing the macro.') def runMacro(self): @@ -292,8 +303,13 @@ def runMacro(self): self.ui.button.setChecked(False) raise e + # For backward compatibility def abort(self): - '''abort the macro.''' + self.warning("abort is not implemented, stop is being called instead") + self.stop() + + def stop(self): + '''stop the macro.''' if self.door is None: return self.door.PauseMacro() @@ -301,15 +317,15 @@ def abort(self): # we provide a warning message that does not make the process too slow # It may also be useful and 'ABORT' at TaurusApplication level # (macros+motions+acquisitions) - title = 'Aborting macro' + title = 'Stopping macro' message = 'The following macro is still running:\n\n' message += '%s %s\n\n' % (self.macro_name, ' '.join(self.macro_args)) - message += 'Are you sure you want to abort?\n' + message += 'Are you sure you want to stop?\n' buttons = Qt.QMessageBox.Ok | Qt.QMessageBox.Cancel ans = Qt.QMessageBox.warning( self, title, message, buttons, Qt.QMessageBox.Ok) if ans == Qt.QMessageBox.Ok: - self.door.abort(synch=True) + self.door.stop(synch=True) else: self.ui.button.setChecked(True) self.door.ResumeMacro() @@ -338,7 +354,7 @@ def __init__(self, parent=None, designMode=False): self.setText('Abort') self.setToolTip('Abort Macro') - self.connect(self, Qt.SIGNAL('clicked()'), self.abort) + self.clicked.connect(self.abort) def getModelClass(self): '''reimplemented from :class:`TaurusBaseWidget`''' @@ -474,10 +490,10 @@ def create_layout(self, macro_name): self.w_bottom.layout().addWidget(mb_abort, 3, 1) # Toggle progressbar - Qt.QObject.connect(self.show_progress, Qt.SIGNAL( - 'stateChanged(int)'), self.toggle_progress) + self.show_progress.stateChanged.connect(self.toggle_progress) # connect the argument editors - signals = [(e, 'textChanged(QString)') for e in _argEditors] + # signals = [(e, 'textChanged(QString)') for e in _argEditors] + signals = [getattr(e, 'textChanged') for e in _argEditors] self.mb.connectArgEditors(signals) self.setLayout(Qt.QVBoxLayout()) @@ -485,17 +501,12 @@ def create_layout(self, macro_name): self.layout().addWidget(self.w_bottom) # Update possible macro result - Qt.QObject.connect(self.mb, Qt.SIGNAL( - 'resultUpdated'), self.update_result) - - Qt.QObject.connect(self.w_macro_name, Qt.SIGNAL( - 'textEdited(QString)'), self.update_macro_name) - Qt.QObject.connect(self.w_macro_name, Qt.SIGNAL( - 'editingFinished()'), self.update_layout) - Qt.QObject.connect(self.w_macro_name, Qt.SIGNAL( - 'textChanged(QString)'), self.mb.setMacroName) - Qt.QObject.connect(self.w_macro_name, Qt.SIGNAL( - 'textChanged(QString)'), self.mb.setButtonText) + self.mb.resultUpdated.connect(self.update_result) + + self.w_macro_name.textEdited.connect(self.update_macro_name) + self.w_macro_name.editingFinished.connect(self.update_layout) + self.w_macro_name.textChanged.connect(self.mb.setMacroName) + self.w_macro_name.textChanged.connect(self.mb.setButtonText) # Since everything is now connected, the parameters will be updated self.w_macro_name.setText(macro_name) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroeditor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroeditor.py index 85481add09..042eed69e3 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroeditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroeditor.py @@ -23,8 +23,14 @@ ## ############################################################################## -from taurus.external.qt import Qt -from PyQt4 import Qsci +from taurus.external.qt import Qt, compat + +try: + # TODO: Check if qscintilla can be used with other bindings + from PyQt4 import Qsci +except Exception as e: + raise ImportError('MacroEditor requires Qsci (qscintilla): %r', e) + from taurus.qt.qtgui.resource import getThemeIcon @@ -49,37 +55,36 @@ def __init__(self, parent=None, designMode=False): self.textEdit.setLexer(self.pythonLexer) self.newAction = Qt.QAction(getThemeIcon("document-new"), "New", self) - self.connect(self.newAction, Qt.SIGNAL("triggered()"), self.newFile) + self.newAction.triggered.connect(self.newFile) self.newAction.setToolTip("Create new file") self.newAction.setShortcut("Ctrl+N") self.openAction = Qt.QAction( getThemeIcon("document-open"), "Open", self) - self.connect(self.openAction, Qt.SIGNAL("triggered()"), self.openFile) + self.openAction.triggered.connect(self.openFile) self.openAction.setToolTip("Open existing file") self.openAction.setShortcut("Ctrl+O") self.saveAction = Qt.QAction( getThemeIcon("document-save"), "Save", self) - self.connect(self.saveAction, Qt.SIGNAL("triggered()"), self.saveFile) + self.saveAction.triggered.connect(self.saveFile) self.saveAction.setToolTip("Save document to disk") self.saveAction.setShortcut("Ctrl+S") self.saveAsAction = Qt.QAction(getThemeIcon( "document-save-as"), "Save as...", self) - self.connect(self.saveAsAction, Qt.SIGNAL( - "triggered()"), self.saveFile) + self.saveAction.triggered.connect(self.saveFile) self.saveAsAction.setToolTip("Save document under a new name") self.cutAction = Qt.QAction(getThemeIcon("edit-cut"), "Cut", self) - self.connect(self.cutAction, Qt.SIGNAL("triggered()"), self.cut) + self.cutAction.triggered.connect(self.cut) self.cutAction.setToolTip( "Cut current selection's contents to the clipboard") self.cutAction.setShortcut("Ctrl+X") self.cutAction.setEnabled(False) self.copyAction = Qt.QAction(getThemeIcon("edit-copy"), "Copy", self) - self.connect(self.copyAction, Qt.SIGNAL("triggered()"), self.copy) + self.copyAction.triggered.connect(self.copy) self.copyAction.setToolTip( "Copy current selection's contents to the clipboard") self.copyAction.setShortcut("Ctrl+C") @@ -87,19 +92,17 @@ def __init__(self, parent=None, designMode=False): self.pasteAction = Qt.QAction( getThemeIcon("edit-paste"), "Paste", self) - self.connect(self.pasteAction, Qt.SIGNAL("triggered()"), self.paste) + self.pasteAction.triggered.connect(self.paste) self.pasteAction.setToolTip( "Paste the clipboard's contents into the current selection") self.pasteAction.setShortcut("Ctrl+V") self.aboutAction = Qt.QAction("About", self) - self.connect(self.aboutAction, Qt.SIGNAL("triggered()"), self.about) + self.aboutAction.triggered.connect(self.about) self.aboutAction.setToolTip("Show the application's About box") - self.connect(self.textEdit, Qt.SIGNAL( - "copyAvailable(bool)"), self.cutAction.setEnabled) - self.connect(self.textEdit, Qt.SIGNAL( - "copyAvailable(bool)"), self.copyAction.setEnabled) + self.textEdit.copyAvailable.connect(self.cutAction.setEnabled) + self.textEdit.copyAvailable.connect(self.copyAction.setEnabled) self.setCurrentFile("") @@ -117,7 +120,7 @@ def newFile(self): def openFile(self): if self.maybeSave(): - fileName = Qt.QFileDialog.getOpenFileName(self) + fileName, _ = compat.getOpenFileName(self) if not fileName is None and file != "": self.loadFile(fileName) @@ -128,7 +131,7 @@ def saveFile(self): return self.__saveFile(self.curFile) def __saveAs(self): - self.fileName = Qt.QFileDialog.getSaveFileName(self) + self.fileName, _ = compat.getSaveFileName(self) if self.fileName == "": return False return self.__saveFile(self.fileName) @@ -190,7 +193,7 @@ def maybeSave(self): def loadFile(self, fileName): try: fileHandle = open(fileName, 'r') - except IOError, e: + except IOError as e: Qt.QMessageBox.warning(self, "MacroEditor", "Cannot read file %s:\n%s." % (fileName, e)) return False @@ -205,7 +208,7 @@ def loadFile(self, fileName): def __saveFile(self, fileName): try: file = open(fileName, 'w') - except IOError, e: + except IOError as e: Qt.QMessageBox.warning( self, "MacroEditor", "Cannot write file %s:\n%s." % (fileName, e)) return False diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py index f3115c55bb..7bfeee426d 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py @@ -47,6 +47,8 @@ from .favouriteseditor import FavouritesMacrosEditor, HistoryMacrosViewer from .common import MacroComboBox, MacroExecutionWindow, standardPlotablesFilter +from sardana.macroserver.msparameter import Optional + class MacroProgressBar(Qt.QProgressBar): @@ -56,7 +58,14 @@ def __init__(self, parent=None): class SpockCommandWidget(Qt.QLineEdit, TaurusBaseContainer): - def __init__(self, name, parent=None, designMode=False): + pressedReturn = Qt.pyqtSignal() + spockComboBox = Qt.pyqtSignal('QString') + elementUp = Qt.pyqtSignal() + elementDown = Qt.pyqtSignal() + setHistoryFocus = Qt.pyqtSignal() + expandTree = Qt.pyqtSignal() + + def __init__(self, name='', parent=None, designMode=False): # self.newValue - is used as a flag to indicate whether a controlUp controlDown actions are used to iterate existing element or put new one # self.disableEditMode - flag, used to disable edition, when user enters name of the macro which is not valid (not allowed to edit in the yellow line) # switches off validation @@ -78,9 +87,8 @@ def __init__(self, name, parent=None, designMode=False): self.setEnabled(False) self.setActions() - self.connect(self, Qt.SIGNAL( - "textChanged(const QString &)"), self.textChanged) - self.connect(self, Qt.SIGNAL("returnPressed()"), self.returnPressed) + self.textChanged.connect(self.onTextChanged) + self.returnPressed.connect(self.onReturnPressed) def setActions(self): self._downAction = Qt.QAction("downAction", self) @@ -104,13 +112,10 @@ def setActions(self): self.addAction(self._ctrlUpAction) self.addAction(self._downAction) self.addAction(self._upAction) - self.connect(self._downAction, Qt.SIGNAL( - "triggered()"), self.downAction) - self.connect(self._upAction, Qt.SIGNAL("triggered()"), self.upAction) - self.connect(self._ctrlDownAction, Qt.SIGNAL( - "triggered()"), self.controlDownAction) - self.connect(self._ctrlUpAction, Qt.SIGNAL( - "triggered()"), self.controlUpAction) + self._downAction.triggered.connect(self.downAction) + self._upAction.triggered.connect(self.upAction) + self._ctrlDownAction.triggered.connect(self.controlDownAction) + self._ctrlUpAction.triggered.connect(self.controlUpAction) def setCommand(self): command = self._model.toSpockCommand().strip() @@ -136,9 +141,8 @@ def setModel(self, model): self.disableEditMode = not enable self.setEnabled(enable) self._model = model - self.connect(self._model, Qt.SIGNAL( - "dataChanged(QModelIndex,QModelIndex)"), self.onDataChanged) - self.connect(self._model, Qt.SIGNAL("modelReset()"), self.setCommand) + self._model.dataChanged.connect(self.onDataChanged) + self._model.modelReset.connect(self.setCommand) def model(self): return self._model @@ -256,31 +260,44 @@ def validateAllExpresion(self, secValidation=False): self.setStyleSheet("") self.setToolTip('
'.join(problems)) return - self.currentIndex = Qt.QModelIndex() ix = self.getIndex() self.currentIndex = ix counter = 1 + + # Get the parameters information to check if there are optional + # paramters + ms_obj = self.getModelObj() + macro_obj = ms_obj.getElementInfo(mlist[0]) + macro_params_info = None + if macro_obj is not None: + macro_params_info = macro_obj.parameters + while not ix == Qt.QModelIndex(): + if macro_params_info is None: + break try: propValue = mlist[counter] try: self.validateOneValue(propValue) - self.model().setData(self.currentIndex, Qt.QVariant(propValue)) + self.model().setData(self.currentIndex, propValue) except Exception as e: - self.model().setData(self.currentIndex, Qt.QVariant('None')) - txt = str(Qt.from_qvariant( - ix.sibling(ix.row(), 0).data(), str)) + self.model().setData(self.currentIndex, 'None') + txt = str(ix.sibling(ix.row(), 0).data()) message = "" + txt + " " + e[0] problems.append(message) except IndexError: - txt = str(Qt.from_qvariant( - ix.sibling(ix.row(), 0).data(), str)) - problems.append("" + txt + " is missing!") + param_info = macro_params_info[counter-1] + # Skip validation in case of optional parameters + if param_info['default_value'] == Optional: + self.model().setData(self.currentIndex, None) + else: + txt = str(ix.sibling(ix.row(), 0).data()) + problems.append("" + txt + " is missing!") - data = str(Qt.from_qvariant(ix.data(), str)) - if data != 'None': - self.model().setData(self.currentIndex, Qt.QVariant('None')) + data = str(ix.data()) + if data != 'None': + self.model().setData(self.currentIndex, 'None') counter += 1 ix = self.getIndex() self.currentIndex = ix @@ -293,7 +310,7 @@ def validateAllExpresion(self, secValidation=False): index = self.findParamRepeat(i) self.currentIndex = self.model()._insertRow(index) nn = self.model().nodeFromIndex(self.currentIndex) - self.emit(Qt.SIGNAL("expandTree")) + self.expandTree.emit() ix = self.getIndex() if not secValidation: self.validateAllExpresion(True) @@ -351,15 +368,15 @@ def validateOneValue(self, value): paramNode.setValue(value) return self.getModelObj().validateSingleParam(paramNode) - def returnPressed(self): + def onReturnPressed(self): # SLOT called when return is pressed if self.toolTip() == "": - self.emit(Qt.SIGNAL("pressedReturn")) + self.pressedReturn.emit() else: raise Exception( "Cannot start macro. Please correct following mistakes:
" + self.toolTip()) - def textChanged(self, strs): + def onTextChanged(self, strs): # SLOT called when QLineEdit text is changed if strs == "": self.updateMacroEditor("") @@ -410,7 +427,7 @@ def downAction(self): # line when model is changed. (when new row in history is chosen) self.disableSpockCommandUpdate = False - self.emit(Qt.SIGNAL("elementDown")) + self.elementDown.emit() text = str(self.text()).split() if len(text) > 0: self.validateMacro(text[0]) @@ -418,7 +435,7 @@ def downAction(self): def upAction(self): self.disableSpockCommandUpdate = False - self.emit(Qt.SIGNAL("elementUp")) + self.elementUp.emit() text = str(self.text()).split() if len(text) > 0: self.validateMacro(text[0]) @@ -452,7 +469,7 @@ def controlDownAction(self): value = self.prevValue("") self.backspace() self.insert(value) - self.model().setData(self.currentIndex, Qt.QVariant(value)) + self.model().setData(self.currentIndex, value) else: self.currentIndex = self.getIndex(len(elementsNum) - 1) if not self.currentIndex.isValid(): @@ -464,7 +481,7 @@ def controlDownAction(self): c = c - (sel[1] - len(str(value))) self.insert(value) self.setCursorPosition(c) - self.model().setData(self.currentIndex, Qt.QVariant(value)) + self.model().setData(self.currentIndex, value) def controlUpAction(self): c = self.cursorPosition() @@ -494,7 +511,7 @@ def controlUpAction(self): value = self.nextValue("") self.backspace() self.insert(value) - self.model().setData(self.currentIndex, Qt.QVariant(value)) + self.model().setData(self.currentIndex, value) else: self.currentIndex = self.getIndex(len(elementsNum) - 1) if not self.currentIndex.isValid(): @@ -506,7 +523,7 @@ def controlUpAction(self): c = c - (sel[1] - len(str(value))) self.insert(value) self.setCursorPosition(c) - self.model().setData(self.currentIndex, Qt.QVariant(value)) + self.model().setData(self.currentIndex, value) def getParamItems(self, index): # Returns list of items that can be chosen for the node corresponding @@ -580,7 +597,7 @@ def updateMacroEditor(self, macroName): # I had to make the macroname lowered as macros in comboBox (with macros), has names with all letter low. # Because of that sometimes it was not loading macros in MacroEditor # TO FIX - self.emit(Qt.SIGNAL("spockComboBox"), str(macroName).lower()) + self.spockComboBox.emit(str(macroName).lower()) def measureSelection(self, position): s = str(self.text()) + " " @@ -606,6 +623,12 @@ def focusOutEvent(self, event): class TaurusMacroExecutorWidget(TaurusWidget): + doorChanged = Qt.pyqtSignal('QString') + macroNameChanged = Qt.pyqtSignal('QString') + macroStarted = Qt.pyqtSignal('QString') + plotablesFilterChanged = Qt.pyqtSignal(object) + shortMessageEmitted = Qt.pyqtSignal('QString') + def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) self.setObjectName(self.__class__.__name__) @@ -617,23 +640,19 @@ def __init__(self, parent=None, designMode=False): self.addToFavouritesAction = Qt.QAction(getThemeIcon( "software-update-available"), "Add to favourites", self) - self.connect(self.addToFavouritesAction, Qt.SIGNAL( - "triggered()"), self.onAddToFavourites) + self.addToFavouritesAction.triggered.connect(self.onAddToFavourites) self.addToFavouritesAction.setToolTip("Add to favourites") self.stopMacroAction = Qt.QAction( getIcon(":/actions/media_playback_stop.svg"), "Stop macro", self) - self.connect(self.stopMacroAction, Qt.SIGNAL( - "triggered()"), self.onStopMacro) + self.stopMacroAction.triggered.connect(self.onStopMacro) self.stopMacroAction.setToolTip("Stop macro") self.pauseMacroAction = Qt.QAction( getIcon(":/actions/media_playback_pause.svg"), "Pause macro", self) - self.connect(self.pauseMacroAction, Qt.SIGNAL( - "triggered()"), self.onPauseMacro) + self.pauseMacroAction.triggered.connect(self.onPauseMacro) self.pauseMacroAction.setToolTip("Pause macro") self.playMacroAction = Qt.QAction( getIcon(":/actions/media_playback_start.svg"), "Start macro", self) - self.connect(self.playMacroAction, Qt.SIGNAL( - "triggered()"), self.onPlayMacro) + self.playMacroAction.triggered.connect(self.onPlayMacro) self.playMacroAction.setToolTip("Start macro") actionsLayout = Qt.QHBoxLayout() actionsLayout.setContentsMargins(0, 0, 0, 0) @@ -712,25 +731,20 @@ def __init__(self, parent=None, designMode=False): # spockCommandLayout.addWidget(spockCommandLabel) spockCommandLayout.addWidget(self.spockCommand) self.layout().addLayout(spockCommandLayout) - self.connect(self.macroComboBox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onMacroComboBoxChanged) - self.connect(self.favouritesMacrosEditor.list, Qt.SIGNAL( - "favouriteSelected"), self.onFavouriteSelected) - self.connect(self.historyMacrosViewer.list, Qt.SIGNAL( - "historySelected"), self.onHistorySelected) - - self.connect(self.spockCommand, Qt.SIGNAL( - "pressedReturn"), self.onPlayMacro) - self.connect(self.spockCommand, Qt.SIGNAL( - "spockComboBox"), self.setComboBoxItem) - self.connect(self.spockCommand, Qt.SIGNAL( - "elementUp"), self.setHistoryUp) - self.connect(self.spockCommand, Qt.SIGNAL( - "elementDown"), self.setHistoryDown) - self.connect(self.spockCommand, Qt.SIGNAL( - "setHistoryFocus"), self.setHistoryFocus) - self.connect(self.spockCommand, Qt.SIGNAL("expandTree"), - self.standardMacroParametersEditor.tree.expandAll) + + self.macroComboBox.currentIndexChanged['QString'].connect( + self.onMacroComboBoxChanged) + self.favouritesMacrosEditor.list.favouriteSelected.connect( + self.onFavouriteSelected) + self.historyMacrosViewer.list.historySelected.connect( + self.onHistorySelected) + + self.spockCommand.pressedReturn.connect(self.onPlayMacro) + self.spockCommand.spockComboBox.connect(self.setComboBoxItem) + self.spockCommand.elementUp.connect(self.setHistoryUp) + self.spockCommand.elementDown.connect(self.setHistoryDown) + self.spockCommand.expandTree.connect( + self.standardMacroParametersEditor.tree.expandAll) def macroId(self): return self._macroId @@ -804,6 +818,7 @@ def setParamEditorModel(self, paramEditorModel): def setComboBoxItem(self, macroName): self.macroComboBox.selectMacro(macroName) + @Qt.pyqtSlot('QString') def onMacroComboBoxChanged(self, macroName): macroName = str(macroName) if macroName == "": @@ -844,7 +859,7 @@ def onMacroComboBoxChanged(self, macroName): self.standardMacroParametersEditor.setModel( self.paramEditorModel()) - self.emit(Qt.SIGNAL("macroNameChanged"), macroName) + self.macroNameChanged.emit(macroName) def onFavouriteSelected(self, macroNode): self.setFavouritesBuffer(macroNode) @@ -960,14 +975,13 @@ def onMacroStatusUpdated(self, data): macroName = macro.name shortMessage = "" if state == "start": - self.emit(Qt.SIGNAL("macroStarted"), "DoorOutput") + self.macroStarted.emit("DoorOutput") self.macroProgressBar.setRange(range[0], range[1]) self.playMacroAction.setEnabled(False) self.pauseMacroAction.setEnabled(True) self.stopMacroAction.setEnabled(True) - self.emit(Qt.SIGNAL("plotablesFilterChanged"), None) - self.emit(Qt.SIGNAL("plotablesFilterChanged"), - standardPlotablesFilter) + self.plotablesFilterChanged.emit(None) + self.plotablesFilterChanged.emit(standardPlotablesFilter) shortMessage = "Macro %s started." % macroName elif state == "pause": self.playMacroAction.setText("Resume macro") @@ -1004,7 +1018,7 @@ def onMacroStatusUpdated(self, data): shortMessage = "Macro %s stopped." % macroName elif state == "step": shortMessage = "Macro %s at %d %% of progress." % (macroName, step) - self.emit(Qt.SIGNAL("shortMessageEmitted"), shortMessage) + self.shortMessageEmitted.emit(shortMessage) self.macroProgressBar.setValue(step) def disableControlActions(self): @@ -1015,12 +1029,13 @@ def disableControlActions(self): def setModel(self, model): oldModelObj = self.getModelObj() if oldModelObj is not None: - self.disconnect(oldModelObj, Qt.SIGNAL( - "macrosUpdated"), self.macroComboBox.onMacrosUpdated) + # TODO: check if macrosUpdated signal exists + oldModelObj.macrosUpdated.disconnect( + self.macroComboBox.onMacrosUpdated) TaurusWidget.setModel(self, model) newModelObj = self.getModelObj() - self.connect(newModelObj, Qt.SIGNAL("macrosUpdated"), - self.macroComboBox.onMacrosUpdated) + newModelObj.macrosUpdated.connect( + self.macroComboBox.onMacrosUpdated) @classmethod def getQtDesignerPluginInfo(cls): @@ -1040,8 +1055,8 @@ def initComponents(self): self.registerConfigDelegate(self.taurusMacroExecutorWidget) self.taurusMacroExecutorWidget.setUseParentModel(True) self.setCentralWidget(self.taurusMacroExecutorWidget) - self.connect(self.taurusMacroExecutorWidget, Qt.SIGNAL( - 'shortMessageEmitted'), self.onShortMessage) + self.taurusMacroExecutorWidget.shortMessageEmitted.connect( + self.onShortMessage) self.statusBar().showMessage("MacroExecutor ready") def setCustomMacroEditorPaths(self, customMacroEditorPaths): @@ -1052,18 +1067,18 @@ def setCustomMacroEditorPaths(self, customMacroEditorPaths): def loadSettings(self): TaurusMainWindow.loadSettings(self) - self.emit(Qt.SIGNAL("doorChanged"), self.doorName()) + self.doorChanged.emit(self.doorName()) def onDoorChanged(self, doorName): MacroExecutionWindow.onDoorChanged(self, doorName) if self._qDoor: - Qt.QObject.disconnect(self._qDoor, Qt.SIGNAL( - "macroStatusUpdated"), self.taurusMacroExecutorWidget.onMacroStatusUpdated) + self._qDoor.macroStatusUpdated.disconnect( + self.taurusMacroExecutorWidget.onMacroStatusUpdated) if doorName == "": return self._qDoor = Device(doorName) - Qt.QObject.connect(self._qDoor, Qt.SIGNAL( - "macroStatusUpdated"), self.taurusMacroExecutorWidget.onMacroStatusUpdated) + self._qDoor.macroStatusUpdated.connect( + self.taurusMacroExecutorWidget.onMacroStatusUpdated) self.taurusMacroExecutorWidget.onDoorChanged(doorName) @classmethod @@ -1074,22 +1089,20 @@ def getQtDesignerPluginInfo(cls): def createMacroExecutorWidget(args): macroExecutor = TaurusMacroExecutorWidget() macroExecutor.setModelInConfig(True) - Qt.QObject.connect(macroExecutor, Qt.SIGNAL( - "doorChanged"), macroExecutor.onDoorChanged) + macroExecutor.doorChanged.connect(macroExecutor.onDoorChanged) if len(args) == 2: macroExecutor.setModel(args[0]) - macroExecutor.emit(Qt.SIGNAL('doorChanged'), args[1]) + macroExecutor.doorChanged.emit(args[1]) return macroExecutor def createMacroExecutor(args): macroExecutor = TaurusMacroExecutor() macroExecutor.setModelInConfig(True) - Qt.QObject.connect(macroExecutor, Qt.SIGNAL( - "doorChanged"), macroExecutor.onDoorChanged) + macroExecutor.doorChanged.connect(macroExecutor.onDoorChanged) if len(args) == 2: macroExecutor.setModel(args[0]) - macroExecutor.emit(Qt.SIGNAL('doorChanged'), args[1]) + macroExecutor.doorChanged.emit(args[1]) macroExecutor.loadSettings() return macroExecutor diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/customeditors/senv.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/customeditors/senv.py index ee283dc0d0..0ad5dfa871 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/customeditors/senv.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/customeditors/senv.py @@ -52,8 +52,8 @@ def initComponents(self): self.nameComboBox.addItems( ["ActiveMntGrp", "ExtraColumns", "JsonRecorder", "ScanFile", "ScanDir"]) self.nameComboBox.setEditable(True) - self.connect(self.nameComboBox, Qt.SIGNAL( - "currentIndexChanged(int)"), self.onNameComboBoxChanged) + self.nameComboBox.currentIndexChanged.connect( + self.onNameComboBoxChanged) self.layout().addRow("name:", self.nameComboBox) nameIndex = self.model().index(0, 1, self.rootIndex()) @@ -75,6 +75,7 @@ def setModel(self, model): self.setRootIndex(Qt.QModelIndex()) def onNameComboBoxChanged(self, index): + # note that the index parameter is ignored! text = str(self.nameComboBox.currentText()) if self.valueWidget is not None: label = self.layout().labelForField(self.valueWidget) @@ -150,14 +151,11 @@ def __init__(self, parent=None, paramModel=None): self.layout().addWidget(self.extraColumnsTable) - self.connect(addNewColumnButton, Qt.SIGNAL( - "clicked()"), self.onAddNewColumn) - self.connect(removeSelectedColumnsButton, Qt.SIGNAL( - "clicked()"), self.onRemoveSelectedColumns) - self.connect(self.extraColumnsModel, Qt.SIGNAL( - "dataChanged (const QModelIndex&,const QModelIndex&)"), self.onExtraColumnsChanged) - self.connect(self.extraColumnsModel, Qt.SIGNAL( - "modelReset()"), self.onExtraColumnsChanged) + addNewColumnButton.clicked.connect(self.onAddNewColumn) + removeSelectedColumnsButton.clicked.connect( + self.onRemoveSelectedColumns) + self.extraColumnsModel.dataChanged.connect(self.onExtraColumnsChanged) + self.extraColumnsModel.modelReset.connect(self.onExtraColumnsChanged) def getValue(self): return repr(self.extraColumnsTable.model().columns()) @@ -171,14 +169,14 @@ def setValue(self, value): def onAddNewColumn(self): self.extraColumnsTable.insertRows() - self.emit(Qt.SIGNAL("modelChanged()")) + self.onModelChanged() def onRemoveSelectedColumns(self): self.extraColumnsTable.removeRows() - self.emit(Qt.SIGNAL("modelChanged()")) + self.onModelChanged() def onExtraColumnsChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) + self.onModelChanged() class ExtraColumnsTable(Qt.QTableView): @@ -233,8 +231,7 @@ def createEditor(self, parent, option, index): def setEditorData(self, editor, index): if index.column() == 2: - text = Qt.from_qvariant(index.model().data( - index, Qt.Qt.DisplayRole), str) + text = index.model().data(index, Qt.Qt.DisplayRole) editor.setCurrentText(text) else: Qt.QItemDelegate.setEditorData(self, editor, index) @@ -248,17 +245,16 @@ def setModelData(self, editor, model, index): taurusTreeAttributeItem = selectedItems[0] itemData = taurusTreeAttributeItem.itemData() if isinstance(itemData, TaurusAttrInfo): - model.setData(index, Qt.QVariant(itemData.fullName())) + model.setData(index, itemData.fullName()) elif column == 2: - model.setData(index, Qt.QVariant(editor.currentText())) + model.setData(index, editor.currentText()) else: Qt.QItemDelegate.setModelData(self, editor, model, index) def sizeHint(self, option, index): if index.column() == 0: fm = option.fontMetrics - text = Qt.from_qvariant(index.model().data( - index, Qt.Qt.DisplayRole), str) + text = index.model().data(index, Qt.Qt.DisplayRole) document = Qt.QTextDocument() document.setDefaultFont(option.font) document.setHtml(text) @@ -286,8 +282,9 @@ def __init__(self, columns=None): self.__columns = columns def setColumns(self, columns): + self.beginResetModel() self.__columns = columns - self.reset() + self.endResetModel() def columns(self): return self.__columns @@ -300,37 +297,37 @@ def columnCount(self, index=Qt.QModelIndex()): def data(self, index, role=Qt.Qt.DisplayRole): if not index.isValid() or not (0 <= index.row() < self.rowCount()): - return Qt.QVariant() + return None row = index.row() column = index.column() # Display Role if role == Qt.Qt.DisplayRole: if column == 0: - return Qt.QVariant(Qt.QString(self.__columns[row]['label'])) + return Qt.QString(self.__columns[row]['label']) elif column == 1: - return Qt.QVariant(Qt.QString(self.__columns[row]['model'])) + return Qt.QString(self.__columns[row]['model']) elif column == 2: - return Qt.QVariant(Qt.QString(self.__columns[row]['instrument'])) - return Qt.QVariant() + return Qt.QString(self.__columns[row]['instrument']) + return None def headerData(self, section, orientation, role=Qt.Qt.DisplayRole): if role == Qt.Qt.TextAlignmentRole: if orientation == Qt.Qt.Horizontal: - return Qt.QVariant(int(Qt.Qt.AlignLeft | Qt.Qt.AlignVCenter)) - return Qt.QVariant(int(Qt.Qt.AlignRight | Qt.Qt.AlignVCenter)) + return int(Qt.Qt.AlignLeft | Qt.Qt.AlignVCenter) + return int(Qt.Qt.AlignRight | Qt.Qt.AlignVCenter) if role != Qt.Qt.DisplayRole: - return Qt.QVariant() + return None # So this is DisplayRole... if orientation == Qt.Qt.Horizontal: if section == 0: - return Qt.QVariant("Label") + return "Label" elif section == 1: - return Qt.QVariant("Attribute") + return "Attribute" elif section == 2: - return Qt.QVariant("Instrument") - return Qt.QVariant() + return "Instrument" + return None else: - return Qt.QVariant(Qt.QString.number(section + 1)) + return str(section + 1) def flags(self, index): flags = Qt.Qt.ItemIsEnabled | Qt.Qt.ItemIsSelectable @@ -344,15 +341,13 @@ def setData(self, index, value=None, role=Qt.Qt.EditRole): if index.isValid() and (0 <= index.row() < self.rowCount()): row = index.row() column = index.column() - value = Qt.from_qvariant(value, str) if column == 0: self.__columns[row]['label'] = value elif column == 1: self.__columns[row]['model'] = value elif column == 2: self.__columns[row]['instrument'] = value - self.emit( - Qt.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) + self.dataChanged.emit(index, index) return True return False diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/delegate.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/delegate.py index 0645d2d18d..5f584a7271 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/delegate.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/delegate.py @@ -79,8 +79,7 @@ def createEditor(self, parent, option, index): def setEditorData(self, editor, index): if index.column() == 1: - text = Qt.from_qvariant(index.model().data( - index, Qt.Qt.DisplayRole), str) + text = index.model().data(index, Qt.Qt.DisplayRole) if text == "None" or text == "" or text is None: Qt.QStyledItemDelegate.setEditorData(self, editor, index) else: @@ -102,15 +101,14 @@ def setEditorData(self, editor, index): def setModelData(self, editor, model, index): if index.column() == 1: - model.setData(index, Qt.QVariant(editor.getValue())) + model.setData(index, editor.getValue()) else: Qt.QStyledItemDelegate.setModelData(self, editor, model, index) def sizeHint(self, option, index): if index.column() == 0: fm = option.fontMetrics - text = Qt.from_qvariant(index.model().data( - index, Qt.Qt.DisplayRole), str) + text = index.model().data(index, Qt.Qt.DisplayRole) document = Qt.QTextDocument() document.setDefaultFont(option.font) document.setHtml(text) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/macroparameterseditor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/macroparameterseditor.py index 27e2b3a8be..de58b3125c 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/macroparameterseditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/macroparameterseditor.py @@ -118,35 +118,30 @@ def __init__(self, parent=None, designMode=False): self.addAction = Qt.QAction(getThemeIcon( "list-add"), "Add new repetition", self) - self.connect(self.addAction, Qt.SIGNAL( - "triggered()"), self.onAddRepeat) + self.addAction.triggered.connect(self.onAddRepeat) self.addAction.setToolTip( "Clicking this button will add new repetition to current parameter.") self.deleteAction = Qt.QAction(getThemeIcon( "list-remove"), "Remove repetition", self) - self.connect(self.deleteAction, Qt.SIGNAL( - "triggered()"), self.onDelRepeat) + self.deleteAction.triggered.connect(self.onDelRepeat) self.deleteAction.setToolTip( "Clicking this button will remove current repetition.") self.moveUpAction = Qt.QAction(getThemeIcon("go-up"), "Move up", self) - self.connect(self.moveUpAction, Qt.SIGNAL( - "triggered()"), self.onUpRepeat) + self.moveUpAction.triggered.connect(self.onUpRepeat) self.moveUpAction.setToolTip( "Clicking this button will move current repetition up.") self.moveDownAction = Qt.QAction( getThemeIcon("go-down"), "Move down", self) - self.connect(self.moveDownAction, Qt.SIGNAL( - "triggered()"), self.onDownRepeat) + self.moveDownAction.triggered.connect(self.onDownRepeat) self.moveDownAction.setToolTip( "Clicking this button will move current repetition down.") self.duplicateAction = Qt.QAction(getThemeIcon("edit-copy"), "Duplicate", self) - self.connect(self.duplicateAction, Qt.SIGNAL("triggered()"), - self.onDuplicateRepeat) + self.duplicateAction.triggered.connect(self.onDuplicateRepeat) msg = "Clicking this button will duplicate the given node." self.duplicateAction.setToolTip(msg) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/model.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/model.py index fad224e602..6cf78837f8 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/model.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/model.py @@ -48,10 +48,11 @@ def root(self): return self._root def setRoot(self, node=None): + self.beginResetModel() if node is None: node = macro.MacroNode() self._root = node - self.reset() + self.endResetModel() def flags(self, index): if index.column() == 0: @@ -123,7 +124,8 @@ def addRepeat(self, index, callReset=True): paramRepeatNode = self.nodeFromIndex(index) paramRepeatNode.addRepeat() if callReset: - self.reset() + self.beginResetModel() + self.endResetModel() def delRepeat(self, index, callReset=True): branchIndex = self.parent(index) @@ -131,7 +133,8 @@ def delRepeat(self, index, callReset=True): child = self.nodeFromIndex(index) branch.removeChild(child) if callReset: - self.reset() + self.beginResetModel() + self.endResetModel() def upRepeat(self, index, callReset=True): branchIndex = self.parent(index) @@ -139,7 +142,8 @@ def upRepeat(self, index, callReset=True): child = self.nodeFromIndex(index) branch.upChild(child) if callReset: - self.reset() + self.beginResetModel() + self.endResetModel() def downRepeat(self, index, callReset=True): branchIndex = self.parent(index) @@ -147,7 +151,8 @@ def downRepeat(self, index, callReset=True): child = self.nodeFromIndex(index) branch.downChild(child) if callReset: - self.reset() + self.beginResetModel() + self.endResetModel() def DuplicateRepeat(self, index, callReset=True): branchIndex = self.parent(index) @@ -155,7 +160,8 @@ def DuplicateRepeat(self, index, callReset=True): child = self.nodeFromIndex(index) branch.downChild(child) if callReset: - self.reset() + self.beginResetModel() + self.endResetModel() def rowCount(self, index): node = self.nodeFromIndex(index) @@ -168,31 +174,29 @@ def columnCount(self, parent): def data(self, index, role): if not index.isValid() or not (0 <= index.row() < self.rowCount(index.parent())): - return Qt.QVariant() + return None if role == Qt.Qt.DisplayRole: node = self.nodeFromIndex(index) if index.column() == 0: - return Qt.QVariant(node.name()) + return node.name() elif index.column() == 1: - return Qt.QVariant(str(node.value())) - - return Qt.QVariant() + return str(node.value()) + return None def setData(self, index, value, role=Qt.Qt.EditRole): node = self.nodeFromIndex(index) # if index.isValid() and 0 <= index.row() < len(node.parent()): if index.column() == 1: - node.setValue(Qt.from_qvariant(value, str)) - self.emit( - Qt.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) + node.setValue(value) + self.dataChanged.emit(index, index) return True return False def headerData(self, section, orientation, role): if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole: - return Qt.QVariant(self.headers[section]) - return Qt.QVariant() + return self.headers[section] + return None def index(self, row, column, parent): if not parent.isValid(): diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/parameditors.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/parameditors.py index a5cacde127..db1ed40724 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/parameditors.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroparameterseditor/parameditors.py @@ -29,7 +29,7 @@ import os -from taurus.external.qt import Qt +from taurus.external.qt import Qt, compat from taurus.qt.qtgui.input import TaurusAttrListComboBox from sardana.taurus.qt.qtgui.extra_macroexecutor import globals @@ -42,7 +42,7 @@ def str2bool(text): return text in ("True", "1") -class ParamBase: +class ParamBase(object): def __init__(self, paramModel=None): self.setParamModel(paramModel) @@ -66,12 +66,11 @@ def setIndex(self, index): self._index = index paramModel = index.model().nodeFromIndex(index) self.setParamModel(paramModel) - self.connect(self, Qt.SIGNAL("modelChanged()"), self.onModelChanged) self.setValue(paramModel.value()) def onModelChanged(self): model = self.index().model() - model.setData(self.index(), Qt.QVariant(self.getValue())) + model.setData(self.index(), self.getValue()) class ComboBoxBoolean(ParamBase, Qt.QComboBox): @@ -81,8 +80,6 @@ def __init__(self, parent=None, paramModel=None): ParamBase.__init__(self, paramModel) self.addItems(['True', 'False']) - self.connect(self, Qt.SIGNAL("currentIndexChanged(int)"), - self.onCurrentIndexChanged) def getValue(self): return str(self.currentText()) @@ -91,22 +88,16 @@ def setValue(self, value): currentIdx = self.currentIndex() idx = self.findText(value) if currentIdx == idx: - self.emit(Qt.SIGNAL("currentIndexChanged(int)"), - self.currentIndex()) + self.currentIndexChanged.emit(self.currentIndex()) else: self.setCurrentIndex(idx) - def onCurrentIndexChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) - class ComboBoxParam(ParamBase, Qt.QComboBox): def __init__(self, parent=None, paramModel=None): Qt.QComboBox.__init__(self, parent) ParamBase.__init__(self, paramModel) - self.connect(self, Qt.SIGNAL("currentIndexChanged(int)"), - self.onCurrentIndexChanged) def getValue(self): return str(self.currentText()) @@ -115,14 +106,10 @@ def setValue(self, value): currentIdx = self.currentIndex() idx = self.findText(value) if currentIdx == idx: - self.emit(Qt.SIGNAL("currentIndexChanged(int)"), - self.currentIndex()) + self.currentIndexChanged.emit(self.currentIndex()) else: self.setCurrentIndex(idx) - def onCurrentIndexChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) - class MSAttrListComboBoxParam(ParamBase, MSAttrListComboBox): @@ -131,8 +118,6 @@ def __init__(self, parent=None, paramModel=None): ParamBase.__init__(self, paramModel) # self.setUseParentModel(True) # self.setModel("/" + self.paramModel().type() + "List") - self.connect(self, Qt.SIGNAL("currentIndexChanged(int)"), - self.onCurrentIndexChanged) def getValue(self): return str(self.currentText()) @@ -140,9 +125,6 @@ def getValue(self): def setValue(self, value): self.setCurrentText(value) - def onCurrentIndexChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) - class AttrListComboBoxParam(ParamBase, TaurusAttrListComboBox): @@ -178,11 +160,6 @@ class LineEditParam(ParamBase, Qt.QLineEdit): def __init__(self, parent=None, paramModel=None): Qt.QLineEdit.__init__(self, parent) ParamBase.__init__(self, paramModel) - self.connect(self, Qt.SIGNAL( - "textChanged(const QString&)"), self.onTextChanged) - - def onTextChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) # def setDefaultValue(self): # defVal = self.paramModel().defValue() @@ -205,7 +182,6 @@ class CheckBoxParam(ParamBase, Qt.QCheckBox): def __init__(self, parent=None, paramModel=None): Qt.QCheckBox.__init__(self, parent) ParamBase.__init__(self, paramModel) - self.connect(self, Qt.SIGNAL("stateChanged(int)"), self.onStateChanged) def getValue(self): return str(self.isChecked()) @@ -213,9 +189,6 @@ def getValue(self): def setValue(self, value): self.setChecked(str2bool(value)) - def onStateChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) - class SpinBoxParam(ParamBase, Qt.QSpinBox): @@ -282,11 +255,10 @@ def __init__(self, parent=None, paramModel=None): self.text = "" - Qt.QObject.connect(self.button, Qt.SIGNAL( - "clicked()"), self._chooseAFile) + self.button.clicked.connect(self._chooseAFile) def _chooseAFile(self): - path = Qt.QFileDialog().getOpenFileName() + path, _ = compat.getOpenFileName() self.filePath.setText(path) def _readFileContent(self, path): @@ -325,12 +297,11 @@ def __init__(self, parent=None, paramModel=None): self.button.setText("...") self.layout.addWidget(self.button) - self.connect(self.button, Qt.SIGNAL("clicked()"), self.__chooseDirPath) - self.connect(self.dirPath, Qt.SIGNAL( - "textChanged(const QString&)"), self.onDirPathChanged) + self.button.clicked.connect(self.__chooseDirPath) + self.dirPath.textChanged.connect(self.onDirPathChanged) def onDirPathChanged(self): - self.emit(Qt.SIGNAL("modelChanged()")) + self.onModelChanged() def __chooseDirPath(self): path = Qt.QFileDialog().getExistingDirectory() diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/delegate.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/delegate.py index e6d9d46ac0..6880180b01 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/delegate.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/delegate.py @@ -45,7 +45,7 @@ def __init__(self, parent=None): def paint(self, painter, option, index): if index.column() == 2: macroNode = index.model().nodeFromIndex(index) - opts = Qt.QStyleOptionProgressBarV2() + opts = Qt.QStyleOptionProgressBar() opts.rect = option.rect range = macroNode.range() opts.minimum = range[0] @@ -54,7 +54,7 @@ def paint(self, painter, option, index): percent = macroNode.progress() opts.progress = percent # opts.text = Qt.QString('Unavailable' if percent == 0 else '%d%%'%percent) - opts.text = Qt.QString('%d%%' % percent) + opts.text = str('%d%%' % percent) # opts.text = Qt.QString(percent) Qt.QApplication.style().drawControl(Qt.QStyle.CE_ProgressBar, opts, painter) else: @@ -75,7 +75,7 @@ def setEditorData(self, editor, index): def setModelData(self, editor, model, index): if index.column() == 3: - model.setData(index, Qt.QVariant(editor.isChecked())) + model.setData(index, editor.isChecked()) class MacroParametersProxyDelegate(Qt.QItemDelegate): @@ -104,8 +104,7 @@ def createEditor(self, parent, option, index): def setEditorData(self, editor, index): if index.column() == 1: - text = Qt.from_qvariant(index.model().data( - index, Qt.Qt.DisplayRole), str) + text = index.model().data(index, Qt.Qt.DisplayRole) if text in ["None", "", None]: Qt.QItemDelegate.setEditorData(self, editor, index) else: @@ -131,13 +130,12 @@ def setEditorData(self, editor, index): def setModelData(self, editor, model, index): if index.column() == 1: - model.setData(index, Qt.QVariant(editor.getValue())) + model.setData(index, editor.getValue()) def sizeHint(self, option, index): if index.column() == 0: fm = option.fontMetrics - text = Qt.from_qvariant(index.model().data( - index, Qt.Qt.DisplayRole), str) + text = index.model().data(index, Qt.Qt.DisplayRole) document = Qt.QTextDocument() document.setDefaultFont(option.font) document.setHtml(text) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/model.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/model.py index 04bf41b03f..e2f40456c0 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/model.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/model.py @@ -46,8 +46,9 @@ def root(self): return self._root def setRoot(self, root): + self.beginResetModel() self._root = root - self.reset() + self.endResetModel() def clearSequence(self): self.setRoot(macro.SequenceNode()) @@ -169,49 +170,45 @@ def data(self, index, role): if role == Qt.Qt.DisplayRole: node = self.nodeFromIndex(index) if index.column() == 0: - return Qt.QVariant(node.name()) + return node.name() elif index.column() == 1: - return Qt.QVariant(str(node.value())) + return str(node.value()) elif index.column() == 2: if isinstance(node, macro.MacroNode): - return Qt.QVariant(node.progress()) + return node.progress() elif role == Qt.Qt.DecorationRole: node = self.nodeFromIndex(index) if index.column() == 3: if isinstance(node, macro.MacroNode): if node.isPause(): - return Qt.QVariant(Qt.QIcon(":/actions/media-playback-pause.svg")) - return Qt.QVariant() + return Qt.QIcon(":/actions/media-playback-pause.svg") + return None def setData(self, index, value, role=Qt.Qt.EditRole): node = self.nodeFromIndex(index) if index.column() == 1: if isinstance(node, macro.SingleParamNode): - node.setValue(Qt.from_qvariant(value, str)) - self.emit( - Qt.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) + node.setValue(value) + self.dataChanged.emit(index, index) while True: index = index.parent() node = self.nodeFromIndex(index) if isinstance(node, macro.MacroNode): - self.emit(Qt.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index.sibling( + self.dataChanged.emit(index, index.sibling( index.row(), self.columnCount(index) - 1)) break elif index.column() == 2: - progress = Qt.from_qvariant(value, float) - node.setProgress(progress) - self.emit( - Qt.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) + node.setProgress(value) + self.dataChanged.emit(index, index) elif index.column() == 3: - node.setPause(Qt.from_qvariant(value, bool)) - self.emit( - Qt.SIGNAL("dataChanged(QModelIndex,QModelIndex)"), index, index) + node.setPause(value) + self.dataChanged.emit(index, index) return True def headerData(self, section, orientation, role): if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole: - return Qt.QVariant(self.headers[section]) - return Qt.QVariant() + return self.headers[section] + return None def index(self, row, column, parent): assert self.root() is not None @@ -246,18 +243,20 @@ def toXmlString(self, pretty=False, withId=True): return xmlString def fromXmlString(self, xmlString): + self.beginResetModel() xmlElement = etree.fromstring(xmlString) newRoot = macro.SequenceNode(None) newRoot.fromXml(xmlElement) self.setRoot(newRoot) - self.reset() + self.endResetModel() return newRoot def fromPlainText(self, text): + self.beginResetModel() newRoot = macro.SequenceNode(None) newRoot.fromPlainText(text) self.setRoot(newRoot) - self.reset() + self.endResetModel() return newRoot def assignIds(self): @@ -393,8 +392,8 @@ def __getattr__(self, name): def headerData(self, section, orientation, role): if orientation == Qt.Qt.Horizontal and role == Qt.Qt.DisplayRole: - return Qt.QVariant(self.headers[section]) - return Qt.QVariant() + return self.headers[section] + return None def nodeFromIndex(self, index): sourceIndex = self.mapToSource(index) 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 0103b08f50..04b9deeea1 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py @@ -34,7 +34,7 @@ import PyTango from taurus import Device -from taurus.external.qt import Qt +from taurus.external.qt import Qt, compat from taurus.qt.qtgui.container import TaurusMainWindow, TaurusWidget from taurus.qt.qtcore.configuration import BaseConfigurableClass from taurus.qt.qtgui.display import TaurusLed @@ -67,7 +67,7 @@ def __init__(self, text, parent, macroNode): if text in self.macroNode().hookPlaces(): self.setChecked(True) self.setToolTip("This macro will be executed as a %s" % text) - self.connect(self, Qt.SIGNAL('toggled(bool)'), self.onToggle) + self.toggled.connect(self.onToggle) def macroNode(self): return self._macroNode @@ -84,6 +84,9 @@ def onToggle(self, trueFalse): class MacroSequenceTree(Qt.QTreeView, BaseConfigurableClass): + macroNameChanged = Qt.pyqtSignal('QString') + macroChanged = Qt.pyqtSignal(object) + def __init__(self, parent=None): Qt.QTreeView.__init__(self, parent) BaseConfigurableClass.__init__(self) @@ -102,34 +105,30 @@ def __init__(self, parent=None): self.deleteAction = Qt.QAction( getThemeIcon("list-remove"), "Remove macro", self) - self.connect(self.deleteAction, Qt.SIGNAL( - "triggered()"), self.deleteMacro) + self.deleteAction.triggered.connect(self.deleteMacro) self.deleteAction.setToolTip( "Clicking this button will remove current macro.") self.moveUpAction = Qt.QAction(getThemeIcon("go-up"), "Move up", self) - self.connect(self.moveUpAction, Qt.SIGNAL("triggered()"), self.upMacro) + self.moveUpAction.triggered.connect(self.upMacro) self.moveUpAction.setToolTip( "Clicking this button will move current macro up.") self.moveDownAction = Qt.QAction( getThemeIcon("go-down"), "Move down", self) - self.connect(self.moveDownAction, Qt.SIGNAL( - "triggered()"), self.downMacro) + self.moveDownAction.triggered.connect(self.downMacro) self.moveDownAction.setToolTip( "Clicking this button will move current macro down.") self.moveLeftAction = Qt.QAction( getThemeIcon("go-previous"), "Move left", self) - self.connect(self.moveLeftAction, Qt.SIGNAL( - "triggered()"), self.leftMacro) + self.moveLeftAction.triggered.connect(self.leftMacro) self.moveLeftAction.setToolTip( "Clicking this button will move current macro to the left.") self.moveRightAction = Qt.QAction( getThemeIcon("go-next"), "Move right", self) - self.connect(self.moveRightAction, Qt.SIGNAL( - "triggered()"), self.rightMacro) + self.moveRightAction.triggered.connect(self.rightMacro) self.moveRightAction.setToolTip( "Clicking this button will move current macro to the right.") @@ -181,8 +180,8 @@ def selectionChanged(self, selected, deselected): self.moveLeftAction.setEnabled(node.isAllowedMoveLeft()) self.moveRightAction.setEnabled(node.isAllowedMoveRight()) sourceIndex = self.model().mapToSource(proxyIndex) - self.emit(Qt.SIGNAL("macroChanged"), sourceIndex) - self.emit(Qt.SIGNAL("macroNameChanged"), macroName) + self.macroChanged.emit(sourceIndex) + self.macroNameChanged.emit(macroName) def expanded(self): for column in range(self.model().columnCount(Qt.QModelIndex())): @@ -210,8 +209,9 @@ def root(self): return self.model().root() def setRoot(self, root): + self.model().beginResetModel() self.model().setRoot(root) - self.model().reset() + self.model().endResetModel() def addMacro(self, macroNode): node, proxyIndex = self.selectedNodeAndIndex() @@ -296,7 +296,7 @@ def setProgressForMacro(self, macroId, progress): return progressIndex = persistentIndex.sibling(persistentIndex.row(), 2) index = Qt.QModelIndex(progressIndex) - self.model().setData(index, Qt.QVariant(progress)) + self.model().setData(index, progress) def setRangeForMacro(self, macroId, range): persistentIndex = self._idIndexDict.get(macroId, None) @@ -324,6 +324,13 @@ def dropEvent(self, event): class TaurusSequencerWidget(TaurusWidget): + macroStarted = Qt.pyqtSignal('QString') + plotablesFilterChanged = Qt.pyqtSignal(object) + currentMacroChanged = Qt.pyqtSignal(object) + macroNameChanged = Qt.pyqtSignal('QString') + shortMessageEmitted = Qt.pyqtSignal('QString') + sequenceEmpty = Qt.pyqtSignal() + def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) # list representing all macros ids (all from sequence) currently @@ -357,8 +364,7 @@ def __init__(self, parent=None, designMode=False): actionsLayout.setContentsMargins(0, 0, 0, 0) self.newSequenceAction = Qt.QAction( getThemeIcon("document-new"), "New", self) - self.connect(self.newSequenceAction, Qt.SIGNAL( - "triggered()"), self.onNewSequence) + self.newSequenceAction.triggered.connect(self.onNewSequence) self.newSequenceAction.setToolTip("New sequence") self.newSequenceAction.setEnabled(False) newSequenceButton = Qt.QToolButton() @@ -367,8 +373,7 @@ def __init__(self, parent=None, designMode=False): self.openSequenceAction = Qt.QAction( getThemeIcon("document-open"), "Open...", self) - self.connect(self.openSequenceAction, Qt.SIGNAL( - "triggered()"), self.onOpenSequence) + self.openSequenceAction.triggered.connect(self.onOpenSequence) self.openSequenceAction.setToolTip("Open sequence...") openSequenceButton = Qt.QToolButton() openSequenceButton.setDefaultAction(self.openSequenceAction) @@ -376,8 +381,7 @@ def __init__(self, parent=None, designMode=False): self.saveSequenceAction = Qt.QAction( getThemeIcon("document-save"), "Save...", self) - self.connect(self.saveSequenceAction, Qt.SIGNAL( - "triggered()"), self.onSaveSequence) + self.saveSequenceAction.triggered.connect(self.onSaveSequence) self.saveSequenceAction.setToolTip("Save sequence...") self.saveSequenceAction.setEnabled(False) saveSequenceButton = Qt.QToolButton() @@ -386,8 +390,7 @@ def __init__(self, parent=None, designMode=False): self.stopSequenceAction = Qt.QAction( getIcon(":/actions/media_playback_stop.svg"), "Stop", self) - self.connect(self.stopSequenceAction, Qt.SIGNAL( - "triggered()"), self.onStopSequence) + self.stopSequenceAction.triggered.connect(self.onStopSequence) self.stopSequenceAction.setToolTip("Stop sequence") stopSequenceButton = Qt.QToolButton() stopSequenceButton.setDefaultAction(self.stopSequenceAction) @@ -395,8 +398,7 @@ def __init__(self, parent=None, designMode=False): self.pauseSequenceAction = Qt.QAction( getIcon(":/actions/media_playback_pause.svg"), "Pause", self) - self.connect(self.pauseSequenceAction, Qt.SIGNAL( - "triggered()"), self.onPauseSequence) + self.pauseSequenceAction.triggered.connect(self.onPauseSequence) self.pauseSequenceAction.setToolTip("Pause sequence") pauseSequenceButton = Qt.QToolButton() pauseSequenceButton.setDefaultAction(self.pauseSequenceAction) @@ -404,8 +406,7 @@ def __init__(self, parent=None, designMode=False): self.playSequenceAction = Qt.QAction( getIcon(":/actions/media_playback_start.svg"), "Play", self) - self.connect(self.playSequenceAction, Qt.SIGNAL( - "triggered()"), self.onPlaySequence) + self.playSequenceAction.triggered.connect(self.onPlaySequence) self.playSequenceAction.setToolTip("Play sequence") playSequenceButton = Qt.QToolButton() playSequenceButton.setDefaultAction(self.playSequenceAction) @@ -418,8 +419,7 @@ def __init__(self, parent=None, designMode=False): # sequence tree view indicating clearing of the plot after execution self.fullSequencePlotCheckBox = Qt.QCheckBox( "Full sequence plot", self) - self.connect(self.fullSequencePlotCheckBox, Qt.SIGNAL( - "toggled(bool)"), self.setFullSequencePlot) + self.fullSequencePlotCheckBox.toggled.connect(self.setFullSequencePlot) self.fullSequencePlotCheckBox.setChecked(True) actionsLayout.addWidget(self.fullSequencePlotCheckBox) @@ -442,9 +442,7 @@ def __init__(self, parent=None, designMode=False): self.addMacroAction = Qt.QAction( getThemeIcon("list-add"), "Add macro...", self) - self.connect(self.addMacroAction, - Qt.SIGNAL("triggered()"), - self.onAdd) + self.addMacroAction.triggered.connect(self.onAdd) self.addMacroAction.setToolTip( "Clicking this button will add selected macro") self.addMacroAction.setEnabled(False) @@ -497,10 +495,9 @@ def __init__(self, parent=None, designMode=False): self.stackedWidget.addWidget(self.standardMacroParametersEditor) self.customMacroParametersEditor = None - self.connect(self.macroComboBox, Qt.SIGNAL( - "currentIndexChanged(QString)"), self.onMacroComboBoxChanged) - self.connect(self.tree, Qt.SIGNAL("macroChanged"), - self.setMacroParametersRootIndex) + self.macroComboBox.currentIndexChanged.connect( + self.onMacroComboBoxChanged) + self.tree.macroChanged.connect(self.setMacroParametersRootIndex) def contextMenuEvent(self, event): menu = Qt.QMenu() @@ -584,7 +581,7 @@ def onNewSequence(self): self.tree.clearTree() self.newSequenceAction.setEnabled(False) self.saveSequenceAction.setEnabled(False) - self.emit(Qt.SIGNAL("currentMacroChanged"), None) + self.currentMacroChanged.emit(None) def loadFile(self, fileName): if fileName == "": @@ -628,7 +625,7 @@ def loadFile(self, fileName): file.close() self.setSequencesPath(str.join("/", fileName.rsplit("/")[:-1])) - self.emit(Qt.SIGNAL("currentMacroChanged"), None) + self.currentMacroChanged.emit(None) def onOpenSequence(self): if not self._sequenceModel.isEmpty(): @@ -642,11 +639,11 @@ def onOpenSequence(self): self.tree.clearTree() sequencesPath = self.sequencesPath() - fileName = str(Qt.QFileDialog.getOpenFileName( + fileName, _ = compat.getOpenFileName( self, "Choose a sequence to open...", sequencesPath, - "*")) + "*") self.loadFile(fileName) @@ -656,18 +653,18 @@ def onSaveSequence(self): sequencesPath = str(Qt.QDir.homePath()) sequencesPath = os.path.join(sequencesPath, "Untitled.xml") - fileName = str(Qt.QFileDialog.getSaveFileName( + fileName, _ = compat.getSaveFileName( self, "Choose a sequence file name...", sequencesPath, - "*.xml")) + "*.xml") if fileName == "": return try: file = open(fileName, "w") file.write(self.tree.toXmlString(pretty=True, withId=False)) self.setSequencesPath(str.join("/", fileName.rsplit("/")[:-1])) - except Exception, e: + except Exception as e: Qt.QMessageBox.warning( self, "Error while saving macros sequence", @@ -754,18 +751,17 @@ def onMacroStatusUpdated(self, data): #@todo: Check this signal because it doesn't work, # emitExecutionStarted is not set!!! if self.emitExecutionStarted(): - self.emit(Qt.SIGNAL("macroStarted"), "DoorOutput") + self.macroStarted.emit("DoorOutput") self.tree.setRangeForMacro(id, range) self.playSequenceAction.setEnabled(False) self.pauseSequenceAction.setEnabled(True) self.stopSequenceAction.setEnabled(True) if id == self.firstMacroId(): - self.emit(Qt.SIGNAL("plotablesFilterChanged"), None) - self.emit(Qt.SIGNAL("plotablesFilterChanged"), - standardPlotablesFilter) + self.plotablesFilterChanged.emit(None) + self.plotablesFilterChanged.emit(standardPlotablesFilter) shortMessage = "Sequence started." elif not self.isFullSequencePlot(): - self.emit(Qt.SIGNAL("plotablesFilterChanged"), None) + self.plotablesFilterChanged.emit(None) shortMessage += " Macro %s started." % macroName elif state == "pause": self.playSequenceAction.setText("Resume sequence") @@ -805,7 +801,7 @@ def onMacroStatusUpdated(self, data): elif state == "step": shortMessage = "Macro %s at %d %% of progress." % (macroName, step) - self.emit(Qt.SIGNAL("shortMessageEmitted"), shortMessage) + self.shortMessageEmitted.emit(shortMessage) self.tree.setProgressForMacro(id, step) def onDoorChanged(self, doorName): @@ -863,7 +859,7 @@ def onMacroComboBoxChanged(self): self.addMacroAction.setEnabled(False) else: self.addMacroAction.setEnabled(True) - self.emit(Qt.SIGNAL("macroNameChanged"), macroName) + self.macroNameChanged.emit(macroName) def onAdd(self): macroName = str(self.macroComboBox.currentText()) @@ -881,8 +877,8 @@ def isMacroSelected(self): def emptySequence(self): self.tree.clearTree() self.disableButtons() - self.emit(Qt.SIGNAL("currentMacroChanged"), None) - self.emit(Qt.SIGNAL("sequenceEmpty")) + self.currentMacroChanged.emit(None) + self.sequenceEmpty.emit() def fromXmlString(self, xmlString): newRoot = self.tree.fromXmlString(xmlString) @@ -900,7 +896,7 @@ def fromPlainText(self, plainText): try: macroServerObj.recreateMacroNodeAndFillAdditionalInfos( macroNode) - except Exception, e: + except Exception as e: Qt.QMessageBox.warning(self, "Error while parsing the sequence", "Sequence line number %d contains " @@ -913,12 +909,11 @@ def fromPlainText(self, plainText): def setModel(self, model): oldModelObj = self.getModelObj() if oldModelObj is not None: - self.disconnect(oldModelObj, Qt.SIGNAL( - "macrosUpdated"), self.macroComboBox.onMacrosUpdated) + oldModelObj.macrosUpdated.disconnect( + self.macroComboBox.onMacrosUpdated) TaurusWidget.setModel(self, model) newModelObj = self.getModelObj() - self.connect(newModelObj, Qt.SIGNAL("macrosUpdated"), - self.macroComboBox.onMacrosUpdated) + newModelObj.macrosUpdated.connect(self.macroComboBox.onMacrosUpdated) @classmethod def getQtDesignerPluginInfo(cls): @@ -929,6 +924,7 @@ def getQtDesignerPluginInfo(cls): class TaurusSequencer(MacroExecutionWindow): + doorChanged = Qt.pyqtSignal('QString') def __init__(self, parent=None, designMode=False): MacroExecutionWindow.__init__(self) @@ -940,8 +936,8 @@ def initComponents(self): self.taurusSequencerWidget.setUseParentModel(True) self.registerConfigDelegate(self.taurusSequencerWidget) self.setCentralWidget(self.taurusSequencerWidget) - self.connect(self.taurusSequencerWidget, Qt.SIGNAL( - 'shortMessageEmitted'), self.onShortMessage) + self.taurusSequencerWidget.shortMessageEmitted.connect( + self.onShortMessage) self.statusBar().showMessage("Sequencer ready") def setCustomMacroEditorPaths(self, customMacroEditorPaths): @@ -952,23 +948,20 @@ def setCustomMacroEditorPaths(self, customMacroEditorPaths): def loadSettings(self): TaurusMainWindow.loadSettings(self) - self.emit(Qt.SIGNAL("doorChanged"), self.doorName()) + self.doorChanged.emit(self.doorName()) def onDoorChanged(self, doorName): MacroExecutionWindow.onDoorChanged(self, doorName) if self._qDoor: - Qt.QObject.disconnect( - self._qDoor, - Qt.SIGNAL("macroStatusUpdated"), + self._qDoor.macroStatusUpdated.disconnect( self.taurusSequencerWidget.onMacroStatusUpdated) self._qDoor = None if doorName == "": return self._qDoor = Device(doorName) - Qt.QObject.connect(self._qDoor, - Qt.SIGNAL("macroStatusUpdated"), - self.taurusSequencerWidget.onMacroStatusUpdated) + self._qDoor.macroStatusUpdated.connect( + self.taurusSequencerWidget.onMacroStatusUpdated) self.taurusSequencerWidget.onDoorChanged(doorName) @classmethod @@ -979,22 +972,21 @@ def getQtDesignerPluginInfo(cls): def createSequencerWidget(args): sequencer = TaurusSequencerWidget() sequencer.setModelInConfig(True) - Qt.QObject.connect(sequencer, Qt.SIGNAL( - "doorChanged"), sequencer.onDoorChanged) + sequencer.doorChanged.connect(sequencer.onDoorChanged) + if len(args) == 2: sequencer.setModel(args[0]) - sequencer.emit(Qt.SIGNAL('doorChanged'), args[1]) + sequencer.doorChanged.emit(args[1]) return sequencer def createSequencer(args, options): sequencer = TaurusSequencer() sequencer.setModelInConfig(True) - Qt.QObject.connect(sequencer, Qt.SIGNAL( - "doorChanged"), sequencer.onDoorChanged) + sequencer.doorChanged.connect(sequencer.onDoorChanged) if len(args) == 2: sequencer.setModel(args[0]) - sequencer.emit(Qt.SIGNAL('doorChanged'), args[1]) + sequencer.doorChanged.emit(args[1]) sequencer.loadSettings() if options.file is not None: sequencer.taurusSequencerWidget.loadFile(options.file) diff --git a/src/sardana/taurus/qt/qtgui/extra_pool/motor.py b/src/sardana/taurus/qt/qtgui/extra_pool/motor.py index 3ee2c21a21..01ff41587f 100644 --- a/src/sardana/taurus/qt/qtgui/extra_pool/motor.py +++ b/src/sardana/taurus/qt/qtgui/extra_pool/motor.py @@ -53,8 +53,7 @@ def __init__(self, parent=None, designMode=False): self.call__init__(TaurusBaseWidget, str( self.objectName()), designMode=designMode) self.loadUi() - Qt.QObject.connect(self.ui.config, Qt.SIGNAL( - "clicked()"), self.configureMotor) + self.ui.config.clicked.connect(self.configureMotor) def sizeHint(self): return Qt.QSize(330, 50) @@ -74,27 +73,27 @@ def getQtDesignerPluginInfo(cls): #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT properties #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - @Qt.pyqtSignature("getModel()") + @Qt.pyqtSlot() def getModel(self): return self.ui.TaurusGroupBox.getModel() - @Qt.pyqtSignature("setModel(QString)") + @Qt.pyqtSlot("QString") def setModel(self, model): self.ui.TaurusGroupBox.setModel(model) - @Qt.pyqtSignature("resetModel()") + @Qt.pyqtSlot() def resetModel(self): self.ui.TaurusGroupBox.resetModel() - @Qt.pyqtSignature("getShowText()") + @Qt.pyqtSlot() def getShowText(self): return self.ui.TaurusGroupBox.getShowText() - @Qt.pyqtSignature("setShowText(bool)") + @Qt.pyqtSlot(bool) def setShowText(self, showText): self.ui.TaurusGroupBox.setShowText(showText) - @Qt.pyqtSignature("resetShowText()") + @Qt.pyqtSlot() def resetShowText(self): self.ui.TaurusGroupBox.resetShowText() @@ -112,8 +111,7 @@ def __init__(self, parent=None, designMode=False): self.call__init__(TaurusBaseWidget, str( self.objectName()), designMode=designMode) self.loadUi() - Qt.QObject.connect(self.ui.config, Qt.SIGNAL( - "clicked()"), self.configureMotor) + self.ui.config.clicked.connect(self.configureMotor) def sizeHint(self): return Qt.QSize(215, 85) @@ -133,27 +131,27 @@ def getQtDesignerPluginInfo(cls): #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT properties #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - @Qt.pyqtSignature("getModel()") + @Qt.pyqtSlot() def getModel(self): return self.ui.TaurusGroupBox.getModel() - @Qt.pyqtSignature("setModel(QString)") + @Qt.pyqtSlot("QString") def setModel(self, model): self.ui.TaurusGroupBox.setModel(model) - @Qt.pyqtSignature("resetModel()") + @Qt.pyqtSlot() def resetModel(self): self.ui.TaurusGroupBox.resetModel() - @Qt.pyqtSignature("getShowText()") + @Qt.pyqtSlot() def getShowText(self): return self.ui.TaurusGroupBox.getShowText() - @Qt.pyqtSignature("setShowText(bool)") + @Qt.pyqtSlot(bool) def setShowText(self, showText): self.ui.TaurusGroupBox.setShowText(showText) - @Qt.pyqtSignature("resetShowText()") + @Qt.pyqtSlot() def resetShowText(self): self.ui.TaurusGroupBox.resetShowText() @@ -171,8 +169,7 @@ def __init__(self, parent=None, designMode=False): self.call__init__(TaurusBaseWidget, str( self.objectName()), designMode=designMode) self.loadUi() - Qt.QObject.connect(self.ui.config, Qt.SIGNAL( - "clicked()"), self.configureMotor) + self.ui.config.clicked.connect(self.configureMotor) def sizeHint(self): return Qt.QSize(120, 145) @@ -192,27 +189,27 @@ def getQtDesignerPluginInfo(cls): #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT properties #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - @Qt.pyqtSignature("getModel()") + @Qt.pyqtSlot() def getModel(self): return self.ui.TaurusGroupBox.getModel() - @Qt.pyqtSignature("setModel(QString)") + @Qt.pyqtSlot("QString") def setModel(self, model): self.ui.TaurusGroupBox.setModel(model) - @Qt.pyqtSignature("resetModel()") + @Qt.pyqtSlot() def resetModel(self): self.ui.TaurusGroupBox.resetModel() - @Qt.pyqtSignature("getShowText()") + @Qt.pyqtSlot() def getShowText(self): return self.ui.TaurusGroupBox.getShowText() - @Qt.pyqtSignature("setShowText(bool)") + @Qt.pyqtSlot(bool) def setShowText(self, showText): self.ui.TaurusGroupBox.setShowText(showText) - @Qt.pyqtSignature("resetShowText()") + @Qt.pyqtSlot() def resetShowText(self): self.ui.TaurusGroupBox.resetShowText() @@ -246,27 +243,27 @@ def getQtDesignerPluginInfo(cls): #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT properties #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - @Qt.pyqtSignature("getModel()") + @Qt.pyqtSlot() def getModel(self): return self.ui.TaurusGroupBox.getModel() - @Qt.pyqtSignature("setModel(QString)") + @Qt.pyqtSlot("QString") def setModel(self, model): self.ui.TaurusGroupBox.setModel(model) - @Qt.pyqtSignature("resetModel()") + @Qt.pyqtSlot() def resetModel(self): self.ui.TaurusGroupBox.resetModel() - @Qt.pyqtSignature("getShowText()") + @Qt.pyqtSlot() def getShowText(self): return self.ui.TaurusGroupBox.getShowText() - @Qt.pyqtSignature("setShowText(bool)") + @Qt.pyqtSlot(bool) def setShowText(self, showText): self.ui.TaurusGroupBox.setShowText(showText) - @Qt.pyqtSignature("resetShowText()") + @Qt.pyqtSlot() def resetShowText(self): self.ui.TaurusGroupBox.resetShowText() diff --git a/src/sardana/taurus/qt/qtgui/extra_pool/poolchannel.py b/src/sardana/taurus/qt/qtgui/extra_pool/poolchannel.py index 626cee2972..1fb5d01af4 100644 --- a/src/sardana/taurus/qt/qtgui/extra_pool/poolchannel.py +++ b/src/sardana/taurus/qt/qtgui/extra_pool/poolchannel.py @@ -115,8 +115,7 @@ def __init__(self, parent=None, designMode=False): self._devButton.setText('') self.layout().addWidget(self._devButton) - self.connect(self, Qt.SIGNAL( - 'modelChanged(const QString &)'), self._updateTaurusValue) + self.modelChanged.connect(self._updateTaurusValue) def _updateTaurusValue(self): m = self.getModelName() diff --git a/src/sardana/taurus/qt/qtgui/extra_pool/poolioregister.py b/src/sardana/taurus/qt/qtgui/extra_pool/poolioregister.py index 4cce82b180..5a95b4d6d6 100644 --- a/src/sardana/taurus/qt/qtgui/extra_pool/poolioregister.py +++ b/src/sardana/taurus/qt/qtgui/extra_pool/poolioregister.py @@ -248,7 +248,7 @@ def setModel(self, model): # Empty previous buttons # self.ui.lo_buttons_write. for button in self.button_value_dict.keys(): - self.disconnect(button, Qt.SIGNAL('clicked'), self.writeValue) + self.button.clicked.disconnect(self.writeValue) button.deleteLater() self.button_value_dict = {} @@ -260,7 +260,7 @@ def setModel(self, model): button = Qt.QPushButton(label) self.button_value_dict[button] = value self.ui.lo_buttons_write.addWidget(button) - self.connect(button, Qt.SIGNAL('clicked()'), self.writeValue) + self.button.clicked.connect(self.writeValue) def writeValue(self): if self.ioreg_dev is None: diff --git a/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py b/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py index 1cf64a959f..df4d4d36c8 100644 --- a/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py +++ b/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py @@ -64,6 +64,8 @@ class LimitsListener(Qt.QObject): can do whatever with it. """ + updateLimits = Qt.pyqtSignal(object) + def __init__(self): Qt.QObject.__init__(self) @@ -71,7 +73,7 @@ def eventReceived(self, evt_src, evt_type, evt_value): if evt_type not in [TaurusEventType.Change, TaurusEventType.Periodic]: return limits = evt_value.value - self.emit(Qt.SIGNAL('updateLimits(PyQt_PyObject)'), limits.tolist()) + self.updateLimits.emit(limits.tolist()) class PoolMotorClient(): @@ -103,6 +105,7 @@ def moveMotor(self, pos): def moveInc(self, inc): self.moveMotor(self.motor_dev['position'].value + inc) + @Qt.pyqtSlot() def jogNeg(self): neg_limit = -((self.maxint_in_32_bits / 2) - 1) # THERE IS A BUG IN THE ICEPAP THAT DOES NOT ALLOW MOVE ABSOLUTE FURTHER THAN 32 BIT @@ -117,6 +120,7 @@ def jogNeg(self): pass self.moveMotor(neg_limit) + @Qt.pyqtSlot() def jogPos(self): pos_limit = (self.maxint_in_32_bits / 2) - 1 # THERE IS A BUG IN THE ICEPAP THAT DOES NOT ALLOW MOVE ABSOLUTE FURTHER THAN 32 BIT @@ -131,9 +135,11 @@ def jogPos(self): pass self.moveMotor(pos_limit) + @Qt.pyqtSlot() def goHome(self): pass + @Qt.pyqtSlot() def abort(self): self.motor_dev.abort() @@ -142,18 +148,18 @@ class LabelWidgetDragsDeviceAndAttribute(DefaultLabelWidget): """ Offer richer mime data with taurus-device, taurus-attribute, and plain-text. """ def mouseMoveEvent(self, event): - model = self.taurusValueBuddy().getModelName() + model = self.taurusValueBuddy().getModelName().encode('utf-8') mimeData = Qt.QMimeData() mimeData.setText(self.text()) attr_name = model - dev_name = model.rpartition('/')[0] + dev_name = model.rpartition(b'/')[0] mimeData.setData(TAURUS_DEV_MIME_TYPE, dev_name) mimeData.setData(TAURUS_ATTR_MIME_TYPE, attr_name) drag = Qt.QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) - drag.start(Qt.Qt.CopyAction) + drag.exec_(Qt.Qt.CopyAction, Qt.Qt.CopyAction) class PoolMotorConfigurationForm(TaurusAttrForm): @@ -382,32 +388,24 @@ def just_ctrl_status_line(evt_src, evt_type, evt_value): self.__setTaurusIcons() self.ui.motorGroupBox.setContextMenuPolicy(Qt.Qt.CustomContextMenu) - self.connect(self.ui.motorGroupBox, Qt.SIGNAL( - 'customContextMenuRequested(QPoint)'), self.buildContextMenu) - - self.connect(self.ui.btnGoToNeg, Qt.SIGNAL('clicked()'), self.jogNeg) - self.connect(self.ui.btnGoToNegPress, - Qt.SIGNAL('pressed()'), self.jogNeg) - self.connect(self.ui.btnGoToNegPress, - Qt.SIGNAL('released()'), self.abort) - self.connect(self.ui.btnGoToNegInc, Qt.SIGNAL( - 'clicked()'), self.goToNegInc) - self.connect(self.ui.btnGoToPos, Qt.SIGNAL('clicked()'), self.jogPos) - self.connect(self.ui.btnGoToPosPress, - Qt.SIGNAL('pressed()'), self.jogPos) - self.connect(self.ui.btnGoToPosPress, - Qt.SIGNAL('released()'), self.abort) - self.connect(self.ui.btnGoToPosInc, Qt.SIGNAL( - 'clicked()'), self.goToPosInc) - - self.connect(self.ui.btnHome, Qt.SIGNAL('clicked()'), self.goHome) - self.connect(self.ui.btnStop, Qt.SIGNAL('clicked()'), self.abort) + + self.ui.motorGroupBox.customContextMenuRequested.connect( + self.buildContextMenu) + self.ui.btnGoToNeg.clicked.connect(self.jogNeg) + self.ui.btnGoToNegPress.pressed.connect(self.jogNeg) + self.ui.btnGoToNegPress.released.connect(self.abort) + self.ui.btnGoToNegInc.clicked.connect(self.goToNegInc) + self.ui.btnGoToPos.clicked.connect(self.jogPos) + self.ui.btnGoToPosPress.pressed.connect(self.jogPos) + self.ui.btnGoToPosPress.released.connect(self.abort) + self.ui.btnGoToPosInc.clicked.connect(self.goToPosInc) + + self.ui.btnHome.clicked.connect(self.goHome) + self.ui.btnStop.clicked.connect(self.abort) # ALSO UPDATE THE WIDGETS EVERYTIME THE FORM HAS TO BE SHOWN - self.connect(self.ui.btnCfg, Qt.SIGNAL('clicked()'), - taurus_attr_form._updateAttrWidgets) - self.connect(self.ui.btnCfg, Qt.SIGNAL('clicked()'), - self.buildBetterCfgDialogTitle) + self.ui.btnCfg.clicked.connect(taurus_attr_form._updateAttrWidgets) + self.ui.btnCfg.clicked.connect(self.buildBetterCfgDialogTitle) ####################################################################### ######################################## @@ -491,9 +489,11 @@ def updateLimits(self, limits): # def sizeHint(self): # return Qt.QSize(300,30) + @Qt.pyqtSlot() def goToNegInc(self): self.moveInc(-1 * self.ui.inc.value()) + @Qt.pyqtSlot() def goToPosInc(self): self.moveInc(self.ui.inc.value()) @@ -566,28 +566,17 @@ def buildContextMenu(self, point): action_status.setChecked(self.ui.lblStatus.isVisible()) menu.addAction(action_status) - self.connect(action_hide_all, Qt.SIGNAL( - 'triggered()'), self.toggleHideAll) - self.connect(action_show_all, Qt.SIGNAL( - 'triggered()'), self.toggleShowAll) - self.connect(action_move_absolute, Qt.SIGNAL( - 'toggled(bool)'), self.toggleMoveAbsolute) - self.connect(action_move_relative, Qt.SIGNAL( - 'toggled(bool)'), self.toggleMoveRelative) - self.connect(action_move_continuous, Qt.SIGNAL( - 'toggled(bool)'), self.toggleMoveContinuous) - self.connect(action_move_to_limits, Qt.SIGNAL( - 'toggled(bool)'), self.toggleMoveToLimits) - self.connect(action_encoder, Qt.SIGNAL( - 'toggled(bool)'), self.toggleEncoder) - self.connect(action_stop_move, Qt.SIGNAL( - 'toggled(bool)'), self.toggleStopMove) - self.connect(action_homing, Qt.SIGNAL( - 'toggled(bool)'), self.toggleHoming) - self.connect(action_config, Qt.SIGNAL( - 'toggled(bool)'), self.toggleConfig) - self.connect(action_status, Qt.SIGNAL( - 'toggled(bool)'), self.toggleStatus) + action_hide_all.triggered.connect(self.toggleHideAll) + action_show_all.triggered.connect(self.toggleShowAll) + action_move_absolute.toggled.connect(self.toggleMoveAbsolute) + action_move_relative.toggled.connect(self.toggleMoveRelative) + action_move_continuous.toggled.connect(self.toggleMoveContinuous) + action_move_to_limits.toggled.connect(self.toggleMoveToLimits) + action_encoder.toggled.connect(self.toggleEncoder) + action_stop_move.toggled.connect(self.toggleStopMove) + action_homing.toggled.connect(self.toggleHoming) + action_config.toggled.connect(self.toggleConfig) + action_status.toggled.connect(self.toggleStatus) menu.popup(self.cursor().pos()) @@ -692,11 +681,11 @@ def hideEvent(self, event): #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- # QT properties #-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~- - @Qt.pyqtSignature("getModel()") + @Qt.pyqtSlot() def getModel(self): return self.ui.motorGroupBox.getModel() - @Qt.pyqtSignature("setModel(QString)") + @Qt.pyqtSlot("QString") def setModel(self, model): # DUE TO A BUG IN TAUGROUPBOX, WE NEED THE FULL MODEL NAME try: @@ -748,8 +737,8 @@ def setModel(self, model): # CONFIGURE A LISTENER IN ORDER TO UPDATE LIMIT SWITCHES STATES self.limits_listener = LimitsListener() - self.connect(self.limits_listener, Qt.SIGNAL( - 'updateLimits(PyQt_PyObject)'), self.updateLimits) + self.limits_listener.updateLimits.connect( + self.updateLimits) limits_visible = False if self.has_limits: limits_attribute = self.motor_dev.getAttribute( @@ -765,43 +754,43 @@ def setModel(self, model): (model, repr(e))) self.traceback() - @Qt.pyqtSignature("resetModel()") + @Qt.pyqtSlot() def resetModel(self): self.ui.motorGroupBox.resetModel() - @Qt.pyqtSignature("getShowContextMenu()") + @Qt.pyqtSlot() def getShowContextMenu(self): return self.show_context_menu - @Qt.pyqtSignature("setShowContextMenu(bool)") + @Qt.pyqtSlot() def setShowContextMenu(self, showContextMenu): self.show_context_menu = showContextMenu - @Qt.pyqtSignature("resetShowContextMenu()") + @Qt.pyqtSlot() def resetShowContextMenu(self): self.show_context_menu = True - @Qt.pyqtSignature("getStepSize()") + @Qt.pyqtSlot() def getStepSize(self): return self.ui.inc.value() - @Qt.pyqtSignature("setStepSize(double)") + @Qt.pyqtSlot(float) def setStepSize(self, stepSize): self.ui.inc.setValue(stepSize) - @Qt.pyqtSignature("resetStepSize()") + @Qt.pyqtSlot() def resetStepSize(self): self.setStepSize(1) - @Qt.pyqtSignature("getStepSizeIncrement()") + @Qt.pyqtSlot() def getStepSizeIncrement(self): return self.ui.inc.singleStep() - @Qt.pyqtSignature("setStepSizeIncrement(double)") + @Qt.pyqtSlot(float) def setStepSizeIncrement(self, stepSizeIncrement): self.ui.inc.setSingleStep(stepSizeIncrement) - @Qt.pyqtSignature("resetStepSizeIncrement()") + @Qt.pyqtSlot() def resetStepSizeIncrement(self): self.setStepSizeIncrement(1) @@ -823,6 +812,8 @@ class TaurusAttributeListener(Qt.QObject): If that is the case it emits a signal with the event's value. """ + eventReceivedSignal = Qt.pyqtSignal(object) + def __init__(self): Qt.QObject.__init__(self) @@ -830,7 +821,7 @@ def eventReceived(self, evt_src, evt_type, evt_value): if evt_type not in [TaurusEventType.Change, TaurusEventType.Periodic]: return value = evt_value.value - self.emit(Qt.SIGNAL('eventReceived'), value) + self.eventReceivedSignal.emit(value) ################################################## @@ -851,7 +842,7 @@ class PoolMotorTVLabelWidget(TaurusWidget): def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) self.setLayout(Qt.QGridLayout()) - self.layout().setMargin(0) + self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) self.lbl_alias = DefaultLabelWidget(parent, designMode) @@ -885,6 +876,7 @@ def setExpertView(self, expertView): btn_poweron_visible = expertView and self.taurusValueBuddy().hasPowerOn() self.btn_poweron.setVisible(btn_poweron_visible) + @Qt.pyqtSlot() @ProtectTaurusMessageBox(msg='An error occurred trying to write PowerOn Attribute.') def setPowerOn(self): motor_dev = self.taurusValueBuddy().motor_dev @@ -894,10 +886,15 @@ def setPowerOn(self): def setModel(self, model): # Handle User/Expert view - self.disconnect(self.taurusValueBuddy(), Qt.SIGNAL( - 'expertViewChanged(bool)'), self.setExpertView) - self.disconnect(self.btn_poweron, Qt.SIGNAL( - 'clicked()'), self.setPowerOn) + try: + self.taurusValueBuddy().expertViewChanged.disconnect( + self.setExpertView) + except TypeError: + pass + try: + self.btn_poweron.clicked.disconnect(self.setPowerOn) + except TypeError: + pass if model in (None, ''): self.lbl_alias.setModel(model) TaurusWidget.setModel(self, model) @@ -905,10 +902,10 @@ def setModel(self, model): self.lbl_alias.taurusValueBuddy = self.taurusValueBuddy self.lbl_alias.setModel(model) TaurusWidget.setModel(self, model + '/Status') - self.connect(self.taurusValueBuddy(), Qt.SIGNAL( - 'expertViewChanged(bool)'), self.setExpertView) + self.taurusValueBuddy().expertViewChanged.connect( + self.setExpertView) # Handle Power ON/OFF - self.connect(self.btn_poweron, Qt.SIGNAL('clicked()'), self.setPowerOn) + self.btn_poweron.clicked.connect(self.setPowerOn) self.setExpertView(self.taurusValueBuddy()._expertView) def calculateExtendedTooltip(self, cache=False): @@ -935,22 +932,21 @@ def contextMenuEvent(self, event): action_expert_view.setCheckable(True) action_expert_view.setChecked(self.taurusValueBuddy()._expertView) menu.addAction(action_expert_view) - self.connect(action_expert_view, Qt.SIGNAL( - 'toggled(bool)'), self.taurusValueBuddy().setExpertView) + action_expert_view.toggled.connect( + self.taurusValueBuddy().setExpertView) action_tango_attributes = Qt.QAction(self) action_tango_attributes.setIcon( getIcon(':/categories/preferences-system.svg')) action_tango_attributes.setText('Tango Attributes') menu.addAction(action_tango_attributes) - self.connect(action_tango_attributes, Qt.SIGNAL( - 'triggered()'), self.taurusValueBuddy().showTangoAttributes) + action_tango_attributes.triggered.connect( + self.taurusValueBuddy().showTangoAttributes) cm_action = menu.addAction("Compact") cm_action.setCheckable(True) cm_action.setChecked(self.taurusValueBuddy().isCompact()) - self.connect(cm_action, Qt.SIGNAL("toggled(bool)"), - self.taurusValueBuddy().setCompact) + cm_action.toggled.connect(self.taurusValueBuddy().setCompact) menu.exec_(event.globalPos()) event.accept() @@ -959,15 +955,15 @@ def mouseMoveEvent(self, event): model = self.taurusValueBuddy().getModelObj() mimeData = Qt.QMimeData() mimeData.setText(self.lbl_alias.text()) - dev_name = model.getFullName() - attr_name = dev_name + '/Position' + dev_name = model.getFullName().encode('utf-8') + attr_name = dev_name + b'/Position' mimeData.setData(TAURUS_DEV_MIME_TYPE, dev_name) mimeData.setData(TAURUS_ATTR_MIME_TYPE, attr_name) drag = Qt.QDrag(self) drag.setMimeData(mimeData) drag.setHotSpot(event.pos() - self.rect().topLeft()) - drag.start(Qt.Qt.CopyAction) + drag.exec_(Qt.Qt.CopyAction, Qt.Qt.CopyAction) ################################################## # READ WIDGET # @@ -986,11 +982,11 @@ def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) self.setLayout(Qt.QGridLayout()) - self.layout().setMargin(0) + self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) limits_layout = Qt.QHBoxLayout() - limits_layout.setMargin(0) + limits_layout.setContentsMargins(0, 0, 0, 0) limits_layout.setSpacing(0) self.btn_lim_neg = Qt.QPushButton() @@ -1022,7 +1018,7 @@ def __init__(self, parent=None, designMode=False): self.btn_stop.setIcon(getIcon(':/actions/media_playback_stop.svg')) self.layout().addWidget(self.btn_stop, 0, 2) - self.connect(self.btn_stop, Qt.SIGNAL('clicked()'), self.abort) + self.btn_stop.clicked.connect(self.abort) # WITH COMPACT VIEW, WE NEED TO FORWARD DOUBLE CLICK EVENT self.lbl_read.installEventFilter(self) @@ -1066,6 +1062,7 @@ def eventFilter(self, obj, event): pass return True + @Qt.pyqtSlot() @ProtectTaurusMessageBox(msg='An error occurred trying to abort the motion.') def abort(self): motor_dev = self.taurusValueBuddy().motor_dev @@ -1096,8 +1093,11 @@ def prepare_button(self, btn): def setModel(self, model): if hasattr(self, 'taurusValueBuddy'): - self.disconnect(self.taurusValueBuddy(), Qt.SIGNAL( - 'expertViewChanged(bool)'), self.setExpertView) + try: + self.taurusValueBuddy().expertViewChanged.disconnect( + self.setExpertView) + except TypeError: + pass if model in (None, ''): TaurusWidget.setModel(self, model) self.lbl_read.setModel(model) @@ -1108,27 +1108,31 @@ def setModel(self, model): self.lbl_enc_read.setModel(model + '/Encoder') # Handle User/Expert view self.setExpertView(self.taurusValueBuddy()._expertView) - self.connect(self.taurusValueBuddy(), Qt.SIGNAL( - 'expertViewChanged(bool)'), self.setExpertView) + self.taurusValueBuddy().expertViewChanged.connect( + self.setExpertView) ################################################## # WRITE WIDGET # ################################################## - class PoolMotorTVWriteWidget(TaurusWidget): layoutAlignment = Qt.Qt.AlignTop - try: - # TODO: For Taurus 4 compatibility - applied = Qt.pyqtSignal() - except AttributeError: - pass + + applied = Qt.pyqtSignal() def __init__(self, parent=None, designMode=False): TaurusWidget.__init__(self, parent, designMode) + + # ------------------------------------------------------------ + # Workaround for Taurus3 support + if int(taurus.Release.version.split('.')[0]) < 4: + from taurus.qt.qtgui.base.taurusbase import baseOldSignal + self.applied = baseOldSignal('applied', self) + # ------------------------------------------------------------ + self.setLayout(Qt.QGridLayout()) - self.layout().setMargin(0) + self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(0) self.le_write_absolute = TaurusValueLineEdit() @@ -1136,7 +1140,7 @@ def __init__(self, parent=None, designMode=False): self.qw_write_relative = Qt.QWidget() self.qw_write_relative.setLayout(Qt.QHBoxLayout()) - self.qw_write_relative.layout().setMargin(0) + self.qw_write_relative.layout().setContentsMargins(0, 0, 0, 0) self.qw_write_relative.layout().setSpacing(0) self.cb_step = Qt.QComboBox() @@ -1164,8 +1168,8 @@ def __init__(self, parent=None, designMode=False): self.layout().addWidget(self.qw_write_relative, 0, 0) self.cbAbsoluteRelative = Qt.QComboBox() - self.connect(self.cbAbsoluteRelative, Qt.SIGNAL( - 'currentIndexChanged(QString)'), self.cbAbsoluteRelativeChanged) + self.cbAbsoluteRelative.currentIndexChanged['QString'].connect( + self.cbAbsoluteRelativeChanged) self.cbAbsoluteRelative.addItems(['Abs', 'Rel']) self.layout().addWidget(self.cbAbsoluteRelative, 0, 1) @@ -1178,7 +1182,7 @@ def __init__(self, parent=None, designMode=False): #self.layout().addWidget(self.btn_stop, 0, 2) btns_layout = Qt.QHBoxLayout() - btns_layout.setMargin(0) + btns_layout.setContentsMargins(0, 0, 0, 0) btns_layout.setSpacing(0) btns_layout.addItem(Qt.QSpacerItem( @@ -1219,19 +1223,15 @@ def __init__(self, parent=None, designMode=False): self.layout().addLayout(btns_layout, 1, 0, 1, 3) - self.connect(self.btn_step_down, Qt.SIGNAL('clicked()'), self.stepDown) - self.connect(self.btn_step_up, Qt.SIGNAL('clicked()'), self.stepUp) - ###self.connect(self.btn_stop, Qt.SIGNAL('clicked()'), self.abort) - self.connect(self.btn_to_neg, Qt.SIGNAL('clicked()'), self.goNegative) - self.connect(self.btn_to_neg_press, Qt.SIGNAL( - 'pressed()'), self.goNegative) - self.connect(self.btn_to_neg_press, - Qt.SIGNAL('released()'), self.abort) - self.connect(self.btn_to_pos, Qt.SIGNAL('clicked()'), self.goPositive) - self.connect(self.btn_to_pos_press, Qt.SIGNAL( - 'pressed()'), self.goPositive) - self.connect(self.btn_to_pos_press, - Qt.SIGNAL('released()'), self.abort) + self.btn_step_down.clicked.connect(self.stepDown) + self.btn_step_up.clicked.connect(self.stepUp) + # self.btn_stop.clicked.connect(self.abort) + self.btn_to_neg.clicked.connect(self.goNegative) + self.btn_to_neg_press.pressed.connect(self.goNegative) + self.btn_to_neg_press.released.connect(self.abort) + self.btn_to_pos.clicked.connect(self.goPositive) + self.btn_to_pos_press.pressed.connect(self.goPositive) + self.btn_to_pos_press.released.connect(self.abort) # Align everything on top self.layout().addItem(Qt.QSpacerItem( @@ -1249,20 +1249,11 @@ def __init__(self, parent=None, designMode=False): # IN EXPERT VIEW, WE HAVE TO FORWARD THE ''editingFinished()' SIGNAL # FROM TaurusValueLineEdit TO Switcher - try: - self.connect(self.le_write_absolute, Qt.SIGNAL( - TaurusBaseWritableWidget.appliedSignalSignature), self.emitEditingFinished) - except AttributeError: - # TODO: For Taurus 4 adaptation - self.le_write_absolute.applied.connect(self.emitEditingFinished) - self.connect(self.btn_step_down, Qt.SIGNAL( - "clicked()"), self.emitEditingFinished) - self.connect(self.btn_step_up, Qt.SIGNAL( - "clicked()"), self.emitEditingFinished) - self.connect(self.btn_to_neg, Qt.SIGNAL( - "clicked()"), self.emitEditingFinished) - self.connect(self.btn_to_pos, Qt.SIGNAL( - "clicked()"), self.emitEditingFinished) + self.le_write_absolute.applied.connect(self.emitEditingFinished) + self.btn_step_down.clicked.connect(self.emitEditingFinished) + self.btn_step_up.clicked.connect(self.emitEditingFinished) + self.btn_to_neg.clicked.connect(self.emitEditingFinished) + self.btn_to_pos.clicked.connect(self.emitEditingFinished) # list of widgets used for edition editingWidgets = (self.le_write_absolute, self.cbAbsoluteRelative, @@ -1297,9 +1288,11 @@ def cbAbsoluteRelativeChanged(self, abs_rel_option): self.le_write_absolute.setVisible(abs_visible) self.qw_write_relative.setVisible(rel_visible) + @Qt.pyqtSlot() def stepDown(self): self.goRelative(-1) + @Qt.pyqtSlot() def stepUp(self): self.goRelative(+1) @@ -1312,6 +1305,7 @@ def goRelative(self, direction): target_position = position + increment motor_dev.getAttribute('Position').write(target_position) + @Qt.pyqtSlot() @ProtectTaurusMessageBox(msg='An error occurred trying to move the motor.') def goNegative(self): motor_dev = self.taurusValueBuddy().motor_dev @@ -1319,6 +1313,7 @@ def goNegative(self): min_value = float(motor_dev.getAttribute('Position').min_value) motor_dev.getAttribute('Position').write(min_value) + @Qt.pyqtSlot() @ProtectTaurusMessageBox(msg='An error occurred trying to move the motor.') def goPositive(self): motor_dev = self.taurusValueBuddy().motor_dev @@ -1326,6 +1321,7 @@ def goPositive(self): max_value = float(motor_dev.getAttribute('Position').max_value) motor_dev.getAttribute('Position').write(max_value) + @Qt.pyqtSlot() @ProtectTaurusMessageBox(msg='An error occurred trying to abort the motion.') def abort(self): motor_dev = self.taurusValueBuddy().motor_dev @@ -1361,8 +1357,11 @@ def setExpertView(self, expertView): def setModel(self, model): if hasattr(self, 'taurusValueBuddy'): - self.disconnect(self.taurusValueBuddy(), Qt.SIGNAL( - 'expertViewChanged(bool)'), self.setExpertView) + try: + self.taurusValueBuddy().expertViewChanged.disconnect( + self.setExpertView) + except TypeError: + pass if model in (None, ''): TaurusWidget.setModel(self, model) self.le_write_absolute.setModel(model) @@ -1372,8 +1371,8 @@ def setModel(self, model): # Handle User/Expert View self.setExpertView(self.taurusValueBuddy()._expertView) - self.connect(self.taurusValueBuddy(), Qt.SIGNAL( - 'expertViewChanged(bool)'), self.setExpertView) + self.taurusValueBuddy().expertViewChanged.connect( + self.setExpertView) def keyPressEvent(self, key_event): if key_event.key() == Qt.Qt.Key_Escape: @@ -1381,12 +1380,9 @@ def keyPressEvent(self, key_event): key_event.accept() TaurusWidget.keyPressEvent(self, key_event) + @Qt.pyqtSlot() def emitEditingFinished(self): - try: - self.emit(Qt.SIGNAL(TaurusBaseWritableWidget.appliedSignalSignature)) - except AttributeError: - # TODO: For Taurus 4 adaptation - self.applied.emit() + self.applied.emit() ################################################## @@ -1420,6 +1416,8 @@ class PoolMotorTV(TaurusValue): @TODO expert view for read widget should include signals (indexer/encoder/inpos)... ''' + expertViewChanged = Qt.pyqtSignal(bool) + def __init__(self, parent=None, designMode=False): TaurusValue.__init__(self, parent=parent, designMode=designMode) self.setLabelWidgetClass(PoolMotorTVLabelWidget) @@ -1436,28 +1434,44 @@ def __init__(self, parent=None, designMode=False): def setExpertView(self, expertView): self._expertView = expertView - self.emit(Qt.SIGNAL('expertViewChanged(bool)'), expertView) + self.expertViewChanged.emit(expertView) def minimumHeight(self): return None # @todo: UGLY HACK to avoid subwidgets being forced to minimumheight=20 def setModel(self, model): TaurusValue.setModel(self, model) + + # disconnect signals try: - # disconnect signals if self.limits_listener is not None: - self.disconnect(self.limits_listener, Qt.SIGNAL( - 'eventReceived'), self.updateLimits) + self.limits_listener.eventReceivedSignal.disconnect( + self.updateLimits) + except TypeError: + pass + + try: if self.poweron_listener is not None: - self.disconnect(self.poweron_listener, Qt.SIGNAL( - 'eventReceived'), self.updatePowerOn) + self.poweron_listener.eventReceivedSignal.disconnect( + self.updatePowerOn) + except TypeError: + pass + + try: if self.status_listener is not None: - self.disconnect(self.status_listener, Qt.SIGNAL( - 'eventReceived'), self.updateStatus) + self.status_listener.eventReceivedSignal.disconnect( + self.updateStatus) + except TypeError: + pass + + try: if self.position_listener is not None: - self.disconnect(self.position_listener, Qt.SIGNAL( - 'eventReceived'), self.updatePosition) + self.position_listener.eventReceivedSignal.disconnect( + self.updatePosition) + except TypeError: + pass + try: # remove listeners if self.motor_dev is not None: if self.hasHwLimits(): @@ -1475,12 +1489,11 @@ def setModel(self, model): self.motor_dev = None return self.motor_dev = taurus.Device(model) - # CONFIGURE A LISTENER IN ORDER TO UPDATE LIMIT SWITCHES STATES self.limits_listener = TaurusAttributeListener() if self.hasHwLimits(): - self.connect(self.limits_listener, Qt.SIGNAL( - 'eventReceived'), self.updateLimits) + self.limits_listener.eventReceivedSignal.connect( + self.updateLimits) self.motor_dev.getAttribute( 'Limit_Switches').addListener(self.limits_listener) @@ -1488,26 +1501,25 @@ def setModel(self, model): # True/False EXPERT OPERATION self.poweron_listener = TaurusAttributeListener() if self.hasPowerOn(): - self.connect(self.poweron_listener, Qt.SIGNAL( - 'eventReceived'), self.updatePowerOn) + self.poweron_listener.eventReceivedSignal.connect( + self.updatePowerOn) self.motor_dev.getAttribute( 'PowerOn').addListener(self.poweron_listener) # CONFIGURE AN EVENT RECEIVER IN ORDER TO UPDATED STATUS TOOLTIP self.status_listener = TaurusAttributeListener() - self.connect(self.status_listener, Qt.SIGNAL( - 'eventReceived'), self.updateStatus) + self.status_listener.eventReceivedSignal.connect( + self.updateStatus) self.motor_dev.getAttribute( 'Status').addListener(self.status_listener) # CONFIGURE AN EVENT RECEIVER IN ORDER TO ACTIVATE LIMIT BUTTONS ON # SOFTWARE LIMITS self.position_listener = TaurusAttributeListener() - self.connect(self.position_listener, Qt.SIGNAL( - 'eventReceived'), self.updatePosition) + self.position_listener.eventReceivedSignal.connect( + self.updatePosition) self.motor_dev.getAttribute( 'Position').addListener(self.position_listener) - self.motor_dev.getAttribute('Position').enablePolling(force=True) self.setExpertView(self._expertView) diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/controllertree.py b/src/sardana/taurus/qt/qtgui/extra_sardana/controllertree.py index 7445bb772e..d49fd19c48 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/controllertree.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/controllertree.py @@ -124,11 +124,11 @@ class ControllerBaseModel(TaurusBaseModel): def setDataSource(self, pool): if self._data_src is not None: - Qt.QObject.disconnect(self._data_src, Qt.SIGNAL( - 'controllerClassesUpdated'), self.controllerClassesUpdated) + self._data_src.controllerClassesUpdated.disconnect( + self.controllerClassesUpdated) if pool is not None: - Qt.QObject.connect(pool, Qt.SIGNAL( - 'controllerClassesUpdated'), self.controllerClassesUpdated) + pool.controllerClassesUpdated.connect( + self.controllerClassesUpdated) TaurusBaseModel.setDataSource(self, pool) def controllerClassesUpdated(self): @@ -278,6 +278,9 @@ def getModelClass(self): class ControllerClassSelectionDialog(Qt.QDialog): + __pyqtSignals__ = ["accepted", + "rejected"] + def __init__(self, parent=None, designMode=False, model_name=None, perspective=None): Qt.QDialog.__init__(self, parent) @@ -297,8 +300,8 @@ def __init__(self, parent=None, designMode=False, model_name=None, perspective=N self._buttonBox.setStandardButtons(bts) layout.addWidget(self._panel) layout.addWidget(self._buttonBox) - self.connect(self._buttonBox, Qt.SIGNAL("accepted()"), self.accept) - self.connect(self._buttonBox, Qt.SIGNAL("rejected()"), self.reject) + self._buttonBox.accepted.connect(self.accept) + self._buttonBox.rejected.connect(self.reject) def selectedItems(self): return self._panel.selectedItems() diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/expdescription.py b/src/sardana/taurus/qt/qtgui/extra_sardana/expdescription.py index a52b9e9be5..293332471c 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/expdescription.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/expdescription.py @@ -27,7 +27,9 @@ __all__ = ["ExpDescriptionEditor"] -from taurus.external.qt import Qt + +import json +from taurus.external.qt import Qt, QtCore, QtGui import copy import taurus import taurus.core @@ -96,6 +98,70 @@ def filterAcceptsRow(self, sourceRow, sourceParent): return True +def find_diff(first, second): + """ + Return a dict of keys that differ with another config object. If a value + is not found in one fo the configs, it will be represented by KEYNOTFOUND. + :param first: Fist configuration to diff. + :param second: Second configuration to diff. + :return: Dict of Key => (first.val, second.val) + """ + + KEYNOTFOUNDIN1 = 'KeyNotFoundInRemote' + KEYNOTFOUNDIN2 = 'KeyNotFoundInLocal' + + # The GUI can not change these keys. They are changed by the server. + SKIPKEYS = ['_controller_name', 'description', 'timer', 'monitor', 'ndim', + 'source'] + + # These keys can have a list as value. + SKIPLIST = ['scanfile', 'plot_axes', 'prescansnapshot', 'shape'] + + DICT_TYPES = [taurus.core.util.containers.CaselessDict, dict] + diff = {} + sd1 = set(first) + sd2 = set(second) + + # Keys missing in the second dict + for key in sd1.difference(sd2): + if key in SKIPKEYS: + continue + diff[key] = (first[key], KEYNOTFOUNDIN2) + # Keys missing in the first dict + for key in sd2.difference(sd1): + if key in SKIPKEYS: + continue + diff[key] = (KEYNOTFOUNDIN1, second[key]) + + # Check for differences + for key in sd1.intersection(sd2): + if key in SKIPKEYS: + continue + value1 = first[key] + value2 = second[key] + if type(value1) in DICT_TYPES: + try: + idiff = find_diff(value1, value2) + except Exception: + idiff = 'Error on processing' + if len(idiff) > 0: + diff[key] = idiff + elif type(value1) == list and key.lower() not in SKIPLIST: + ldiff = [] + for v1, v2 in zip(value1, value2): + try: + idiff = find_diff(v1, v2) + except Exception: + idiff = 'Error on processing' + ldiff.append(idiff) + if len(ldiff) > 0: + diff[key] = ldiff + else: + if value1 != value2: + diff[key] = (first[key], second[key]) + return diff + + @UILoadable(with_ui='ui') class ExpDescriptionEditor(Qt.QWidget, TaurusBaseWidget): ''' @@ -106,12 +172,18 @@ class ExpDescriptionEditor(Qt.QWidget, TaurusBaseWidget): using the `ExperimentConfiguration` environmental variable for that Door. ''' - def __init__(self, parent=None, door=None, plotsButton=True): + createExpConfChangedDialog = Qt.pyqtSignal() + experimentConfigurationChanged = Qt.pyqtSignal(object) + + def __init__(self, parent=None, door=None, plotsButton=True, + autoUpdate=False): Qt.QWidget.__init__(self, parent) TaurusBaseWidget.__init__(self, 'ExpDescriptionEditor') self.loadUi() self.ui.buttonBox.setStandardButtons( Qt.QDialogButtonBox.Reset | Qt.QDialogButtonBox.Apply) + self.ui.buttonBox.button(Qt.QDialogButtonBox.Reset).setText('Reload') + newperspectivesDict = copy.deepcopy( self.ui.sardanaElementTree.KnownPerspectives) #newperspectivesDict[self.ui.sardanaElementTree.DftPerspective]['model'] = [SardanaAcquirableProxyModel, SardanaElementPlainModel] @@ -128,51 +200,202 @@ def __init__(self, parent=None, door=None, plotsButton=True): self._dirty = False self._dirtyMntGrps = set() - self.connect(self.ui.activeMntGrpCB, Qt.SIGNAL( - 'activated (QString)'), self.changeActiveMntGrp) - self.connect(self.ui.createMntGrpBT, Qt.SIGNAL( - 'clicked ()'), self.createMntGrp) - self.connect(self.ui.deleteMntGrpBT, Qt.SIGNAL( - 'clicked ()'), self.deleteMntGrp) - self.connect(self.ui.compressionCB, Qt.SIGNAL( - 'currentIndexChanged (int)'), self.onCompressionCBChanged) - self.connect(self.ui.pathLE, Qt.SIGNAL( - 'textEdited (QString)'), self.onPathLEEdited) - self.connect(self.ui.filenameLE, Qt.SIGNAL( - 'textEdited (QString)'), self.onFilenameLEEdited) - self.connect(self.ui.channelEditor.getQModel(), Qt.SIGNAL( - 'dataChanged (QModelIndex, QModelIndex)'), self._updateButtonBox) - self.connect(self.ui.channelEditor.getQModel(), Qt.SIGNAL( - 'modelReset ()'), self._updateButtonBox) + self._autoUpdate = False + self._warningWidget = None + self.setContextMenuPolicy(Qt.Qt.ActionsContextMenu) + self._autoUpdateAction = Qt.QAction("Auto update", self) + self._autoUpdateAction.setCheckable(True) + self._autoUpdateAction.toggled.connect(self.setAutoUpdate) + self.addAction(self._autoUpdateAction) + self._autoUpdateAction.setChecked(autoUpdate) + self.registerConfigProperty( + self._autoUpdateAction.isChecked, + self._autoUpdateAction.setChecked, + "autoUpdate") + + # Pending event variables + self._expConfChangedDialog = None + + self.createExpConfChangedDialog.connect( + self._createExpConfChangedDialog) + self.ui.activeMntGrpCB.activated['QString'].connect( + self.changeActiveMntGrp) + self.ui.createMntGrpBT.clicked.connect( + self.createMntGrp) + self.ui.deleteMntGrpBT.clicked.connect( + self.deleteMntGrp) + self.ui.compressionCB.currentIndexChanged['int'].connect( + self.onCompressionCBChanged) + self.ui.pathLE.textEdited.connect( + self.onPathLEEdited) + self.ui.filenameLE.textEdited.connect( + self.onFilenameLEEdited) + self.ui.channelEditor.getQModel().dataChanged.connect( + self._updateButtonBox) + self.ui.channelEditor.getQModel().modelReset.connect( + self._updateButtonBox) preScanList = self.ui.preScanList - self.connect(preScanList, Qt.SIGNAL('dataChanged'), - self.onPreScanSnapshotChanged) - # TODO: For Taurus 4 compatibility - if hasattr(preScanList, "dataChangedSignal"): - preScanList.dataChangedSignal.connect( - self.onPreScanSnapshotChanged) - self.connect(self.ui.choosePathBT, Qt.SIGNAL( - 'clicked ()'), self.onChooseScanDirButtonClicked) + preScanList.dataChangedSignal.connect(self.onPreScanSnapshotChanged) + self.ui.choosePathBT.clicked.connect( + self.onChooseScanDirButtonClicked) self.__plotManager = None + tooltip = None + + # TODO: Disable show scan button since scan plot have to be + # adapted to support QT5 + # -------------------------------------------------------------------- + from taurus.external.qt import PYQT4, API + if not PYQT4: + self.debug('Show plots is only supported with PyQt4 for now') + plotsButton = False + tooltip = "Show/Hide plots is not ready for %s" % API + # -------------------------------------------------------------------- + icon = resource.getIcon(":/actions/view.svg") + measGrpTab = self.ui.tabWidget.widget(0) self.togglePlotsAction = Qt.QAction(icon, "Show/Hide plots", self) + if tooltip is not None: + self.togglePlotsAction.setToolTip(tooltip) self.togglePlotsAction.setCheckable(True) self.togglePlotsAction.setChecked(False) self.togglePlotsAction.setEnabled(plotsButton) - self.addAction(self.togglePlotsAction) - self.connect(self.togglePlotsAction, Qt.SIGNAL("toggled(bool)"), - self.onPlotsButtonToggled) + measGrpTab.addAction(self.togglePlotsAction) + measGrpTab.setContextMenuPolicy(Qt.Qt.ActionsContextMenu) + self.togglePlotsAction.toggled.connect(self.onPlotsButtonToggled) self.ui.plotsButton.setDefaultAction(self.togglePlotsAction) if door is not None: self.setModel(door) - self.connect(self.ui.buttonBox, Qt.SIGNAL( - "clicked(QAbstractButton *)"), self.onDialogButtonClicked) + + self.ui.buttonBox.clicked.connect(self.onDialogButtonClicked) # Taurus Configuration properties and delegates self.registerConfigDelegate(self.ui.channelEditor) + def setAutoUpdate(self, auto_update): + if auto_update and not self._autoUpdate: + self._warningWidget = self._getWarningWidget() + self.ui.verticalLayout_3.insertWidget(0, self._warningWidget) + if not auto_update and self._autoUpdate: + self.ui.verticalLayout_3.removeWidget(self._warningWidget) + self._warningWidget.deleteLater() + self._warningWidget = None + self._autoUpdate = auto_update + + def _getWarningWidget(self): + w = Qt.QWidget() + layout = QtGui.QHBoxLayout() + w.setLayout(layout) + icon = QtGui.QIcon.fromTheme('dialog-warning') + pixmap = QtGui.QPixmap(icon.pixmap(QtCore.QSize(32, 32))) + label_icon = QtGui.QLabel() + label_icon.setPixmap(pixmap) + label = QtGui.QLabel('This experiment configuration dialog ' + 'updates automatically on external changes!') + layout.addWidget(label_icon) + layout.addWidget(label) + layout.addStretch(1) + return w + + def _getResumeText(self): + msg_resume = '

Summary of changes:

    ' + mnt_grps = '' + envs = '' + for key in self._diff: + if key == 'MntGrpConfigs': + for names in self._diff['MntGrpConfigs']: + if mnt_grps != '': + mnt_grps += ', ' + mnt_grps += '{0}'.format(names) + else: + if envs != '': + envs += ', ' + envs += '{0}'.format(key) + values = '' + if mnt_grps != '': + values += '
  • Measurement Groups: {0}
  • '.format(mnt_grps) + if envs != '': + values += '
  • Enviroment variables: {0}
  • '.format(envs) + + msg_resume += values + msg_resume += '

' + return msg_resume + + def _getDetialsText(self): + msg_detials = 'Changes {key: [external, local], ...}\n' + msg_detials += json.dumps(self._diff, sort_keys=True) + return msg_detials + + def _createExpConfChangedDialog(self): + msg_details = self._getDetialsText() + msg_info = self._getResumeText() + self._expConfChangedDialog = Qt.QMessageBox() + self._expConfChangedDialog.setIcon(Qt.QMessageBox.Warning) + self._expConfChangedDialog.setWindowTitle('External Changes') + # text = ''' + #

+ # The experiment configuration has been modified externally.
+ # You can either:
Load the new configuration from the + # door + # (discarding local changes) or Keep your local configuration + # (would eventually overwrite the external changes when applying). + #

''' + text = ''' +

The experiment configuration has been modified externally. + You can either: +

    +
  • Load the new configuration from the door + (discarding local changes)
  • +
  • Keep your local configuration (would eventually + overwrite the external changes when applying)
  • +

+ ''' + self._expConfChangedDialog.setText(text) + self._expConfChangedDialog.setTextFormat(QtCore.Qt.RichText) + self._expConfChangedDialog.setInformativeText(msg_info) + self._expConfChangedDialog.setDetailedText(msg_details) + self._expConfChangedDialog.setStandardButtons(Qt.QMessageBox.Ok | + Qt.QMessageBox.Cancel) + btn_ok = self._expConfChangedDialog.button(Qt.QMessageBox.Ok) + btn_ok.setText('Load') + btn_cancel = self._expConfChangedDialog.button(Qt.QMessageBox.Cancel) + btn_cancel.setText('Keep') + result = self._expConfChangedDialog.exec_() + self._expConfChangedDialog = None + if result == Qt.QMessageBox.Ok: + self._reloadConf(force=True) + + @QtCore.pyqtSlot() + def _experimentConfigurationChanged(self): + self._diff = '' + try: + self._diff = self._getDiff() + except Exception as e: + raise RuntimeError('Error on processing! {0}'.format(e)) + + if len(self._diff) > 0: + if self._autoUpdate: + self._reloadConf(force=True) + else: + if self._expConfChangedDialog is None: + if hasattr(self, 'createExpConfChangedDialog'): + self.createExpConfChangedDialog.emit() + else: + msg_details = self._getDetialsText() + msg_info = self._getResumeText() + self._expConfChangedDialog.setInformativeText(msg_info) + self._expConfChangedDialog.setDetailedText(msg_details) + + def _getDiff(self): + door = self.getModelObj() + if door is None: + return [] + + new_conf = door.getExperimentConfiguration() + old_conf = self._localConfig + return find_diff(new_conf, old_conf) + def getModelClass(self): '''reimplemented from :class:`TaurusBaseWidget`''' return taurus.core.taurusdevice.TaurusDevice @@ -182,7 +405,7 @@ def onChooseScanDirButtonClicked(self): self, 'Choose directory for saving files', self.ui.pathLE.text()) if ret: self.ui.pathLE.setText(ret) - self.ui.pathLE.emit(Qt.SIGNAL('textEdited (QString)'), ret) + self.ui.pathLE.textEdited.emit(ret) def onDialogButtonClicked(self, button): role = self.ui.buttonBox.buttonRole(button) @@ -210,6 +433,8 @@ def setModel(self, model): msname = door.macro_server.getFullName() self.ui.taurusModelTree.setModel(tghost) self.ui.sardanaElementTree.setModel(msname) + door.experimentConfigurationChanged.connect( + self._experimentConfigurationChanged) def _reloadConf(self, force=False): if not force and self.isDataChanged(): @@ -237,6 +462,8 @@ def _reloadConf(self, force=False): for tg_info in tg_elements.values(): avail_triggers[tg_info.full_name] = tg_info.getData() self.ui.channelEditor.getQModel().setAvailableTriggers(avail_triggers) + self.experimentConfigurationChanged.emit(copy.deepcopy(conf)) + def _setDirty(self, dirty): self._dirty = dirty @@ -331,12 +558,11 @@ def writeExperimentConfiguration(self, ask=True): self._dirtyMntGrps = set() self.ui.channelEditor.getQModel().setDataChanged(False) self._setDirty(False) - self.emit(Qt.SIGNAL('experimentConfigurationChanged'), - copy.deepcopy(conf)) + self.experimentConfigurationChanged.emit(copy.deepcopy(conf)) return True + @Qt.pyqtSlot('QString') def changeActiveMntGrp(self, activeMntGrpName): - activeMntGrpName = str(activeMntGrpName) if self._localConfig is None: return if activeMntGrpName == self._localConfig['ActiveMntGrp']: @@ -426,6 +652,7 @@ def deleteMntGrp(self): self.ui.channelEditor.getQModel().setDataSource({}) self._setDirty(True) + @Qt.pyqtSlot('int') def onCompressionCBChanged(self, idx): if self._localConfig is None: return @@ -459,24 +686,24 @@ def onPreScanSnapshotChanged(self, items): def onPlotsButtonToggled(self, checked): if checked: - from taurus.qt.qtgui.taurusgui.macrolistener import \ + from sardana.taurus.qt.qtgui.macrolistener import \ DynamicPlotManager self.__plotManager = DynamicPlotManager(self) self.__plotManager.setModel(self.getModelName()) - self.connect(self, Qt.SIGNAL('experimentConfigurationChanged'), - self.__plotManager.onExpConfChanged) + self.experimentConfigurationChanged.connect( + self.__plotManager.onExpConfChanged) else: - self.disconnect(self, Qt.SIGNAL('experimentConfigurationChanged'), - self.__plotManager.onExpConfChanged) + self.experimentConfigurationChanged.disconnect( + self.__plotManager.onExpConfChanged) self.__plotManager.removePanels() self.__plotManager.setModel(None) self.__plotManager = None -def demo(model=None): +def demo(model=None, autoUpdate=False): """Experiment configuration""" #w = main_ChannelEditor() - w = ExpDescriptionEditor() + w = ExpDescriptionEditor(autoUpdate=autoUpdate) if model is None: from sardana.taurus.qt.qtgui.extra_macroexecutor import \ TaurusMacroConfigurationDialog @@ -496,14 +723,23 @@ def main(): app = Application.instance() owns_app = app is None - if owns_app: + import taurus.core.util.argparse + parser = taurus.core.util.argparse.get_taurus_parser() + parser.usage = "%prog [options] " + parser.add_option('--auto-update', dest='auto_update', + action='store_true', + help='Set auto update of experiment configuration') app = Application(app_name="Exp. Description demo", app_version="1.0", - org_domain="Sardana", org_name="Tango community") + org_domain="Sardana", org_name="Tango community", + cmd_line_parser=parser) args = app.get_command_line_args() + opt = app.get_command_line_options() + if len(args) == 1: - w = demo(model=args[0]) + auto_update = opt.auto_update is not None + w = demo(model=args[0], autoUpdate=auto_update) else: w = demo() w.show() @@ -513,5 +749,6 @@ def main(): else: return w + if __name__ == "__main__": main() diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/macrotree.py b/src/sardana/taurus/qt/qtgui/extra_sardana/macrotree.py index bb01715584..ab701163c3 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/macrotree.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/macrotree.py @@ -126,11 +126,11 @@ class MacroBaseModel(TaurusBaseModel): def setDataSource(self, ms): if self._data_src is not None: - Qt.QObject.disconnect(self._data_src, Qt.SIGNAL( - 'macrosUpdated'), self.macrosUpdated) + self._data_src.macrosUpdated.disconnect( + self.macrosUpdated) if ms is not None: - Qt.QObject.connect(ms, Qt.SIGNAL( - 'macrosUpdated'), self.macrosUpdated) + ms.macrosUpdated.connect( + self.macrosUpdated) TaurusBaseModel.setDataSource(self, ms) def macrosUpdated(self): @@ -273,8 +273,8 @@ def __init__(self, parent=None, designMode=False, model_name=None, perspective=N self._buttonBox.setStandardButtons(bts) layout.addWidget(self._panel) layout.addWidget(self._buttonBox) - self.connect(self._buttonBox, Qt.SIGNAL("accepted()"), self.accept) - self.connect(self._buttonBox, Qt.SIGNAL("rejected()"), self.reject) + self._buttonBox.accepted.connect(self.accept) + self._buttonBox.rejected.connect(self.reject) def selectedItems(self): return self._panel.selectedItems() diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/measurementgroup.py b/src/sardana/taurus/qt/qtgui/extra_sardana/measurementgroup.py index b00e0200bb..ad88c1b13b 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/measurementgroup.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/measurementgroup.py @@ -273,6 +273,8 @@ def getElementTypeToolTip(t): class BaseMntGrpChannelItem(TaurusBaseTreeItem): """ """ + dataChanged = Qt.pyqtSignal('QModelIndex', 'QModelIndex') + def data(self, index): """Returns the data of this node for the given index @@ -329,20 +331,18 @@ def data(self, index): def setData(self, index, qvalue): taurus_role = index.model().role(index.column()) - str_value = Qt.from_qvariant(qvalue, str) if taurus_role in (ChannelView.Channel, ChannelView.Conditioning, - ChannelView.NXPath, ChannelView.DataType): - data = str_value - elif taurus_role in (ChannelView.Enabled, ChannelView.Output): - data = Qt.from_qvariant(qvalue, bool) + ChannelView.NXPath, ChannelView.DataType, + ChannelView.Enabled, ChannelView.Output): + data = qvalue elif taurus_role == ChannelView.PlotType: - data = PlotType[str_value] + data = PlotType[qvalue] elif taurus_role == ChannelView.Normalization: - data = Normalization[str_value] + data = Normalization[qvalue] elif taurus_role == ChannelView.PlotAxes: - data = [a for a in str_value.split('|')] + data = [a for a in qvalue.split('|')] elif taurus_role == ChannelView.Shape: - s = str_value + s = qvalue try: data = eval(s, {}, {}) if not isinstance(data, (tuple, list)): @@ -512,12 +512,12 @@ def data(self, index, role=Qt.Qt.DisplayRole): " in favor of synchronization. Re-apply" " configuration in order to upgrade.") self.warning(msg) - return Qt.QVariant(AcqSynchType[synchronization]) + return AcqSynchType[synchronization] elif taurus_role in (ChannelView.Timer, ChannelView.Monitor): ch_name, ch_data = index.internalPointer().itemData() ctrlname = ch_data['_controller_name'] if ctrlname.startswith("__"): - return Qt.QVariant() + return None ch_info = self.getAvailableChannels()[ch_name] if ch_info['type'] in ('CTExpChannel', 'OneDExpChannel', 'TwoDExpChannel'): unitdict = self.getPyData(ctrlname=ctrlname) @@ -527,10 +527,10 @@ def data(self, index, role=Qt.Qt.DisplayRole): key = taurus_role == ChannelView.Timer and 'timer' or 'monitor' master_full_name = self._mgconfig.get(key, None) if master_full_name is None: - return Qt.QVariant() + return None else: master_info = self.getAvailableChannels()[master_full_name] - return Qt.QVariant(master_info['name']) + return master_info['name'] elif taurus_role == ChannelView.Synchronizer: ch_name, ch_data = index.internalPointer().itemData() ctrlname = ch_data['_controller_name'] @@ -539,12 +539,12 @@ def data(self, index, role=Qt.Qt.DisplayRole): trigger_fullname = ctrl_data.get(key, None) all_triggers = self.getAvailableTriggers() if trigger_fullname is None: - return Qt.QVariant() + return None else: trigger_name = all_triggers[trigger_fullname] - return Qt.QVariant(trigger_name['name']) + return trigger_name['name'] - return Qt.QVariant() + return None def setData(self, index, qvalue, role=Qt.Qt.EditRole): # For those things which are at the unit level, we handle them here @@ -554,8 +554,7 @@ def setData(self, index, qvalue, role=Qt.Qt.EditRole): ch_info = self.getAvailableChannels()[ch_name] ctrl_data = self.getPyData(ctrlname=ch_data['_controller_name']) key = self.data_keys_map[taurus_role] - data = Qt.from_qvariant(qvalue, str) - + data = qvalue self._dirty = True self.beginResetModel() is_settable = ch_info['type'] in ( @@ -573,16 +572,15 @@ def setData(self, index, qvalue, role=Qt.Qt.EditRole): self._mgconfig[key] = data self.endResetModel() return True - if taurus_role == ChannelView.Synchronizer: + elif taurus_role == ChannelView.Synchronizer: ch_name, ch_data = index.internalPointer().itemData() ctrlname = ch_data['_controller_name'] key = self.data_keys_map[taurus_role] - data = Qt.from_qvariant(qvalue, str) self._dirty = True self.beginResetModel() ctrl_data = self.getPyData(ctrlname=ctrlname) - ctrl_data[key] = data - self._mgconfig[key] = data + ctrl_data[key] = qvalue + self._mgconfig[key] = qvalue self.endResetModel() return True # for the rest, we use the regular TaurusBaseModel item-oriented approach @@ -591,8 +589,7 @@ def setData(self, index, qvalue, role=Qt.Qt.EditRole): item = index.internalPointer() item.setData(index, qvalue) self._dirty = True - self.emit(Qt.SIGNAL("dataChanged(const QModelIndex &, const QModelIndex &)"), - index, index) + self.dataChanged.emit(index, index) return True # @todo: Very inefficient implementation. We should use {begin|end}InsertRows @@ -683,11 +680,10 @@ class MntGrpChannelModel(BaseMntGrpChannelModel): def setDataSource(self, mg): if self._data_src is not None: - Qt.QObject.disconnect(self._data_src, Qt.SIGNAL( - 'configurationChanged'), self.configurationChanged) + self._data_src.configurationChanged.disconnect( + self.configurationChanged) if mg is not None: - Qt.QObject.connect(mg, Qt.SIGNAL( - 'configurationChanged'), self.configurationChanged) + mg.configurationChanged.connect(self.configurationChanged) BaseMntGrpChannelModel.setDataSource(self, mg) def configurationChanged(self): @@ -788,11 +784,11 @@ def setEditorData(self, editor, index): taurus_role = model.role(index.column()) if taurus_role == ChannelView.PlotType: editor.addItems(PlotType.keys()) - current = Qt.from_qvariant(model.data(index), str) + current = model.data(index) editor.setCurrentIndex(editor.findText(current)) elif taurus_role == ChannelView.Normalization: editor.addItems(Normalization.keys()) - current = Qt.from_qvariant(model.data(index), str) + current = model.data(index) editor.setCurrentIndex(editor.findText(current)) elif taurus_role in (ChannelView.Timer, ChannelView.Monitor): key = taurus_role == ChannelView.Timer and 'timer' or 'monitor' @@ -804,33 +800,32 @@ def setEditorData(self, editor, index): if all_channels[ch_name]['type'] in ('CTExpChannel', 'OneDExpChannel', 'TwoDExpChannel'): for full_name, channel_data in ctrl_dict: editor.addItem( - channel_data['name'], Qt.QVariant(full_name)) - current = Qt.from_qvariant(model.data(index), str) + channel_data['name'], full_name) + current = model.data(index) editor.setCurrentIndex(editor.findText(current)) else: for ctrl_data in dataSource['controllers'].values(): if key in ctrl_data: channel = all_channels[ctrl_data[key]] - editor.addItem(channel['name'], - Qt.QVariant(channel['full_name'])) + editor.addItem(channel['name'], channel['full_name']) current = dataSource.get(key) # current global timer/monitor - editor.setCurrentIndex(editor.findData(Qt.QVariant(current))) + editor.setCurrentIndex(editor.findData(current)) elif taurus_role == ChannelView.Synchronization: editor.addItems(AcqSynchType.keys()) - current = Qt.from_qvariant(model.data(index), str) + current = model.data(index) editor.setCurrentIndex(editor.findText(current)) elif taurus_role == ChannelView.PlotAxes: selectables = ['', ''] + \ [n for n, d in getChannelConfigs(dataSource)] editor.setChoices(selectables) - current = Qt.from_qvariant(model.data(index), str) + current = model.data(index) editor.setCurrentChoices(current) elif taurus_role == ChannelView.Synchronizer: # add the triggergates to the editor all_triggers = model.getAvailableTriggers() for full_name, tg_data in all_triggers.items(): - editor.addItem(tg_data['name'], Qt.QVariant(full_name)) - current = Qt.from_qvariant(model.data(index), str) + editor.addItem(tg_data['name'], full_name) + current = model.data(index) editor.setCurrentIndex(editor.findText(current)) else: Qt.QStyledItemDelegate.setEditorData(self, editor, index) @@ -839,10 +834,10 @@ def setModelData(self, editor, model, index): taurus_role = model.role(index.column()) dataSource = model.dataSource() if taurus_role in (ChannelView.Channel, ChannelView.PlotType, ChannelView.Normalization): - data = Qt.QVariant(editor.currentText()) + data = editor.currentText() model.setData(index, data) elif taurus_role == ChannelView.Synchronization: - old_value = Qt.from_qvariant(model.data(index), str) + old_value = model.data(index) new_value = str(editor.currentText()) if new_value == old_value: return @@ -857,11 +852,11 @@ def setModelData(self, editor, model, index): Qt.QMessageBox.Yes | Qt.QMessageBox.Cancel) if op != Qt.QMessageBox.Yes: return - data = Qt.QVariant(new_value) + data = new_value model.setData(index, data) elif taurus_role in (ChannelView.Timer, ChannelView.Monitor): key = taurus_role == ChannelView.Timer and 'timer' or 'monitor' - old_value = Qt.from_qvariant(model.data(index), str) + old_value = model.data(index) new_value = str(editor.currentText()) if new_value == old_value: return @@ -915,10 +910,10 @@ def setModelData(self, editor, model, index): return model.setData(index, selected_master) elif taurus_role == ChannelView.PlotAxes: - data = Qt.QVariant(editor.text()) + data = editor.text() model.setData(index, data) elif taurus_role == ChannelView.Synchronizer: - old_value = Qt.from_qvariant(model.data(index), str) + old_value = model.data(index) new_value = str(editor.currentText()) if new_value == old_value: return @@ -971,8 +966,7 @@ def __init__(self, parent=None, designMode=False, with_filter_widget=True, persp self.setContextMenuPolicy(Qt.Qt.ActionsContextMenu) self._simpleViewAction = Qt.QAction("Simple View", self) self._simpleViewAction.setCheckable(True) - self.connect(self._simpleViewAction, Qt.SIGNAL( - "toggled(bool)"), self.setSimpleView) + self._simpleViewAction.toggled.connect(self.setSimpleView) self.addAction(self._simpleViewAction) self.registerConfigProperty( self.isSimpleView, self.setSimpleView, "simpleView") @@ -1004,34 +998,12 @@ def createViewWidget(self): # causes a segfault when calling ChannelDelegate.createEditor tableView.setItemDelegate(self._delegate) tableView.setSortingEnabled(False) - self.connect(self._editorBar, Qt.SIGNAL( - "addTriggered"), self.addChannel) - # TODO: For Taurus 4 compatibility - if hasattr(self._editorBar, "addTriggered"): - self._editorBar.addTriggered.connect(self.addChannel) - self.connect(self._editorBar, Qt.SIGNAL( - "removeTriggered"), self.removeChannels) - # TODO: For Taurus 4 compatibility - if hasattr(self._editorBar, "removeTriggered"): - self._editorBar.removeTriggered.connect(self.removeChannels) - self.connect(self._editorBar, Qt.SIGNAL( - "moveUpTriggered"), self.moveUpChannel) - # TODO: For Taurus 4 compatibility - if hasattr(self._editorBar, "moveUpTriggered"): - self._editorBar.moveUpTriggered.connect(self.moveUpChannel) - self.connect(self._editorBar, Qt.SIGNAL( - "moveDownTriggered"), self.moveDownChannel) - # TODO: For Taurus 4 compatibility - if hasattr(self._editorBar, "moveDownTriggered"): - self._editorBar.moveDownTriggered.connect(self.moveDownChannel) - self.connect(self._editorBar, Qt.SIGNAL( - "moveTopTriggered"), self.moveTopChannel) - if hasattr(self._editorBar, "moveTopTriggered"): - self._editorBar.moveTopTriggered.connect(self.moveTopChannel) - self.connect(self._editorBar, Qt.SIGNAL( - "moveBottomTriggered"), self.moveBottomChannel) - if hasattr(self._editorBar, "moveBottomTriggered"): - self._editorBar.moveBottomTriggered.connect(self.moveBottomChannel) + self._editorBar.addTriggered.connect(self.addChannel) + self._editorBar.removeTriggered.connect(self.removeChannels) + self._editorBar.moveUpTriggered.connect(self.moveUpChannel) + self._editorBar.moveDownTriggered.connect(self.moveDownChannel) + self._editorBar.moveTopTriggered.connect(self.moveTopChannel) + self._editorBar.moveBottomTriggered.connect(self.moveBottomChannel) return tableView def createToolArea(self): diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/sardanaeditor.py b/src/sardana/taurus/qt/qtgui/extra_sardana/sardanaeditor.py old mode 100755 new mode 100644 index 70d0ce6b59..5bb434590a --- a/src/sardana/taurus/qt/qtgui/extra_sardana/sardanaeditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/sardanaeditor.py @@ -150,12 +150,8 @@ def __init__(self, parent=None, designMode=None): SardanaLibTreeWidget(self, with_navigation_bar=False, with_filter_widget=False,) elementTree.treeView().setColumnHidden(1, True) - try: - self._elementTree.itemDoubleClicked.connect( - self.on_element_clicked) - except AttributeError: - self.connect(self._elementTree, Qt.SIGNAL("itemDoubleClicked"), - self.on_element_clicked) + self._elementTree.itemDoubleClicked.connect( + self.on_element_clicked) self.insertWidget(0, self._elementTree) self.setAutoTooltip(False) @@ -206,12 +202,8 @@ def createMenuActions(self): def register_editorstack(self, editorstack): TaurusBaseEditor.register_editorstack(self, editorstack) - try: - self.editorstack.refresh_save_all_action.connect( - self.refresh_save_and_apply_action) - except AttributeError: - self.connect(editorstack, Qt.SIGNAL('refresh_save_all_action()'), - self.refresh_save_and_apply_action) + editorstack.refresh_save_all_action.connect( + self.refresh_save_and_apply_action) def refresh_save_and_apply_action(self): self.save_and_apply_action.setEnabled(self.save_action.isEnabled()) diff --git a/src/sardana/taurus/qt/qtgui/macrolistener/__init__.py b/src/sardana/taurus/qt/qtgui/macrolistener/__init__.py new file mode 100644 index 0000000000..1dda39e783 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/macrolistener/__init__.py @@ -0,0 +1,31 @@ +#!/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 . +## +############################################################################## + +"""This package contains a collection of taurus widgets designed to connect +to sardana""" + +__docformat__ = 'restructuredtext' + +from .macrolistener import * # noqa: F401,F403 diff --git a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py new file mode 100644 index 0000000000..db8251773c --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py @@ -0,0 +1,613 @@ +#!/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 . +## +############################################################################## + +""" +This module provides objects to manage macro-related tasks. Its primary use is +to be used within a TaurusGui for managing panels for: +- setting preferences in the sardana control system for data I/O +- displaying results of macro executions, including creating/removing panels + for plotting results of scans +- editing macros + +.. note:: This module was originally implemented in taurus as + `taurus.qt.qtgui.taurusgui.macrolistener` +""" + +from __future__ import print_function + +from builtins import object + +import datetime + +from taurus.core.util.containers import CaselessList +from taurus.external.qt import Qt +from taurus.qt.qtgui.base import TaurusBaseComponent + + +__all__ = ['MacroBroker', 'DynamicPlotManager'] +__docformat__ = 'restructuredtext' + + +class ChannelFilter(object): + + def __init__(self, chlist): + self.chlist = tuple(chlist) + + def __call__(self, x): + return x in self.chlist + + +class DynamicPlotManager(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. + ''' + + newShortMessage = Qt.pyqtSignal('QString') + + def __init__(self, parent=None): + Qt.QObject.__init__(self, parent) + TaurusBaseComponent.__init__(self, self.__class__.__name__) + + self.__panels = {} + + self._trends1d = {} + self._trends2d = {} + + def setModel(self, doorname): + '''reimplemented from :meth:`TaurusBaseComponent` + + :param doorname: (str) device name corresponding to a Door device. + ''' + TaurusBaseComponent.setModel(self, doorname) + # self._onDoorChanged(doorname) + if not doorname: + return + door = self.getModelObj() + if not isinstance(door, Qt.QObject): + msg = "Unexpected type (%s) for %s" % (repr(type(door)), doorname) + Qt.QMessageBox.critical( + self.parent(), 'Door connection error', msg) + return + + self._checkJsonRecorder() + + # read the expconf + expconf = door.getExperimentConfiguration() + self.onExpConfChanged(expconf) + + def _checkJsonRecorder(self): + '''Checks if JsonRecorder env var is set and offers to set it''' + door = self.getModelObj() + if 'JsonRecorder' not in door.getEnvironment(): + msg = ('JsonRecorder environment variable is not set, but it ' + + 'is needed for displaying trend plots.\n' + + 'Enable it globally for %s?') % door.fullname + result = Qt.QMessageBox.question( + self.parent(), 'JsonRecorder not set', msg, + Qt.QMessageBox.Yes | Qt.QMessageBox.No) + if result == Qt.QMessageBox.Yes: + door.putEnvironment('JsonRecorder', True) + self.info('JsonRecorder Enabled for %s' % door.fullname) + + def onExpConfChanged(self, expconf): + ''' + Slot to be called when experimental configuration changes. It should + remove the temporary panels and create the new ones needed. + + :param expconf: (dict) An Experiment Description dictionary. See + :meth:`sardana.taurus.qt.qtcore.tango.sardana. + QDoor.getExperimentDescription` + for more details + ''' + + from sardana.taurus.core.tango.sardana import PlotType + from sardana.taurus.core.tango.sardana.pool import getChannelConfigs + activeMntGrp = expconf['ActiveMntGrp'] + if activeMntGrp is None: + return + if activeMntGrp not in expconf['MntGrpConfigs']: + self.warning( + "ActiveMntGrp '%s' is not defined" % + activeMntGrp) + return + mgconfig = expconf['MntGrpConfigs'][activeMntGrp] + channels = dict(getChannelConfigs(mgconfig, sort=False)) + + # classify by type of plot: + trends1d = {} + trends2d = {} + + for chname, chdata in channels.items(): + ptype = chdata['plot_type'] + if ptype == PlotType.No: + continue + elif ptype == PlotType.Spectrum: + axes = tuple(chdata['plot_axes']) + # TODO: get default value from the channel. + ndim = chdata.get('ndim', 0) or 0 + if ndim == 0: # this is a trend + if axes in trends1d: + trends1d[axes].append(chname) + else: + trends1d[axes] = CaselessList([chname]) + elif ndim == 1: # a 1D plot (e.g. a spectrum) + pass # TODO: implement + else: + self.warning('Cannot create plot for %s', chname) + + elif ptype == PlotType.Image: + axes = tuple(chdata['plot_axes']) + # TODO: get default value from the channel. + ndim = chdata.get('ndim', 1) + if ndim == 0: # a mesh-like plot? + pass # TODO implement + elif ndim == 1: # a 2D trend + if axes in trends2d: + trends2d[axes].append(chname) + else: + trends2d[axes] = CaselessList([chname]) + elif ndim == 2: # a 2D plot (e.g. an image) + pass # TODO: implement + else: + self.warning('Cannot create plot for %s', chname) + try: + # TODO: adapt _updateTemporaryTrends1D to use tpg + new1d, removed1d = self._updateTemporaryTrends1D(trends1d) + self.newShortMessage.emit("Changed panels (%i new, %i removed)" + % (len(new1d), len(removed1d))) + except Exception: + self.warning( + 'Plots cannot be updated. Only qwt5 is supported for now' + ) +# self._updateTemporaryTrends2D(trends2d) + + def _updateTemporaryTrends1D(self, trends1d): + '''adds necessary trend1D panels and removes no longer needed ones + + :param trends1d: (dict) A dict whose keys are tuples of axes and + whose values are list of model names to plot + + :returns: (tuple) two lists new,rm:new contains the names of the new + panels and rm contains the names of the removed panels + ''' + from taurus.qt.qtgui.plot import TaurusTrend # TODO: use tpg instead! + newpanels = [] + for axes, plotables in trends1d.items(): + if not axes: + continue + if axes not in self._trends1d: + w = TaurusTrend() + w.setXIsTime(False) + w.setScanDoor(self.getModelObj().fullname) + # TODO: use a standard key for and + w.setScansXDataKey(axes[0]) + pname = u'Trend1D - %s' % ":".join(axes) + panel = self.createPanel(w, pname, registerconfig=False, + permanent=False) + try: # if the panel is a dockwidget, raise it + panel.raise_() + except Exception: + pass + self._trends1d[axes] = pname + newpanels.append(pname) + + widget = self.getPanelWidget(self._trends1d[axes]) + flt = ChannelFilter(plotables) + widget.onScanPlotablesFilterChanged(flt) + + # remove trends that are no longer configured + removedpanels = [] + olditems = list(self._trends1d.items()) + for axes, name in olditems: + if axes not in trends1d: + removedpanels.append(name) + self.removePanel(name) + self._trends1d.pop(axes) + + return newpanels, removedpanels + + def _updateTemporaryTrends2D(self, trends2d): + '''adds necessary trend2D panels and removes no longer needed ones + + :param trends2d: (dict) A dict whose keys are tuples of axes and + whose values are list of model names to plot + + :returns: (tuple) two lists new,rm:new contains the names of the new + panels and rm contains the names of the removed panels + + ..note:: Not fully implemented yet + ''' + try: + from taurus.qt.qtgui.extra_guiqwt.taurustrend2d import \ + TaurusTrend2DDialog + from taurus.qt.qtgui.extra_guiqwt.image import ( + TaurusTrend2DScanItem) + except Exception: + self.info('guiqwt extension cannot be loaded. ' + + '2D Trends will not be created') + raise + return + + for axes, plotables in trends2d.items(): + for chname in plotables: + pname = u'Trend2D - %s' % chname + if pname in self._trends2d: + self._trends2d[pname].widget().trendItem.clearTrend() + else: + axis = axes[0] + w = TaurusTrend2DDialog(stackMode='event') + plot = w.get_plot() + t2d = TaurusTrend2DScanItem(chname, axis, + self.getModelObj().fullname) + plot.add_item(t2d) + self.createPanel(w, pname, registerconfig=False, + permanent=False) + self._trends2d[(axes, chname)] = pname + + 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 + + :param widget: (QWidget) widget to be used for the panel + :param name: (str) name of the panel. Must be unique. + + Note: for backawards compatibility, this implementation accepts + arbitrary keyword arguments which are just ignored + ''' + widget.setWindowTitle(name) + widget.show() + self.__panels[name] = widget + + def getPanelWidget(self, name): + '''Returns the widget associated to a panel name + + :param name: (str) name of the panel. KeyError is raised if not found + + :return: (QWidget) + ''' + return self.__panels[name] + + def removePanel(self, name): + '''stop managing the given panel + + :param name: (str) name of the panel''' + widget = self.__panels.pop(name) + if hasattr(widget, 'setModel'): + widget.setModel(None) + widget.setParent(None) + widget.close() + + def removePanels(self, names=None): + '''removes panels. + + :param names: (seq) names of the panels to be removed. If None is + given (default), all the panels are removed. + ''' + if names is None: + names = (list(self._trends1d.values()) + + list(self._trends2d.values())) + # TODO: do the same for other temporary panels + for pname in names: + self.removePanel(pname) + + +class MacroBroker(DynamicPlotManager): + '''A manager of all macro-related panels of a TaurusGui. + + It creates, destroys and manages connections for the following objects: + + - Macro Configuration dialog + - Experiment Configuration panel + - Macro Executor panel + - Sequencer panel + - Macro description viewer + - Door output, result and debug panels + - Macro editor + - Macro "panic" button (to abort macros) + - Dynamic plots (see :class:`DynamicPlotManager`) + ''' + + def __init__(self, parent): + '''Passing the parent object (the main window) is mandatory''' + DynamicPlotManager.__init__(self, parent) + + self._createPermanentPanels() + + # connect the broker to shared data + Qt.qApp.SDM.connectReader("doorName", self.setModel) + Qt.qApp.SDM.connectReader("expConfChanged", self.onExpConfChanged) + Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') + + def setModel(self, doorname): + ''' Reimplemented from :class:`DynamicPlotManager`.''' + # disconnect the previous door + door = self.getModelObj() + if door is not None: # disconnect it from *all* shared data providing + SDM = Qt.qApp.SDM + try: + SDM.disconnectWriter("macroStatus", door, + "macroStatusUpdated") + except Exception: + self.info("Could not disconnect macroStatusUpdated") + try: + SDM.disconnectWriter("doorOutputChanged", door, + "outputUpdated") + except Exception: + self.info("Could not disconnect outputUpdated") + try: + SDM.disconnectWriter("doorInfoChanged", door, "infoUpdated") + except Exception: + self.info("Could not disconnect infoUpdated") + try: + SDM.disconnectWriter("doorWarningChanged", door, + "warningUpdated") + except Exception: + self.info("Could not disconnect warningUpdated") + try: + SDM.disconnectWriter("doorErrorChanged", door, "errorUpdated") + except Exception: + self.info("Could not disconnect errorUpdated") + try: + SDM.disconnectWriter("doorDebugChanged", door, "debugUpdated") + except Exception: + self.info("Could not disconnect debugUpdated") + try: + SDM.disconnectWriter("doorResultChanged", door, + "resultUpdated") + except Exception: + self.info("Could not disconnect resultUpdated") + try: + SDM.disconnectWriter("expConfChanged", door, + "experimentConfigurationChanged") + except Exception: + self.info( + "Could not disconnect experimentConfigurationChanged") + # set the model + DynamicPlotManager.setModel(self, doorname) + + # connect the new door + door = self.getModelObj() + if door is not None: + SDM = Qt.qApp.SDM + SDM.connectWriter("macroStatus", door, "macroStatusUpdated") + SDM.connectWriter("doorOutputChanged", door, "outputUpdated") + SDM.connectWriter("doorInfoChanged", door, "infoUpdated") + SDM.connectWriter("doorWarningChanged", door, "warningUpdated") + SDM.connectWriter("doorErrorChanged", door, "errorUpdated") + SDM.connectWriter("doorDebugChanged", door, "debugUpdated") + SDM.connectWriter("doorResultChanged", door, "resultUpdated") + SDM.connectWriter("expConfChanged", door, + "experimentConfigurationChanged") + + def _createPermanentPanels(self): + '''creates panels on the main window''' + from sardana.taurus.qt.qtgui.extra_macroexecutor import \ + TaurusMacroExecutorWidget, TaurusSequencerWidget, \ + TaurusMacroConfigurationDialog, TaurusMacroDescriptionViewer, \ + DoorOutput, DoorDebug, DoorResult + + from sardana.taurus.qt.qtgui.extra_sardana import \ + ExpDescriptionEditor + + mainwindow = self.parent() + + # Create macroconfiguration dialog & action + self.__macroConfigurationDialog = \ + TaurusMacroConfigurationDialog(mainwindow) + self.macroConfigurationAction = mainwindow.taurusMenu.addAction( + Qt.QIcon.fromTheme("preferences-system-session"), + "Macro execution configuration...", + self.__macroConfigurationDialog.show) + + SDM = Qt.qApp.SDM + SDM.connectReader("macroserverName", + self.__macroConfigurationDialog.selectMacroServer) + SDM.connectReader("doorName", + self.__macroConfigurationDialog.selectDoor) + SDM.connectWriter("macroserverName", self.__macroConfigurationDialog, + 'macroserverNameChanged') + SDM.connectWriter("doorName", self.__macroConfigurationDialog, + 'doorNameChanged') + + # Create ExpDescriptionEditor dialog + self.__expDescriptionEditor = ExpDescriptionEditor(plotsButton=False) + SDM.connectReader("doorName", self.__expDescriptionEditor.setModel) + mainwindow.createPanel(self.__expDescriptionEditor, + 'Experiment Config', + registerconfig=True, + icon=Qt.QIcon.fromTheme('preferences-system'), + permanent=True) + ############################### + # TODO: These lines can be removed once the door does emit + # "experimentConfigurationChanged" signals + SDM.connectWriter("expConfChanged", self.__expDescriptionEditor, + "experimentConfigurationChanged") + ################################ + + # put a Macro Executor + self.__macroExecutor = TaurusMacroExecutorWidget() + SDM.connectReader("macroserverName", self.__macroExecutor.setModel) + SDM.connectReader("doorName", self.__macroExecutor.onDoorChanged) + SDM.connectReader("macroStatus", + self.__macroExecutor.onMacroStatusUpdated) + SDM.connectWriter("macroName", self.__macroExecutor, + "macroNameChanged") + SDM.connectWriter("executionStarted", self.__macroExecutor, + "macroStarted") + SDM.connectWriter("plotablesFilter", self.__macroExecutor, + "plotablesFilterChanged") + SDM.connectWriter("shortMessage", self.__macroExecutor, + "shortMessageEmitted") + mainwindow.createPanel(self.__macroExecutor, 'Macros', + registerconfig=True, permanent=True) + + # put a Sequencer + self.__sequencer = TaurusSequencerWidget() + SDM.connectReader("macroserverName", self.__sequencer.setModel) + SDM.connectReader("doorName", self.__sequencer.onDoorChanged) + SDM.connectReader("macroStatus", + self.__sequencer.onMacroStatusUpdated) + SDM.connectWriter("macroName", self.__sequencer.tree, + "macroNameChanged") + SDM.connectWriter("macroName", self.__sequencer, + "macroNameChanged") + SDM.connectWriter("executionStarted", self.__sequencer, + "macroStarted") + SDM.connectWriter("plotablesFilter", self.__sequencer, + "plotablesFilterChanged") + SDM.connectWriter("shortMessage", self.__sequencer, + "shortMessageEmitted") + mainwindow.createPanel(self.__sequencer, 'Sequences', + registerconfig=True, permanent=True) + + # puts a macrodescriptionviewer + self.__macroDescriptionViewer = TaurusMacroDescriptionViewer() + SDM.connectReader("macroserverName", + self.__macroDescriptionViewer.setModel) + SDM.connectReader("macroName", + self.__macroDescriptionViewer.onMacroNameChanged) + mainwindow.createPanel(self.__macroDescriptionViewer, + 'MacroDescription', registerconfig=True, + permanent=True) + + # puts a doorOutput + self.__doorOutput = DoorOutput() + SDM.connectReader("doorOutputChanged", + self.__doorOutput.onDoorOutputChanged) + SDM.connectReader("doorInfoChanged", + self.__doorOutput.onDoorInfoChanged) + SDM.connectReader("doorWarningChanged", + self.__doorOutput.onDoorWarningChanged) + SDM.connectReader("doorErrorChanged", + self.__doorOutput.onDoorErrorChanged) + mainwindow.createPanel(self.__doorOutput, 'DoorOutput', + registerconfig=False, permanent=True) + + # puts doorDebug + self.__doorDebug = DoorDebug() + SDM.connectReader("doorDebugChanged", + self.__doorDebug.onDoorDebugChanged) + mainwindow.createPanel(self.__doorDebug, 'DoorDebug', + registerconfig=False, permanent=True) + + # puts doorResult + self.__doorResult = DoorResult(mainwindow) + SDM.connectReader("doorResultChanged", + self.__doorResult.onDoorResultChanged) + mainwindow.createPanel(self.__doorResult, 'DoorResult', + registerconfig=False, permanent=True) + + # puts sardanaEditor + # self.__sardanaEditor = SardanaEditor() + # SDM.connectReader("macroserverName", self.__sardanaEditor.setModel) + # mainwindow.createPanel(self.__sardanaEditor, 'SardanaEditor', + # registerconfig=False, permanent=True) + + # add panic button for aborting the door + text = "Panic Button: stops the pool (double-click for abort)" + self.doorAbortAction = mainwindow.jorgsBar.addAction( + Qt.QIcon("actions:process-stop.svg"), + text, self.__onDoorAbort) + + # store beginning of times as a datetime + self.__lastAbortTime = datetime.datetime(1, 1, 1) + + # store doubleclick interval as a timedelta + td = datetime.timedelta(0, 0, 1000 * Qt.qApp.doubleClickInterval()) + self.__doubleclickInterval = td + + def __onDoorAbort(self): + '''slot to be called when the abort action is triggered. + It sends stop command to the pools (or abort if the action + has been triggered twice in less than self.__doubleclickInterval + + .. note:: An abort command is always preceded by an stop command + ''' + # decide whether to send stop or abort + now = datetime.datetime.now() + if now - self.__lastAbortTime < self.__doubleclickInterval: + cmd = 'abort' + else: + cmd = 'stop' + + door = self.getModelObj() + + # abort the door + door.command_inout('abort') + # send stop/abort to all pools + pools = door.macro_server.getElementsOfType('Pool') + for pool in pools.values(): + self.info('Sending %s command to %s' % (cmd, pool.getFullName())) + try: + pool.getObj().command_inout(cmd) + except Exception: + self.info('%s command failed on %s', cmd, pool.getFullName(), + exc_info=1) + self.newShortMessage.emit("%s command sent to all pools" % cmd) + self.__lastAbortTime = now + + def createPanel(self, widget, name, **kwargs): + ''' Reimplemented from :class:`DynamicPlotManager` to delegate panel + management to the parent widget (a TaurusGui)''' + mainwindow = self.parent() + return mainwindow.createPanel(widget, name, **kwargs) + + def getPanelWidget(self, name): + ''' Reimplemented from :class:`DynamicPlotManager` to delegate panel + management to the parent widget (a TaurusGui)''' + mainwindow = self.parent() + return mainwindow.getPanel(name).widget() + + def removePanel(self, name): + ''' Reimplemented from :class:`DynamicPlotManager` to delegate panel + management to the parent widget (a TaurusGui)''' + mainwindow = self.parent() + mainwindow.removePanel(name) + + def removeTemporaryPanels(self, names=None): + '''Remove temporary panels managed by this widget''' + # for now, the only temporary panels are the plots + DynamicPlotManager.removePanels(self, names=names) + + +if __name__ == "__main__": + import sys + from taurus.qt.qtgui.application import TaurusApplication + + app = TaurusApplication() + + b = DynamicPlotManager(None) + + b.setModel('door/cp1/1') + + print('...') + sys.exit(app.exec_()) diff --git a/src/sardana/util/funcgenerator.py b/src/sardana/util/funcgenerator.py index 340595b9d9..f72bac46a7 100644 --- a/src/sardana/util/funcgenerator.py +++ b/src/sardana/util/funcgenerator.py @@ -64,6 +64,7 @@ def __init__(self, name="FunctionGenerator"): self._initial_domain = None self._active_domain = None self._position_event = threading.Event() + self._position = None self._initial_domain_in_use = None self._active_domain_in_use = None self._active_events = list() @@ -75,6 +76,7 @@ def __init__(self, name="FunctionGenerator"): self._direction = None self._condition = None self._id = None + self._start_fired = False def get_name(self): return self._name @@ -167,6 +169,7 @@ def start(self): self._stopped = False self._started = True self._position = None + self._start_fired = False self._position_event.clear() self._id = 0 self.fire_event(EventType("state"), State.Moving) @@ -211,6 +214,13 @@ def sleep(self, period): break time.sleep(nap) + def fire_start(self): + self.fire_event(EventType("start"), self._id) + self._start_fired = True + if self._id > 0: + msg = "start was fired with {0} delay".format(self._id) + self.warning(msg) + def wait_active(self): candidate = self.active_events[0] if self.initial_domain_in_use == SynchDomain.Time: @@ -244,6 +254,8 @@ def fire_active(self): else: break self._id += i + if not self._start_fired: + self.fire_start() self.fire_event(EventType("active"), self._id) self.active_events = self.active_events[i + 1:] self.passive_events = self.passive_events[i:] @@ -267,6 +279,11 @@ def wait_passive(self): def fire_passive(self): self.fire_event(EventType("passive"), self._id) self.set_passive_events(self.passive_events[1:]) + if len(self.passive_events) == 0: + self.fire_end() + + def fire_end(self): + self.fire_event(EventType("end"), self._id) def set_configuration(self, configuration): # make a copy since we may inject the initial time diff --git a/src/sardana/util/parser.py b/src/sardana/util/parser.py new file mode 100644 index 0000000000..79c8e6f708 --- /dev/null +++ b/src/sardana/util/parser.py @@ -0,0 +1,273 @@ +#!/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 . +## +############################################################################## + +"""This package contains macro parameters parsing utilities.""" + +__all__ = ["ParamParser"] + +import re +import collections + +# Token specification +PARAM = r"(?P[^\[\]\s]+)" +QUOTEDPARAM = r'"(?P.*?)(?.*?)'" +LPAREN = r"(?P\[)" +RPAREN = r"(?P\])" +WS = r"(?P\s+)" + +master_pat = re.compile("|".join([QUOTEDPARAM, SINGQUOTEDPARAM, PARAM, LPAREN, + RPAREN, WS])) + +# Tokenizer +Token = collections.namedtuple("Token", ["type", "value"]) + + +def generate_tokens(text): + scanner = master_pat.scanner(text) + for m in iter(scanner.match, None): + # quoted parameters must be returned without the quotes that's why we + # extract a given group, otherwise we would extract the whole match + group = (m.group("QUOTEDPARAM") + or m.group("SINGQUOTEDPARAM") + or m.group()) + tok = Token(m.lastgroup, group) + if tok.type != "WS": + yield tok + + +def is_repeat_param(param_def): + return isinstance(param_def["type"], list) + + +def is_repeat_param_single(param_def): + return len(param_def) == 1 + + +class ParseError(Exception): + pass + + +class UnrecognizedParamValue(ParseError): + pass + + +class ExcessParamValue(ParseError): + pass + + +class ParamParser: + """Implementation of a recursive descent parser. Use the ._accept() method + to test and accept the current lookahead token. Use the ._expect() + method to exactly match and discard the next token on the input + (or raise a SyntaxError if it doesn't match). + + Inspired on Python Cookbook 3 (chapter 2.19) + """ + + def __init__(self, params_def=None): + self._params_def = params_def + + def parse(self, text): + self.tokens = generate_tokens(text) + self.tok = None # Last symbol consumed + self.nexttok = None # Next symbol tokenized + self._advance() # Load first lookahead token + params = self._params() + self._end_check() + return params + + def _advance(self): + """Advance one token ahead""" + self.tok, self.nexttok = self.nexttok, next(self.tokens, None) + + def _accept(self, toktype): + """Test and consume the next token if it matches toktype""" + if self.nexttok and self.nexttok.type == toktype: + self._advance() + return True + else: + return False + + def _expect(self, toktype): + """Consume next token if it matches toktype or raise SyntaxError""" + if not self._accept(toktype): + raise SyntaxError("Expected " + toktype) + + # Grammar rules follow + + def _params(self, params_def=None, is_repeat=False): + """Interpret parameter values by iterating over generated tokens + according to parameters definition. + + It is used either at the macro level or a the repeat parameter + repetition level. + + :param params_def: parameters definition as used by the + :meth:`sardana.macroserver.msmetamacro.Parametrizable.get_parameter` + or by the + `attr:`sardana.taurus.core.tango.sardana.macro.MacroInfo.parameters` + :type params_def: list + :param end_check: whether to check if there are parameter values + exceeding parameters definition + :type end_check: bool + :return: parameter values + :rtype: list + """ + params_def = params_def or self._params_def + len_params_def = len(params_def) + params = [] + for param_idx, param_def in enumerate(params_def): + # no next tokens means that the string being parsed had finished + if self.nexttok is None: + break + if is_repeat_param(param_def): + is_last_param = False + if param_idx == len_params_def - 1: + is_last_param = True + repeat_param_def = param_def["type"] + param_value = self._repeat_param(repeat_param_def, + is_last_param) + else: + try: + param_value = self._param() + except UnrecognizedParamValue: + # this exception may occur if repeat is not complete - + # uses default values + if is_repeat: + return params + raise + params.append(param_value) + return params + + def _param(self): + """Interpret normal parameter value. Respect quotes for string + parameters. + + :return: parameter value + :rtype: str + """ + if self._accept("QUOTEDPARAM"): + # quoted parameters allows using quotes escaped by \\ + string = self.tok.value + string = string.replace('\\"', '"') + param = string + elif self._accept("SINGQUOTEDPARAM"): + param = self.tok.value + elif self._accept("PARAM"): + tok_value = self.tok.value + param = tok_value + else: + msg = "%s is not a valid param value" % self.tok.value + raise UnrecognizedParamValue(msg) + return param + + def _repeat_param(self, repeat_param_def, is_last_param): + """Interpret repeat parameter. + + Accepts repeat parameters using the following rules: + * enclosed in parenthesis + * non-enclosed in parenthesis multiple repetitions of the last repeat + parameter (can be single or multiple) + * non-enclosed in parenthesis one repetition of single repeat + parameter at arbitrary position + + :param repeat_param_def: repeat parameter definition + :type repeat_param_def: list + :param is_last_param: whether this repeat parameter is the last in the + definition + :type is_last_param: bool + :return: repeat parameter value + :rtype: list + """ + repeats = [] + + if self._accept("LPAREN"): + while True: + repeat = self._repeat(repeat_param_def) + if repeat is None: + break + repeats.append(repeat) + self._expect("RPAREN") + else: + single = is_repeat_param_single(repeat_param_def) + if is_last_param: + while True: + repeat = [] + for _ in repeat_param_def: + try: + param = self._param() + except UnrecognizedParamValue: + return repeats + if single: + repeat = param + else: + repeat.append(param) + repeats.append(repeat) + elif single: + param = self._param() + repeats = [param] + return repeats + + def _repeat(self, repeat_param_def): + """Interpret one repetition of the repeat parameter. + + :param repeat_param_def: repeat parameter definition + :type repeat_param_def: list + :return: repeat value + :rtype: list or None + """ + repeat = None + if self._accept("LPAREN"): + # empty brackets will be interpreted as a default value + if self._accept("RPAREN"): + repeat = [] + else: + repeat = self._params(repeat_param_def, is_repeat=True) + # repetitions of single repeat parameters are not enclosed + # in parenthesis so remove it + if is_repeat_param_single(repeat_param_def): + repeat = repeat[0] + self._expect("RPAREN") + else: + try: + repeat = self._param() + except UnrecognizedParamValue: + # no repeat found - return None + pass + return repeat + + def _end_check(self): + """Check if there are excessive tokens.""" + excess_tokens = "" + if len(self._params_def) == 0 and self.nexttok is not None: + excess_tokens += self.nexttok.value + while True: + self._advance() + if self.nexttok is None: + break + excess_tokens += self.nexttok.value + if len(excess_tokens) > 0: + raise ExcessParamValue("excess tokens are %s" % excess_tokens) diff --git a/src/sardana/util/test/test_funcgenerator.py b/src/sardana/util/test/test_funcgenerator.py index 3feb15dca2..b117fec21d 100644 --- a/src/sardana/util/test/test_funcgenerator.py +++ b/src/sardana/util/test/test_funcgenerator.py @@ -73,8 +73,10 @@ def __init__(self): self.init() def init(self): + self.start = False self.active_event_ids = list() self.passive_event_ids = list() + self.end = False def event_received(self, *args, **kwargs): _, type_, value = args @@ -83,6 +85,10 @@ def event_received(self, *args, **kwargs): self.active_event_ids.append(value) elif name == "passive": self.passive_event_ids.append(value) + elif name == "start": + self.start = True + elif name == "end": + self.end = True else: ValueError("wrong event type") @@ -122,9 +128,11 @@ def test_run_time(self): self.event.wait(100) active_event_ids = self.listener.active_event_ids active_event_ids_ok = range(0, 10) - msg = "Received active event ids: %s, expected: %s" % (active_event_ids, - active_event_ids_ok) + msg = "Received active event ids: %s, expected: %s" % ( + active_event_ids, active_event_ids_ok) self.assertListEqual(active_event_ids, active_event_ids_ok, msg) + self.assertTrue(self.listener.start, "Start event is missing") + self.assertTrue(self.listener.end, "End event is missing") def test_stop_time(self): self.func_generator.initial_domain = SynchDomain.Time diff --git a/src/sardana/util/test/test_parser.py b/src/sardana/util/test/test_parser.py new file mode 100644 index 0000000000..c8a4c77eac --- /dev/null +++ b/src/sardana/util/test/test_parser.py @@ -0,0 +1,833 @@ +#!/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 . +## +############################################################################## + +"""Tests for parser utilities.""" + +from taurus.external import unittest +from taurus.test import insertTest +from sardana.util.parser import ParamParser + + +pt0_params_def = [] + +pt1d_params_def = [ + { + "default_value": 99, + "description": "some bloody float", + "max": None, + "min": 1, + "name": "value", + "type": "Float" + } +] + +pt2_params_def = [ + { + "default_value": None, + "description": "some bloody motor", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + } +] +pt3_params_def = [ + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "value", + "max": None, + "min": 1, + "name": "position", + "type": "Float" + } + ] + } +] + +pt3d_params_def = [ + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": 21, + "description": "value", + "max": None, + "min": 1, + "name": "position", + "type": "Float" + } + ] + } +] + +pt4_params_def = [ + { + "default_value": None, + "description": "List of motors", + "max": None, + "min": 1, + "name": "motor_list", + "type": [ + { + "default_value": None, + "description": "motor name", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + } + ] + } +] + +pt5_params_def = [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "value", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + } +] + +pt6_params_def = [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "value", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + } +] + +pt7_params_def = [ + { + "default_value": None, + "description": "List of motor/position pairs", + "max": None, + "min": 1, + "name": "m_p_pair", + "type": [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": None, + "description": "Position to move to", + "max": None, + "min": 1, + "name": "position", + "type": "Float" + } + ] + } +] + +pt7d1_params_def = [ + { + "default_value": None, + "description": "List of motor/position pairs", + "max": None, + "min": 1, + "name": "m_p_pair", + "type": [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": 2, + "description": "Position to move to", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + } +] + +pt7d2_params_def = [ + { + "default_value": None, + "description": "List of motor/position pairs", + "max": None, + "min": 1, + "name": "m_p_pair", + "type": [ + { + "default_value": 'mot1', + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": 2, + "description": "Position to move to", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + } +] + +pt8_params_def = [ + { + "default_value": None, + "description": "List of motor/position pairs", + "max": None, + "min": 1, + "name": "m_p_pair", + "type": [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": None, + "description": "Position to move to", + "max": 2, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + } +] + +pt9_params_def = [ + { + "default_value": None, + "description": "List of motor/position pairs", + "max": None, + "min": 1, + "name": "m_p_pair", + "type": [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + { + "default_value": None, + "description": "Position to move to", + "max": 2, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + } +] + +pt10_params_def = [ + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "value", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + }, + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, +] + +pt11_params_def = [ + + { + "default_value": None, + "description": "Counter to count", + "max": None, + "min": 1, + "name": "counter", + "type": "ExpChannel" + }, + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "value", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + }, + ] + }, + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, +] + +pt12_params_def = [ + + { + "default_value": None, + "description": "List of values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "value", + "max": None, + "min": 1, + "name": "pos", + "type": "Float" + }, + ] + }, + { + "default_value": None, + "description": "List of Motors", + "max": None, + "min": 1, + "name": "motor_list", + "type": [ + { + "default_value": 'mot1', + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + }, + ] + }, +] + +pt13_params_def = [ + { + "default_value": None, + "description": "Motor groups", + "max": None, + "min": 1, + "name": "motor_group_list", + "type": [ + { + "default_value": None, + "description": "List of motors", + "max": None, + "min": 1, + "name": "motor list", + "type": [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + } + ] + } + ] + } +] + +pt14_params_def = [ + { + "default_value": None, + "description": "Motor groups", + "max": None, + "min": 1, + "name": "motor_group_list", + "type": [ + { + "default_value": None, + "description": "List of motors", + "max": None, + "min": 1, + "name": "motor list", + "type": [ + { + "default_value": None, + "description": "Motor to move", + "max": None, + "min": 1, + "name": "motor", + "type": "Motor" + } + ] + }, + { + "default_value": None, + "description": "Number", + "max": None, + "min": 1, + "name": "float", + "type": "Float" + } + ] + } +] + +extra1_params_def = [ + { + "default_value": None, + "description": "Parameter", + "max": None, + "min": 1, + "name": "param", + "type": "String" + }, + { + "default_value": None, + "description": "List of Scan files", + "max": None, + "min": 1, + "name": "ScanFiles List", + "type": [ + { + "default_value": None, + "description": "ScanFile", + "max": None, + "min": 1, + "name": "ScanFile", + "type": "String", + } + ] + } +] + +extra2_params_def = [ + + { + "default_value": None, + "description": "Value 1", + "max": None, + "min": 1, + "name": "value1", + "type": "Float" + }, + { + "default_value": None, + "description": "Value 2", + "max": None, + "min": 1, + "name": "value2", + "type": "float" + }, + { + "default_value": None, + "description": "List of Strings", + "max": None, + "min": 1, + "name": "string_list", + "type": [ + { + "default_value": None, + "description": "string", + "max": None, + "min": 1, + "name": "string", + "type": "String" + }, + ] + }, +] +extra3_params_def = [ + + { + "default_value": None, + "description": "param", + "max": None, + "min": 1, + "name": "param", + "type": "String" + }, + { + "default_value": None, + "description": "Value", + "max": None, + "min": 1, + "name": "value", + "type": "String" + }, +] + +extra4_params_def = [ + + { + "default_value": None, + "description": "value 1", + "max": None, + "min": 1, + "name": "value1", + "type": "Float" + }, + { + "default_value": None, + "description": "Value 2", + "max": None, + "min": 1, + "name": "value2", + "type": "Float" + }, +] + + +extra5_params_def = [ + + { + "default_value": None, + "description": "List of Motor and Values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "Motor", + "max": None, + "min": 1, + "name": "pos", + "type": "Motor" + }, + { + "default_value": None, + "description": "Position to move to", + "max": 2, + "min": 1, + "name": "pos", + "type": "Float" + } + ] + }, + { + "default_value": None, + "description": "Counter to use", + "max": None, + "min": 1, + "name": "counter", + "type": "ExpChan" + }, + { + "default_value": None, + "description": "Value", + "max": None, + "min": 1, + "name": "Value", + "type": "Float" + } +] +extra6_params_def = [ + + { + "default_value": None, + "description": "List of Values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "Value 1", + "max": None, + "min": 1, + "name": "value1", + "type": "Float" + }, + { + "default_value": None, + "description": "Value 2", + "max": None, + "min": 1, + "name": "value2", + "type": "Float" + } + ] + } +] + +extra7_params_def = [ + + { + "default_value": None, + "description": "value 1", + "max": None, + "min": 1, + "name": "value1", + "type": "Float" + }, + { + "default_value": None, + "description": "List of Values", + "max": None, + "min": 1, + "name": "numb_list", + "type": [ + { + "default_value": None, + "description": "Value 2.1", + "max": None, + "min": 1, + "name": "value21", + "type": "Float" + }, + { + "default_value": None, + "description": "Value 2.2", + "max": None, + "min": 1, + "name": "value22", + "type": "Float" + } + ] + } +] +extra8_params_def = [ + + { + "default_value": None, + "description": "value 1", + "max": None, + "min": 1, + "name": "value1", + "type": "Float" + }, + { + "default_value": None, + "description": "Value 2", + "max": None, + "min": 1, + "name": "value2", + "type": "Float" + }, +] + + +# parameters examples tests +@insertTest(helper_name="parse", params_def=pt0_params_def, + params_str="", params=[]) +@insertTest(helper_name="parse", params_def=pt1d_params_def, + params_str="1", params=["1"]) +@insertTest(helper_name="parse", params_def=pt1d_params_def, + params_str="", params=[]) +@insertTest(helper_name="parse", params_def=pt2_params_def, + params_str="mot1", params=["mot1"]) +@insertTest(helper_name="parse", params_def=pt3_params_def, + params_str="1 34 15", params=[["1", "34", "15"]]) +@insertTest(helper_name="parse", params_def=pt3_params_def, + params_str="[1 34 15]", params=[["1", "34", "15"]]) +@insertTest(helper_name="parse", params_def=pt3d_params_def, + params_str="1 34 15", params=[["1", "34", "15"]]) +@insertTest(helper_name="parse", params_def=pt3d_params_def, + params_str="[1 34 15]", params=[["1", "34", "15"]]) +@insertTest(helper_name="parse", params_def=pt3d_params_def, + params_str="[1 [] 15]", params=[["1", [], "15"]]) +@insertTest(helper_name="parse", params_def=pt4_params_def, + params_str="[mot1 mot2 mot3]", params=[["mot1", "mot2", "mot3"]]) +@insertTest(helper_name="parse", params_def=pt4_params_def, + params_str="mot1 mot2 mot3", params=[["mot1", "mot2", "mot3"]]) +@insertTest(helper_name="parse", params_def=pt5_params_def, + params_str="mot1 1 3", params=["mot1", ["1", "3"]]) +@insertTest(helper_name="parse", params_def=pt5_params_def, + params_str="mot1 [1 3]", params=["mot1", ["1", "3"]]) +@insertTest(helper_name="parse", params_def=pt6_params_def, + params_str="mot1 [1 34 1]", params=["mot1", ["1", "34", "1"]]) +@insertTest(helper_name="parse", params_def=pt6_params_def, + params_str="mot1 1 34 1", params=["mot1", ["1", "34", "1"]]) +@insertTest(helper_name="parse", params_def=pt7_params_def, + params_str="mot1 1 mot2 3", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7_params_def, + params_str="[[mot1 1] [mot2 3]]", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7d1_params_def, + params_str="[[mot1 1] [mot2 3]]", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7d1_params_def, + params_str="mot1 1 mot2 3", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7d1_params_def, + params_str="[[mot1] [mot2 3]]", + params=[[["mot1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7d2_params_def, + params_str="[[mot1 1] [mot2 3]]", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7d2_params_def, + params_str="mot1 1 mot2 3", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt7d2_params_def, + params_str="[[] [mot2 3] []]", + params=[[[], ["mot2", "3"], []]]) +@insertTest(helper_name="parse", params_def=pt8_params_def, + params_str="[[mot1 1] [mot2 3]]", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt8_params_def, + params_str="mot1 1 mot2 3", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt9_params_def, + params_str="[[mot1 1] [mot2 3]]", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt9_params_def, + params_str="mot1 1 mot2 3", + params=[[["mot1", "1"], ["mot2", "3"]]]) +@insertTest(helper_name="parse", params_def=pt10_params_def, + params_str="[1 3] mot1", params=[["1", "3"], "mot1"]) +@insertTest(helper_name="parse", params_def=pt10_params_def, + params_str="1 mot1", params=[["1"], "mot1"]) +@insertTest(helper_name="parse", params_def=pt11_params_def, + params_str="ct1 [1 3] mot1", params=["ct1", ["1", "3"], "mot1"]) +@insertTest(helper_name="parse", params_def=pt12_params_def, + params_str="[1 3 4] [mot1 mot2]", + params=[["1", "3", "4"], ["mot1", "mot2"]]) +@insertTest(helper_name="parse", params_def=pt13_params_def, + params_str="[[mot1 mot2] [mot3 mot4]]", + params=[[["mot1", "mot2"], ["mot3", "mot4"]]]) +@insertTest(helper_name="parse", params_def=pt14_params_def, + params_str="[[[mot1 mot2] 3] [[mot3] 5]]", + params=[[[["mot1", "mot2"], "3"], [["mot3"], "5"]]]) +# extra tests for complex parameter values +@insertTest(helper_name="parse", params_def=extra1_params_def, + params_str="ScanFile ['file.nxs' 'file.dat']", + params=["ScanFile", ["file.nxs", "file.dat"]]) +@insertTest(helper_name="parse", params_def=extra2_params_def, + params_str="2 3 ['Hello world!' 'How are you?']", + params=["2", "3", ["Hello world!", "How are you?"]]) +@insertTest(helper_name="parse", params_def=extra3_params_def, + params_str="ScanFile file.dat", + params=["ScanFile", "file.dat"]) +@insertTest(helper_name="parse", params_def=extra4_params_def, + params_str="'2 3'", params=["2 3"]) +@insertTest(helper_name="parse", params_def=extra5_params_def, + params_str="[[mot01 3][mot02 5]] ct01 999", + params=[[["mot01", "3"], ["mot02", "5"]], "ct01", "999"]) +@insertTest(helper_name="parse", params_def=extra6_params_def, + params_str="[[2 3][4 5]]", + params=[[["2", "3"], ["4", "5"]]]) +@insertTest(helper_name="parse", params_def=extra7_params_def, + params_str="1 [2 3]", + params=["1", ["2", "3"]]) +@insertTest(helper_name="parse", params_def=extra8_params_def, + params_str="2 3", params=["2", "3"]) +class ParamParserTestCase(unittest.TestCase): + """Unit tests for ParamParser class. Mainly based on macro examples for + parameters definition. + """ + def parse(self, params_def, params_str, params): + """Helper method to test parameters parsing. To be used with + insertTest decorator. + """ + p = ParamParser(params_def) + result = p.parse(params_str) + msg = "Parsing failed (result: %r; expected: %r)" % \ + (result, params) + self.assertListEqual(result, params, msg)