From a9c28dd9a38803baa63a1e813c8f2777926022a8 Mon Sep 17 00:00:00 2001 From: Grzegorz Kowalski Date: Mon, 21 Oct 2019 11:42:39 +0200 Subject: [PATCH 01/62] do not cast macro id to int --- src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py | 1 - .../qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py index be2f222a07..6cd671a865 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/macroexecutor.py @@ -936,7 +936,6 @@ def onMacroStatusUpdated(self, data): "range"], data["step"], data["id"] if id is None: return - id = int(id) if id != self.macroId(): return macroName = macro.name diff --git a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py index 97becda18d..be7fc7b350 100644 --- a/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py +++ b/src/sardana/taurus/qt/qtgui/extra_macroexecutor/sequenceeditor/sequenceeditor.py @@ -728,7 +728,6 @@ def onMacroStatusUpdated(self, data): "range"], data["step"], data["id"] if id is None: return - id = int(id) if not id in self.macroIds(): return macroName = macro.name From 7a9775eb2b41d153266221955ca5878fb51d7919 Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Wed, 17 Jun 2020 12:26:03 +0200 Subject: [PATCH 02/62] Add custom user to scan --- src/sardana/macroserver/scan/gscan.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py index 6330825a2a..c61dc17019 100644 --- a/src/sardana/macroserver/scan/gscan.py +++ b/src/sardana/macroserver/scan/gscan.py @@ -640,11 +640,16 @@ def _setupEnvironment(self, additional_env): serialno = 1 self.macro.setEnv("ScanID", serialno) + try: + user = self.macro.getEnv("ScanUser") + except UnknownEnv: + user = USER_NAME + env = ScanDataEnvironment( {'serialno': serialno, # TODO: this should be got from # self.measurement_group.getChannelsInfo() - 'user': USER_NAME, + 'user': user, 'title': self.macro.getCommand()}) # Initialize the data_desc list (and add the point number column) From 29f42d9fd61923589008b60629499d3cda51d10a Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Fri, 7 Aug 2020 14:19:29 +0100 Subject: [PATCH 03/62] Separate concerns in qt scan manager objects --- .../qt/qtgui/extra_sardana/showscanonline.py | 4 +- .../qt/qtgui/macrolistener/macrolistener.py | 42 ++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py index 102eab1aae..0a99f33673 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py @@ -29,6 +29,7 @@ import click +from taurus.external.qt import Qt from taurus.qt.qtgui.taurusgui import TaurusGui from sardana.taurus.qt.qtgui.macrolistener import (DynamicPlotManager, assertPlotAvailability) @@ -37,7 +38,8 @@ class ShowScanOnline(DynamicPlotManager): def __init__(self, parent): - DynamicPlotManager.__init__(self, parent) + DynamicPlotManager.__init__(self, parent=parent) + Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') def onExpConfChanged(self, expconf): DynamicPlotManager.onExpConfChanged(self, expconf) diff --git a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py index f06fbbe6a3..f23d54d7b6 100644 --- a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py +++ b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py @@ -177,16 +177,15 @@ def _end_update(self): self._timer = None -class DynamicPlotManager(Qt.QObject, TaurusBaseComponent): - '''This is a manager of plots related to the execution of macros. +class PlotManager(Qt.QObject, TaurusBaseComponent): + ''' + This is a manager of plots related to the execution of macros. It dynamically creates/removes plots according to the configuration made by an ExperimentConfiguration widget. Currently it supports only 1D scan trends (2D scans are only half-baked) - To use it simply instantiate it and pass it a door name as a model. You may - want to call :meth:`onExpConfChanged` to update the configuration being - used. + To use it simply instantiate it and pass it a door name as a model. ''' plots_available = pyqtgraph is not None @@ -196,16 +195,13 @@ class DynamicPlotManager(Qt.QObject, TaurusBaseComponent): Single = 'single' # each curve has its own plot XAxis = 'x-axis' # group curves with same X-Axis - def __init__(self, parent=None): + def __init__(self, plot=None, parent=None): Qt.QObject.__init__(self, parent) TaurusBaseComponent.__init__(self, self.__class__.__name__) self._group_mode = self.XAxis - Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') - self._plot = MultiPlotWidget() - self.createPanel( - self._plot, 'Scan plot', registerconfig=False, permanent=False) + self.plot = plot or MultiPlotWidget() def setGroupMode(self, group): assert group in (self.Single, self.XAxis) @@ -329,7 +325,7 @@ def prepare(self, data_desc): raise NotImplementedError nb_points = data.get('total_scan_intervals', 2**16 - 1) + 1 - self._plot.prepare(plots, nb_points=nb_points) + self.plot.prepare(plots, nb_points=nb_points) # build status message serialno = 'Scan #{}'.format(data.get('serialno', '?')) @@ -350,18 +346,35 @@ def prepare(self, data_desc): def newPoint(self, point): data = point['data'] - self._plot.onNewPoint(data) + self.plot.onNewPoint(data) point_nb = 'Point #{}'.format(data['point_nb']) msg = self.message_template.format(progress=point_nb) self.newShortMessage.emit(msg) def end(self, end_data): data = end_data['data'] - self._plot.onEnd(data) + self.plot.onEnd(data) progress = 'Ended {}'.format(data['endtime']) msg = self.message_template.format(progress=progress) self.newShortMessage.emit(msg) + +class DynamicPlotManager(PlotManager): + '''This is a manager of plots related to the execution of macros. + It dynamically creates/removes plots according to the configuration made by + an ExperimentConfiguration widget. + + Currently it supports only 1D scan trends (2D scans are only half-baked) + + To use it simply instantiate it and pass it a door name as a model. + ''' + + def __init__(self, *args, **kwargs): + PlotManager.__init__(self, *args, **kwargs) + self.__panels = {} + self.createPanel( + self.plot, 'Scan plot', registerconfig=False, permanent=False) + def createPanel(self, widget, name, **kwargs): '''Creates a "panel" from a widget. In this basic implementation this means that the widgets is shown as a non-modal top window @@ -414,12 +427,13 @@ class MacroBroker(DynamicPlotManager): def __init__(self, parent): '''Passing the parent object (the main window) is mandatory''' - DynamicPlotManager.__init__(self, parent) + DynamicPlotManager.__init__(self, parent=parent) self._createPermanentPanels() # connect the broker to shared data Qt.qApp.SDM.connectReader("doorName", self.setModel) + Qt.qApp.SDM.connectWriter("shortMessage", self, 'newShortMessage') def setModel(self, doorname): ''' Reimplemented from :class:`DynamicPlotManager`.''' From 7cc9ffda0077dd5d38de9ce8212af0c5079bdc6c Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Fri, 7 Aug 2020 15:33:00 +0100 Subject: [PATCH 04/62] Add ScanPlotWidget and ScanPlotWindow --- .../qt/qtgui/extra_sardana/showscanonline.py | 43 +++++++++++++++---- .../qt/qtgui/macrolistener/macrolistener.py | 6 ++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py index 0a99f33673..326318810b 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py @@ -23,16 +23,43 @@ ## ############################################################################## -"""This module contains a taurus ShowScanOnline widget.""" +""" +This module contains a taurus ShowScanWidget, ShowScanWindow and ShowScanOnline +widgets. +""" -__all__ = ["ShowScanOnline"] +__all__ = ["ScanPlotWidget", "ScanPlotWindow", "ShowScanOnline"] import click from taurus.external.qt import Qt from taurus.qt.qtgui.taurusgui import TaurusGui -from sardana.taurus.qt.qtgui.macrolistener import (DynamicPlotManager, - assertPlotAvailability) +from sardana.taurus.qt.qtgui.macrolistener import ( + MultiPlotWidget, PlotManager, DynamicPlotManager, assertPlotAvailability +) + + +class ScanPlotWidget(MultiPlotWidget): + + def __init__(self, parent=None): + super().__init__(parent) + self.manager = PlotManager(self) + self.setModel = self.manager.setModel + self.setGroupMode = self.manager.setGroupMode + + +class ScanPlotWindow(Qt.QMainWindow): + + def __init__(self, parent=None): + super().__init__() + plot_widget = ScanPlotWidget(parent=self) + self.setCentralWidget(plot_widget) + self.plotWidget = self.centralWidget + self.setModel = plot_widget.setModel + self.setGroupMode = plot_widget.setGroupMode + sbar = self.statusBar() + sbar.showMessage("Ready!") + plot_widget.manager.newShortMessage.connect(sbar.showMessage) class ShowScanOnline(DynamicPlotManager): @@ -108,12 +135,10 @@ def main(group, taurus_log_level, door): assertPlotAvailability() - gui = TaurusGuiLite() - - widget = ShowScanOnline(gui) - widget.setModel(door) + widget = ScanPlotWindow() widget.setGroupMode(group) - gui.show() + widget.setModel(door) + widget.show() return app.exec_() diff --git a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py index f23d54d7b6..1a2e7a07c2 100644 --- a/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py +++ b/src/sardana/taurus/qt/qtgui/macrolistener/macrolistener.py @@ -56,7 +56,10 @@ from sardana.taurus.core.tango.sardana import PlotType -__all__ = ['MacroBroker', 'DynamicPlotManager', 'assertPlotAvailability'] +__all__ = [ + 'MultiPlotWidget', 'MacroBroker', 'PlotManager', 'DynamicPlotManager', + 'assertPlotAvailability' +] __docformat__ = 'restructuredtext' @@ -91,6 +94,7 @@ class MultiPlotWidget(Qt.QWidget): def __init__(self, parent=None): super().__init__(parent) layout = Qt.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) self.win = pyqtgraph.GraphicsLayoutWidget() layout.addWidget(self.win) self._plots = {} From 868e10a902e16d93567c802a8600d4f198cef5fa Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Fri, 7 Aug 2020 17:52:09 +0100 Subject: [PATCH 05/62] Add ScanInfoForm and ScanPointForm --- .../qt/qtgui/extra_sardana/showscanonline.py | 186 +++++++++++++++++- .../qt/qtgui/extra_sardana/ui/ScanInfoForm.ui | 141 +++++++++++++ .../qtgui/extra_sardana/ui/ScanPointForm.ui | 52 +++++ .../qt/qtgui/extra_sardana/ui/ScanWindow.ui | 150 ++++++++++++++ 4 files changed, 525 insertions(+), 4 deletions(-) create mode 100644 src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui create mode 100644 src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui create mode 100644 src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py index 326318810b..44d7fbeaa7 100644 --- a/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/showscanonline.py @@ -28,17 +28,172 @@ widgets. """ -__all__ = ["ScanPlotWidget", "ScanPlotWindow", "ShowScanOnline"] +__all__ = [ + "ScanInfoForm", "ScanPointForm", "ScanPlotWidget", + "ScanPlotWindow", "ScanWindow", "ShowScanOnline" +] import click +import pkg_resources -from taurus.external.qt import Qt +from taurus.external.qt import Qt, uic +from taurus.qt.qtgui.base import TaurusBaseWidget from taurus.qt.qtgui.taurusgui import TaurusGui from sardana.taurus.qt.qtgui.macrolistener import ( MultiPlotWidget, PlotManager, DynamicPlotManager, assertPlotAvailability ) +def set_text(label, field=None, data=None, default='---'): + if field is None and data is None: + value = default + elif field is None: + value = data + elif data is None: + value = field + else: + value = data.get(field, default) + if isinstance(value, (tuple, list)): + value = ', '.join(value) + elif isinstance(value, float): + value = '{:8.4f}'.format(value) + else: + value = str(value) + if len(value) > 60: + value = '...{}'.format(value[-57:]) + label.setText(value) + + +def resize_form(form, new_size): + layout = form.layout() + curr_size = layout.rowCount() + nb = new_size - curr_size + while nb > 0: + layout.addRow(Qt.QLabel(), Qt.QLabel()) + nb -= 1 + while nb < 0: + layout.removeRow(layout.rowCount() - 1) + nb += 1 + + +def fill_form(form, fields, offset=0): + resize_form(form, len(fields) + offset) + layout = form.layout() + result = [] + for row, field in enumerate(fields): + label, value = field + w_item = layout.itemAt(row + offset, Qt.QFormLayout.LabelRole) + w_label = w_item.widget() + set_text(w_label, label) + w_item = layout.itemAt(row + offset, Qt.QFormLayout.FieldRole) + w_field = w_item.widget() + set_text(w_field, value) + result.append((w_label, w_field)) + return result + + +def load_scan_info_form(widget): + ui_name = pkg_resources.resource_filename(__package__ + '.ui', + 'ScanInfoForm.ui') + uic.loadUi(ui_name, baseinstance=widget) + return widget + + +class ScanInfoForm(Qt.QWidget, TaurusBaseWidget): + + def __init__(self, parent=None): + super().__init__(parent) + load_scan_info_form(self) + + def setModel(self, doorname): + super().setModel(doorname) + if not doorname: + return + door = self.getModelObj() + door.recordDataUpdated.connect(self.onRecordDataUpdated) + + def onRecordDataUpdated(self, record_data): + data = record_data[1] + handler = self.event_handler.get(data.get("type")) + handler and handler(self, data['data']) + + def onStart(self, meta): + set_text(self.title_value, 'title', meta) + set_text(self.scan_nb_value, 'serialno', meta) + set_text(self.start_value, 'starttime', meta) + set_text(self.end_value, 'endtime', meta) + set_text(self.status_value, 'Running') + + directory = meta.get('scandir', '') + self.directory_groupbox.setEnabled(True if directory else False) + self.directory_groupbox.setTitle('Directory: {}'.format(directory)) + files = meta.get('scanfile', ()) + if isinstance(files, str): + files = files, + elif files is None: + files = () + files = [('File:', filename) for filename in files] + fill_form(self.directory_groupbox, files) + + def onEnd(self, meta): + set_text(self.end_value, 'endtime', meta) + set_text(self.status_value, 'Finished') + + event_handler = { + "data_desc": onStart, + "record_end": onEnd + } + + +def load_scan_point_form(widget): + ui_name = pkg_resources.resource_filename(__package__ + '.ui', + 'ScanPointForm.ui') + uic.loadUi(ui_name, baseinstance=widget) + return widget + + +class ScanPointForm(Qt.QWidget, TaurusBaseWidget): + + def __init__(self, parent=None): + super().__init__(parent) + load_scan_point_form(self) + self._in_scan = False + + def setModel(self, doorname): + super().setModel(doorname) + if not doorname: + return + door = self.getModelObj() + door.recordDataUpdated.connect(self.onRecordDataUpdated) + + def onRecordDataUpdated(self, record_data): + data = record_data[1] + handler = self.event_handler.get(data.get("type")) + handler and handler(self, data['data']) + + def onStart(self, meta): + set_text(self.scan_nb_value, 'serialno', meta) + cols = meta['column_desc'] + col_labels = [(c['label']+':', '') for c in cols] + fields = fill_form(self, col_labels, 1) + self.fields = {col['name']: field for col, field in zip(cols, fields)} + self._in_scan = True + + def onPoint(self, point): + if self._in_scan: + for name, value in point.items(): + set_text(self.fields[name][1], value) + + def onEnd(self, meta): + self._in_scan = False + + event_handler = { + "data_desc": onStart, + "record_data": onPoint, + "record_end": onEnd + } + + class ScanPlotWidget(MultiPlotWidget): def __init__(self, parent=None): @@ -62,6 +217,29 @@ def __init__(self, parent=None): plot_widget.manager.newShortMessage.connect(sbar.showMessage) +def load_scan_window(widget): + ui_name = pkg_resources.resource_filename(__package__ + '.ui', + 'ScanWindow.ui') + uic.loadUi(ui_name, baseinstance=widget) + return widget + + +class ScanWindow(Qt.QMainWindow): + + def __init__(self, parent=None): + super().__init__() + load_scan_window(self) + sbar = self.statusBar() + sbar.showMessage("Ready!") + self.plot_widget.manager.newShortMessage.connect(sbar.showMessage) + + def setModel(self, model): + self.plot_widget.setModel(model) + self.info_form.setModel(model) + self.point_form.setModel(model) + + + class ShowScanOnline(DynamicPlotManager): def __init__(self, parent): @@ -135,8 +313,8 @@ def main(group, taurus_log_level, door): assertPlotAvailability() - widget = ScanPlotWindow() - widget.setGroupMode(group) + widget = ScanWindow() + widget.plot_widget.setGroupMode(group) widget.setModel(door) widget.show() return app.exec_() diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui new file mode 100644 index 0000000000..3784123163 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanInfoForm.ui @@ -0,0 +1,141 @@ + + + Tiago Coutinho + scan_info_form + + + + 0 + 0 + 300 + 169 + + + + Form + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 3 + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Scan #: + + + + + + + --- + + + + + + + Title: + + + + + + + --- + + + + + + + Start: + + + + + + + --- + + + + + + + End: + + + + + + + --- + + + + + + + Status: + + + + + + + --- + + + + + + + Directory:--- + + + + 6 + + + 3 + + + 6 + + + 6 + + + 6 + + + 6 + + + + + + + + + + diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui new file mode 100644 index 0000000000..45056f10f3 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanPointForm.ui @@ -0,0 +1,52 @@ + + + Form + + + + 0 + 0 + 400 + 291 + + + + Form + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + 3 + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Scan #: + + + + + + + + + + + diff --git a/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui new file mode 100644 index 0000000000..9c011166d6 --- /dev/null +++ b/src/sardana/taurus/qt/qtgui/extra_sardana/ui/ScanWindow.ui @@ -0,0 +1,150 @@ + + + MainWindow + + + + 0 + 0 + 891 + 600 + + + + + + 3 + + + 0 + + + 6 + + + 6 + + + 6 + + + + + + + + + + Scan point + + + 2 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 250 + 0 + + + + QFrame::NoFrame + + + 0 + + + true + + + + + 0 + 0 + 250 + 240 + + + + + + + + + + + Scan information + + + 2 + + + + + 250 + 0 + + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 232 + 276 + + + + + + + + + + + + ScanPointForm + QWidget +
sardana.taurus.qt.qtgui.extra_sardana.showscanonline
+ 1 +
+ + ScanInfoForm + QWidget +
sardana.taurus.qt.qtgui.extra_sardana.showscanonline
+ 1 +
+ + ScanPlotWidget + QWidget +
sardana.taurus.qt.qtgui.extra_sardana.showscanonline
+ 1 +
+
+ + +
From 03825e6f812a3f1b75d527c83d0dfeefd57a9dc0 Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Mon, 28 Sep 2020 16:37:13 +0200 Subject: [PATCH 06/62] Fix macro function default settings Make sure we return the exact same default settings as the Macro class (ex: tools like the sequencer GUI expect hints to be a dict) --- src/sardana/macroserver/macro.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py index f062901a95..5013e5d42b 100644 --- a/src/sardana/macroserver/macro.py +++ b/src/sardana/macroserver/macro.py @@ -391,13 +391,24 @@ def my_macro1(self): def where_moveable(self, moveable): self.output("Moveable %s is at %s", moveable.getName(), moveable.getPosition())""" + param_def = [] + result_def = [] + env = () + hints = {} + interactive = False + def __init__(self, param_def=None, result_def=None, env=None, hints=None, interactive=None): - self.param_def = param_def - self.result_def = result_def - self.env = env - self.hints = hints - self.interactive = interactive + if param_def is not None: + self.param_def = param_def + if result_def is not None: + self.result_def = result_def + if env is not None: + self.env = env + if hints is not None: + self.hints = hints + if interactive is not None: + self.interactive = interactive def __call__(self, fn): fn.macro_data = {} From 0925e4209d0cc22a7cd3d22f4b9f915fcb984fab Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Tue, 29 Sep 2020 16:11:43 +0200 Subject: [PATCH 07/62] Fix double unit shown in motor widget --- src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py b/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py index a31d5db83c..b2ec6f8b48 100644 --- a/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py +++ b/src/sardana/taurus/qt/qtgui/extra_pool/poolmotor.py @@ -595,6 +595,7 @@ def __init__(self, parent=None, designMode=False): self.layout().addLayout(limits_layout, 0, 0) self.lbl_read = TaurusLabel() + self.lbl_read.setFgRole('rvalue.magnitude') self.lbl_read.setBgRole('quality') self.lbl_read.setSizePolicy(Qt.QSizePolicy( Qt.QSizePolicy.Expanding, Qt.QSizePolicy.Fixed)) From 7d5bf998a3ba9367d6f0db5188019423c2784b9a Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 14 Oct 2020 11:40:43 +0200 Subject: [PATCH 08/62] Allow to programmatically disable *deterministic scan* optimization Deterministic scan optimization of measurement is not compatible with attaching measurements on the same measurement group as hooks. Add deterministic_scan property to allow disabling this optimization. Fixes #1426 --- src/sardana/macroserver/scan/gscan.py | 35 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py index eacd779395..466050eda1 100644 --- a/src/sardana/macroserver/scan/gscan.py +++ b/src/sardana/macroserver/scan/gscan.py @@ -1058,24 +1058,43 @@ def do_restore(self): class SScan(GScan): """Step scan""" + def __init__(self, macro, generator=None, moveables=[], env={}, + constraints=[], extrainfodesc=[]): + GScan.__init__(self, macro, generator=generator, moveables=moveables, + env=env, constraints=constraints, + extrainfodesc=extrainfodesc) + self._deterministic_scan = None + + @property + def deterministic_scan(self): + if self._deterministic_scan is None: + macro = self.macro + if hasattr(macro, "nb_points") and hasattr(macro, "integ_time"): + self._deterministic_scan = True + else: + self._deterministic_scan = False + return self._deterministic_scan + + @deterministic_scan.setter + def deterministic_scan(self, value): + self._deterministic_scan = value + def scan_loop(self): lstep = None macro = self.macro scream = False - self._deterministic_scan = False if hasattr(macro, "nb_points"): nb_points = float(macro.nb_points) - if hasattr(macro, "integ_time"): - integ_time = macro.integ_time - self.measurement_group.putIntegrationTime(integ_time) - self.measurement_group.setNbStarts(nb_points) - self.measurement_group.prepare() - self._deterministic_scan = True scream = True else: yield 0.0 + if self.deterministic_scan: + self.measurement_group.putIntegrationTime(macro.integ_time) + self.measurement_group.setNbStarts(macro.nb_points) + self.measurement_group.prepare() + self._sum_motion_time = 0 self._sum_acq_time = 0 @@ -1157,7 +1176,7 @@ def stepUp(self, n, step, lstep): # Acquire data self.debug("[START] acquisition") try: - if self._deterministic_scan: + if self.deterministic_scan: state, data_line = mg.count_raw() else: state, data_line = mg.count(integ_time) From 2de301b9a2ea04043c6920579254fb5a53b3adbf Mon Sep 17 00:00:00 2001 From: Jordi Andreu Date: Thu, 15 Oct 2020 17:02:41 +0200 Subject: [PATCH 09/62] Fix delay description in position domain Add the missing delay parameter in position domain in the CTScan. --- src/sardana/macroserver/scan/gscan.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py index 6330825a2a..8b54144e7b 100644 --- a/src/sardana/macroserver/scan/gscan.py +++ b/src/sardana/macroserver/scan/gscan.py @@ -2382,8 +2382,10 @@ def _go_through_waypoints(self): initial_position = start total_time = abs(total_position) / path.max_vel delay_time = path.max_vel_time + delay_position = start - path.initial_user_pos synch = [ - {SynchParam.Delay: {SynchDomain.Time: delay_time}, + {SynchParam.Delay: {SynchDomain.Time: delay_time, + SynchDomain.Position: delay_position}, SynchParam.Initial: {SynchDomain.Position: initial_position}, SynchParam.Active: {SynchDomain.Position: active_position, SynchDomain.Time: active_time}, From 0b2ac1550329cba94e0500e7d2d8d040dd9ec8b6 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 6 Nov 2020 13:28:29 +0100 Subject: [PATCH 10/62] Implement per measurement preparation for trigger/gate Trigger/gate elements could be prepared for multiple starts in a deterministic scan and benefit from it. - Add PrepareOne() to TriggerGate controller. - Call preparation methods in PoolAcquision.preapre() - Implement empty `PrepareOne` in DummyTriggerGateController Implements #1432 --- src/sardana/pool/controller.py | 12 ++++++++++++ src/sardana/pool/poolacquisition.py | 12 ++++++++++++ .../poolcontrollers/DummyTriggerGateController.py | 3 +++ 3 files changed, 27 insertions(+) diff --git a/src/sardana/pool/controller.py b/src/sardana/pool/controller.py index fc24d05030..4895651f8f 100644 --- a/src/sardana/pool/controller.py +++ b/src/sardana/pool/controller.py @@ -912,6 +912,18 @@ class TriggerGateController(Controller, Synchronizer, Stopable, Startable): def __init__(self, inst, props, *args, **kwargs): Controller.__init__(self, inst, props, *args, **kwargs) + # TODO: Implement a Preparable interface and move this method + # and the Loadable.PrepareOne() there. + def PrepareOne(self, nb_starts): + """**Controller API**. Override if necessary. + Called to prepare the trigger/gate axis with the measurement + parameters. + Default implementation does nothing. + + :param int nb_starts: number of starts + """ + pass + class ZeroDController(Controller, Readable, Stopable): """Base class for a 0D controller. Inherit from this class to diff --git a/src/sardana/pool/poolacquisition.py b/src/sardana/pool/poolacquisition.py index 224fb6ce1f..9e66fdcb79 100644 --- a/src/sardana/pool/poolacquisition.py +++ b/src/sardana/pool/poolacquisition.py @@ -499,6 +499,9 @@ def prepare(self, config, acq_mode, value, synch_description=None, config.changed = False + # Call synchronizer controllers prepare method + self._prepare_synch_ctrls(ctrls_synch, nb_starts) + # Call hardware and software start controllers prepare method ctrls = ctrls_hw + ctrls_sw_start self._prepare_ctrls(ctrls, value, repetitions, latency, @@ -510,6 +513,7 @@ def prepare(self, config, acq_mode, value, synch_description=None, self._prepare_ctrls(ctrls_sw, value, repetitions, latency, nb_starts) + @staticmethod def _prepare_ctrls(ctrls, value, repetitions, latency, nb_starts): for ctrl in ctrls: @@ -518,6 +522,14 @@ def _prepare_ctrls(ctrls, value, repetitions, latency, nb_starts): pool_ctrl.ctrl.PrepareOne(axis, value, repetitions, latency, nb_starts) + @staticmethod + def _prepare_synch_ctrls(ctrls, nb_starts): + for ctrl in ctrls: + for chn in ctrl.get_channels(): + axis = chn.axis + pool_ctrl = ctrl.element + pool_ctrl.ctrl.PrepareOne(axis, nb_starts) + def is_running(self): """Checks if acquisition is running. diff --git a/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py b/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py index f2795057b6..bd746ed8ac 100644 --- a/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py +++ b/src/sardana/pool/poolcontrollers/DummyTriggerGateController.py @@ -72,6 +72,9 @@ def StateOne(self, axis): print(e) return sta, status + def PrepareOne(self, axis, nb_starts): + self._log.debug('PrepareOne(%d): entering...' % axis) + def PreStartAll(self): pass From 8759ea1401c7e4984df988461d8a61d2b12d710f Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 6 Nov 2020 14:02:55 +0100 Subject: [PATCH 11/62] Document "How To Trigger/Gate" PrepareOne() --- doc/source/devel/api/api_controller.rst | 1 + doc/source/devel/api/sardana/pool/controller.rst | 12 ++++++++++++ .../howto_triggergatecontroller.rst | 12 ++++++++++++ doc/source/devel/howto_macros/scan_framework.rst | 1 + 4 files changed, 26 insertions(+) diff --git a/doc/source/devel/api/api_controller.rst b/doc/source/devel/api/api_controller.rst index f77e55bff9..be81875ddf 100644 --- a/doc/source/devel/api/api_controller.rst +++ b/doc/source/devel/api/api_controller.rst @@ -13,6 +13,7 @@ Controller API reference * :class:`ZeroDController` - 0D controller API * :class:`PseudoMotorController` - PseudoMotor controller API * :class:`PseudoCounterController` - PseudoCounter controller API + * :class:`TriggerGateController` - Trigger/Gate controller API * :class:`IORegisterController` - IORegister controller API .. _sardana-controller-data-type: diff --git a/doc/source/devel/api/sardana/pool/controller.rst b/doc/source/devel/api/sardana/pool/controller.rst index 4d0f9c79df..9377423f70 100644 --- a/doc/source/devel/api/sardana/pool/controller.rst +++ b/doc/source/devel/api/sardana/pool/controller.rst @@ -44,6 +44,7 @@ * :class:`OneDController` * :class:`TwoDController` * :class:`PseudoCounterController` + * :class:`TriggerGateController` * :class:`IORegisterController` @@ -216,6 +217,17 @@ Pseudo Counter Controller API :undoc-members: +Trigger/Gate Controller API +--------------------------- + +.. inheritance-diagram:: TriggerGateController + :parts: 1 + +.. autoclass:: TriggerGateController + :show-inheritance: + :members: + :undoc-members: + IO Register Controller API ---------------------------- diff --git a/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst b/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst index c1516526f8..0b89a2e1c1 100644 --- a/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst +++ b/doc/source/devel/howto_controllers/howto_triggergatecontroller.rst @@ -89,6 +89,18 @@ The state should be a member of :obj:`~sardana.sardanadefs.State` (For backward compatibility reasons, it is also supported to return one of :class:`PyTango.DevState`). The status could be any string. +.. _sardana-TriggerGateController-howto-prepare: + +Prepare for measurement +~~~~~~~~~~~~~~~~~~~~~~~ + +To prepare a trigger for a measurement you can use the +:meth:`~sardana.pool.controller.TriggerGateController.PrepareOne` method which +receives as an argument the number of starts of the whole measurement. +This information may be used to prepare the hardware for generating +multiple events (triggers or gates) in a complex measurement +e.g. :ref:`sardana-macros-scanframework-determscan`. + .. _sardana-TriggerGateController-howto-load: Load synchronization description diff --git a/doc/source/devel/howto_macros/scan_framework.rst b/doc/source/devel/howto_macros/scan_framework.rst index 23a2a5db7d..43567fac30 100644 --- a/doc/source/devel/howto_macros/scan_framework.rst +++ b/doc/source/devel/howto_macros/scan_framework.rst @@ -195,6 +195,7 @@ the most basic features of a continuous scan:: :: (with more elaborated waypoint generator), see the code of :class:`~sardana.macroserver.macros.scan.meshc` +.. _sardana-macros-scanframework-determscan: Deterministic scans ------------------- From 45dc9d224461a93784c6f61514e884f43663a1d4 Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Wed, 11 Nov 2020 13:00:56 +0100 Subject: [PATCH 12/62] add fix for a parameter of addData and starttime value --- src/sardana/macroserver/scan/test/helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sardana/macroserver/scan/test/helper.py b/src/sardana/macroserver/scan/test/helper.py index d3bdefb8f7..b4fa2007bc 100644 --- a/src/sardana/macroserver/scan/test/helper.py +++ b/src/sardana/macroserver/scan/test/helper.py @@ -12,7 +12,7 @@ import time -from datetime import date +import datetime import threading import numpy import os @@ -47,7 +47,7 @@ def run(self): if skip: continue time.sleep(t) - _dict = dict(data=v, index=idx, label=self.name) + _dict = dict(value=v, index=idx, label=self.name) self.scan_data.addData(_dict) def get_obj(self): @@ -69,7 +69,7 @@ def createScanDataEnvironment(columns, scanDir='/tmp/', env['ScanFile'] = scanFile env['total_scan_intervals'] = -1.0 - today = date.today() + today = datetime.datetime.fromtimestamp(time.time()) env['datetime'] = today env['starttime'] = today env['endtime'] = today From a6fbcbefdf8990a9522f112fca39fd610cba3908 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 6 Nov 2020 14:51:25 +0100 Subject: [PATCH 13/62] Execute per measurement preparation in mesh Per measurement preparation in deterministic scans is done based on the integ_time and nb_points attribute presence in the macro obj. Add nb_points attribute to the mesh macro. --- src/sardana/macroserver/macros/scan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sardana/macroserver/macros/scan.py b/src/sardana/macroserver/macros/scan.py index 57e9aa22fa..0f48477711 100644 --- a/src/sardana/macroserver/macros/scan.py +++ b/src/sardana/macroserver/macros/scan.py @@ -709,6 +709,7 @@ def prepare(self, m1, m1_start_pos, m1_final_pos, m1_nr_interv, self.starts = numpy.array([m1_start_pos, m2_start_pos], dtype='d') self.finals = numpy.array([m1_final_pos, m2_final_pos], dtype='d') self.nr_intervs = numpy.array([m1_nr_interv, m2_nr_interv], dtype='i') + self.nb_points = (m1_nr_interv + 1) * (m2_nr_interv + 1) self.integ_time = integ_time self.bidirectional_mode = bidirectional From e289505d2392133b28ae6c8a528c265db3cca556 Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 11 Nov 2020 22:42:12 +0100 Subject: [PATCH 14/62] Add tests to reveal lack of RefOne calls While acquiring with hardware synchronization RefOne must be called continuously to empty the hardware buffers. Add tests which reveal lack of such calls. --- src/sardana/pool/test/test_acquisition.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/sardana/pool/test/test_acquisition.py b/src/sardana/pool/test/test_acquisition.py index cf0c1139e8..d7d1833640 100644 --- a/src/sardana/pool/test/test_acquisition.py +++ b/src/sardana/pool/test/test_acquisition.py @@ -27,7 +27,7 @@ import numpy -from unittest import TestCase +from unittest import TestCase, mock from taurus.test import insertTest from sardana.sardanautils import is_number, is_pure_str @@ -592,6 +592,14 @@ def _prepare(self, integ_time, repetitions, latency_time, nb_starts): axis = self.channel.axis self.channel_ctrl.set_axis_par(axis, "value_ref_enabled", True) + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "RefOne", + wraps=ctrl.RefOne) as mock_RefOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_RefOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -652,6 +660,14 @@ def _prepare(self, integ_time, repetitions, latency_time, nb_starts): axis = self.channel.axis self.channel_ctrl.set_axis_par(axis, "value_ref_enabled", True) + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "RefOne", + wraps=ctrl.RefOne) as mock_RefOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_RefOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) From 5209b2dea11f63c5cacdc49f0f0a633722ce6a00 Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 11 Nov 2020 22:47:45 +0100 Subject: [PATCH 15/62] Add regression tests for some ReadOne calls While acquiring with hardware synchronization ReadOne must be called continuously to empty the hardware buffers. Add tests to avoid regressions. --- src/sardana/pool/test/test_acquisition.py | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/sardana/pool/test/test_acquisition.py b/src/sardana/pool/test/test_acquisition.py index d7d1833640..7b02dd8370 100644 --- a/src/sardana/pool/test/test_acquisition.py +++ b/src/sardana/pool/test/test_acquisition.py @@ -567,6 +567,14 @@ def setUp(self): self.data_listener = AttributeListener(dtype=object, attr_name="valuebuffer") + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "ReadOne", + wraps=ctrl.ReadOne) as mock_ReadOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_ReadOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -618,6 +626,14 @@ def setUp(self): self.data_listener = AttributeListener(dtype=object, attr_name="valuebuffer") + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "ReadOne", + wraps=ctrl.ReadOne) as mock_ReadOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_ReadOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) @@ -636,6 +652,14 @@ def setUp(self): self.data_listener = AttributeListener(dtype=object, attr_name="valuebuffer") + def acquire(self, integ_time, repetitions, latency_time): + ctrl = self.channel_ctrl.ctrl + with mock.patch.object(ctrl, "ReadOne", + wraps=ctrl.ReadOne) as mock_ReadOne: + BaseAcquisitionHardwareTestCase.acquire(self, integ_time, + repetitions, latency_time) + assert mock_ReadOne.call_count > 1 + @insertTest(helper_name='acquire', integ_time=0.01, repetitions=10, latency_time=0.02) From 46a67d7fbdee160e47f10f3fcb4a91f5a562a1e5 Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 11 Nov 2020 23:42:30 +0100 Subject: [PATCH 16/62] Refactor to avoid code duplicate --- src/sardana/pool/poolacquisition.py | 53 +++++++++++++++-------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/src/sardana/pool/poolacquisition.py b/src/sardana/pool/poolacquisition.py index 224fb6ce1f..c9230b5153 100644 --- a/src/sardana/pool/poolacquisition.py +++ b/src/sardana/pool/poolacquisition.py @@ -812,6 +812,30 @@ def _raw_read_ctrl_value_ref(self, ret, pool_ctrl): finally: self._value_info.finish_one() + def _process_value_buffer(self, acquirable, value, final=False): + final_str = "final " if final else "" + if is_value_error(value): + self.error("Loop %sread value error for %s" % (final_str, + acquirable.name)) + msg = "Details: " + "".join( + traceback.format_exception(*value.exc_info)) + self.debug(msg) + acquirable.put_value(value, propagate=2) + else: + acquirable.extend_value_buffer(value, propagate=2) + + def _process_value_ref_buffer(self, acquirable, value_ref, final=False): + final_str = "final " if final else "" + if is_value_error(value_ref): + self.error("Loop read ref %svalue error for %s" % + (final_str, acquirable.name)) + msg = "Details: " + "".join( + traceback.format_exception(*value_ref.exc_info)) + self.debug(msg) + acquirable.put_value_ref(value_ref, propagate=2) + else: + acquirable.extend_value_ref_buffer(value_ref, propagate=2) + def in_acquisition(self, states): """Determines if we are in acquisition or if the acquisition has ended based on the current unit trigger modes and states returned by the @@ -1056,15 +1080,7 @@ def action_loop(self): if not i % nb_states_per_value: self.read_value(ret=values) for acquirable, value in list(values.items()): - if is_value_error(value): - self.error("Loop read value error for %s" % - acquirable.name) - msg = "Details: " + "".join( - traceback.format_exception(*value.exc_info)) - self.debug(msg) - acquirable.put_value(value) - else: - acquirable.extend_value_buffer(value) + self._process_value_buffer(acquirable, value) time.sleep(nap) i += 1 @@ -1076,24 +1092,11 @@ def action_loop(self): for acquirable, state_info in list(states.items()): if acquirable in values: value = values[acquirable] - if is_value_error(value): - self.error("Loop final read value error for: %s" % - acquirable.name) - msg = "Details: " + "".join( - traceback.format_exception(*value.exc_info)) - self.debug(msg) - acquirable.put_value(value) - else: - acquirable.extend_value_buffer(value, propagate=2) + self._process_value_buffer(acquirable, value, final=True) if acquirable in value_refs: value_ref = value_refs[acquirable] - if is_value_error(value_ref): - self.error("Loop final read value ref error for: %s" % - acquirable.name) - msg = "Details: " + "".join( - traceback.format_exception(*value_ref.exc_info)) - self.debug(msg) - acquirable.extend_value_ref_buffer(value_ref, propagate=2) + self._process_value_ref_buffer(acquirable, value_ref, + final=True) state_info = acquirable._from_ctrl_state_info(state_info) set_state_info = functools.partial(acquirable.set_state_info, state_info, From 93f8e300aa5d8e30874b17b0971ee822f996e03b Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 11 Nov 2020 23:44:04 +0100 Subject: [PATCH 17/62] Call RefOne() continuously while acquiring with HW synch RefOne() must be called while acquiring with hardware synchronization in order to empty the buffers. Do it. --- src/sardana/pool/poolacquisition.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sardana/pool/poolacquisition.py b/src/sardana/pool/poolacquisition.py index c9230b5153..26e8439399 100644 --- a/src/sardana/pool/poolacquisition.py +++ b/src/sardana/pool/poolacquisition.py @@ -1081,6 +1081,9 @@ def action_loop(self): self.read_value(ret=values) for acquirable, value in list(values.items()): self._process_value_buffer(acquirable, value) + self.read_value_ref(ret=value_refs) + for acquirable, value_ref in list(value_refs.items()): + self._process_value_ref_buffer(acquirable, value_ref) time.sleep(nap) i += 1 From f60eec121874e110c360a333e08823c24c25a6ef Mon Sep 17 00:00:00 2001 From: zreszela Date: Thu, 12 Nov 2020 22:27:52 +0100 Subject: [PATCH 18/62] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29c271e292..820f855bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,16 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). This file follows the formats and conventions from [keepachangelog.com] +## [Unreleased] + +### Added + +* ... + +### Fixed + +* Recorders tests helpers (#1439) + ## [3.0.3] 2020-09-18 ### Added From cc4b08ada036dab072726ed30f7aea5a952d0667 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 13 Nov 2020 15:29:01 +0100 Subject: [PATCH 19/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 820f855bbd..c308960388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This file follows the formats and conventions from [keepachangelog.com] ### Fixed +* Execute per measurement preparation in `mesh` scan macro (#1437) * Recorders tests helpers (#1439) ## [3.0.3] 2020-09-18 From 719722d84759bd8dcf26fd5560d72c382d922029 Mon Sep 17 00:00:00 2001 From: zreszela Date: Mon, 23 Nov 2020 15:59:31 +0100 Subject: [PATCH 20/62] Leave Tango device class attributes definition untouched When adding attributes the controller code can modify them e.g. format, type, maxdimsize. Make a copy of the standard class attributes proceeding from the class definition to avoid problems with modifying them. Fixes #1440. --- src/sardana/tango/pool/PoolDevice.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sardana/tango/pool/PoolDevice.py b/src/sardana/tango/pool/PoolDevice.py index ca4ce9b944..13060ffd4f 100644 --- a/src/sardana/tango/pool/PoolDevice.py +++ b/src/sardana/tango/pool/PoolDevice.py @@ -33,6 +33,7 @@ __docformat__ = 'restructuredtext' import time +from copy import deepcopy from PyTango import Util, DevVoid, DevLong64, DevBoolean, DevString,\ DevDouble, DevEncoded, DevVarStringArray, DispLevel, DevState, SCALAR, \ @@ -690,7 +691,9 @@ def _get_dynamic_attributes(self): attr_name_lower = attr_name.lower() if attr_name_lower in std_attrs_lower: data_info = DataInfo.toDataInfo(attr_name, attr_info) - tg_info = dev_class.standard_attr_list[attr_name] + # copy in order to leave the class attributes untouched + # the downstream code can append MaxDimSize to the attr. info + tg_info = deepcopy(dev_class.standard_attr_list[attr_name]) std_attrs[attr_name] = attr_name, tg_info, data_info else: data_info = DataInfo.toDataInfo(attr_name, attr_info) From 50a0162dc5675bb9d23a49a8f85172536eceec3a Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 4 Dec 2020 15:57:15 +0100 Subject: [PATCH 21/62] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c308960388..a17fef07aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This file follows the formats and conventions from [keepachangelog.com] ### Fixed * Execute per measurement preparation in `mesh` scan macro (#1437) +* Continously read value references in hardware synchronized acquisition + instead of reading only at the end (#1442, #1448) * Recorders tests helpers (#1439) ## [3.0.3] 2020-09-18 From 8a4bbefb5bc18676d087b135c8c155f2ffd43d93 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 4 Dec 2020 16:23:25 +0100 Subject: [PATCH 22/62] Disable flake8 job in CI This job execution reveals problem with the flake8 test: https://travis-ci.org/github/sardana-org/sardana/jobs/745428261 This issue appeared after recent new release of flake8 and is due to the way we install flake8 on travis. Even it it should be relatively easy to fix, I think we should not invest time to travis since we will move to some other CI solution (see #1433) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7288db4a42..79d185ed08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: - secure: "p/0UgVZzPKJQqcvQ/97qMgo9kPCE0cZ6vI+308YEJ2o9xj4a3FsfHCZ/vWtjdsrp1sQbtKVDesx+xmK4CLDzQeC2+Xskv8OZDjaG2jYkHcVosZEM3EGW8rLVKzoDWLr6cTy2wexLgjHPCsmrjukPs49/i5p+WU0no64YoLlZdp9TT+gvWSQJLIk6R4eqt4FHMszPybLv0pvb1SEiCzimlX1WM1pBrE0LHgchd2ZBYSUWTTwe+Koi4HCS4Bads8j20K2e3fFKcmR2u9DfmU+7Mf5HRJsj1LYJgBUF76lUG2/fZfpoDe8sWi+eUewTa3zNM4bhRLpV+pmG0ypplM4pIcdvwiHV03nGSGu6XK6OGQ/Mgsw0fmud4JR4f5g9DgEfERlyJKI4A9mPZQ327OmEwOOl33x2AFJAL05Qvm0yXCkf1dwgYXnZl44SQbAczY1NHFL90t6xbHtmTitJrE2Xb+4BLzMe3OOZj6j/0QeiXA4z1FnZr1s8UoAsm68iW194IuFg1RRG9FTISFWaBew5wzwvAJak0DxkpG0k43VkHiVC7sPHqr5CxXMXO/MuaptK2ti6iLK9xBAEUpO9HluOkeJq5WDIIxBiBS9tPi0i3vIpq87RjHkdw5n7pdIqnuJ1nXUjpWsuUyV3fLkY12fFxSbZgqmNhIE5/o9c5VP/69Y=" matrix: - - TEST="flake8" + # - TEST="flake8" - TEST="testsuite" DOCKER_IMG=reszelaz/sardana-test - TEST="doc" DOCKER_IMG=reszelaz/sardana-test From 0ff086cacb657a0349ea6fcf276e3a5d2bcf393a Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 4 Dec 2020 18:30:08 +0100 Subject: [PATCH 23/62] Document Tango connection recycling and disposal. More details in #1453. --- .../devel/howto_macros/macros_general.rst | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/doc/source/devel/howto_macros/macros_general.rst b/doc/source/devel/howto_macros/macros_general.rst index f8e1cb08c7..3fde9e05f4 100644 --- a/doc/source/devel/howto_macros/macros_general.rst +++ b/doc/source/devel/howto_macros/macros_general.rst @@ -965,6 +965,36 @@ simplified usage you should use Taurus. If you strive for very optimal access to Tango and don't need these benefits then most probably PyTango will work better for you. +.. hint:: + If you go for PyTango and wonder if creating a new `tango.DeviceProxy` + in frequent macro executions is inefficient from the I/O point of view you + should not worry about it cause Tango (more precisely CORBA) is taking + care about recycling the connection during a period of 120 s (default). + + If you still would like to optimize your code in order to avoid creation + of a new `tango.DeviceProxy` you may consider using the + `functools.lru_cache` as a singleton cache mechanism:: + + import functools + import tango + from sardana.macroserver.macro import macro + + Device = functools.lru_cache(maxsize=1024)(tango.DeviceProxy) + + @macro() + def switch_states(self): + """Switch TangoTest device state""" + proxy = Device('sys/tg_test/1') + proxy.SwitchStates() + + Here you don't need to worry about the opened connection to the + Tango device server in case you don't execute the macro for a while. + Again, Tango (more precisely CORBA) will take care about it. + See more details about the CORBA scavanger thread in: + `Tango client threading `_ + and `CORBA idle connection shutdown `_. + + .. _sardana-macro-using-external-libraries: Using external python libraries From 56fafea787363b4daa63e26fbac27f6c2917cb35 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 4 Dec 2020 18:31:47 +0100 Subject: [PATCH 24/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17fef07aa..2fd27331df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This file follows the formats and conventions from [keepachangelog.com] * Continously read value references in hardware synchronized acquisition instead of reading only at the end (#1442, #1448) * Recorders tests helpers (#1439) +* Disable flake8 job in travis CI (#1455) ## [3.0.3] 2020-09-18 From 3402dd4ef30880f8dd28eb35ff47a9280de3854c Mon Sep 17 00:00:00 2001 From: reszelaz Date: Thu, 10 Dec 2020 15:35:36 +0100 Subject: [PATCH 25/62] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fd27331df..6e75c61883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ This file follows the formats and conventions from [keepachangelog.com] * Execute per measurement preparation in `mesh` scan macro (#1437) * Continously read value references in hardware synchronized acquisition instead of reading only at the end (#1442, #1448) +* Avoid problems when defining different, e.g. shape, standard attributes, + e.g. pseudo counter's value, in controllers (#1440, #1446) * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) From 9b98a7b887c1a8c0df496a29fd1348677c7527e2 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Thu, 10 Dec 2020 16:15:13 +0100 Subject: [PATCH 26/62] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e75c61883..74dcb816f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ This file follows the formats and conventions from [keepachangelog.com] ### Added -* ... +* Initial delay in position domain to the synchronization description + in *ct* like continuous scans (#1428) ### Fixed From 1d20c6943b0354b773312d7aca7250422871564b Mon Sep 17 00:00:00 2001 From: reszelaz Date: Thu, 10 Dec 2020 16:26:28 +0100 Subject: [PATCH 27/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74dcb816f1..d6dab22ed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This file follows the formats and conventions from [keepachangelog.com] * Initial delay in position domain to the synchronization description in *ct* like continuous scans (#1428) +* Avoid double printing of user units in PMTV: read widget and units widget (#1424) ### Fixed From 4462c393903dd5b291b6f7a88135334704a1c9bf Mon Sep 17 00:00:00 2001 From: zreszela Date: Thu, 10 Dec 2020 22:59:33 +0100 Subject: [PATCH 28/62] Document ScanUser environment variable. --- doc/source/users/environment_variable_catalog.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/source/users/environment_variable_catalog.rst b/doc/source/users/environment_variable_catalog.rst index e7f72174fb..c22db3681b 100644 --- a/doc/source/users/environment_variable_catalog.rst +++ b/doc/source/users/environment_variable_catalog.rst @@ -308,7 +308,7 @@ ScanRecorder Its value may be either of type string or of list of strings. If ScanRecorder variable is defined, it explicitly indicates which recorder -class should be used and for which file defined by ScanFile (based on the +class should be used and for which file defined by ScanFile (based on the order). Example 1: @@ -334,6 +334,17 @@ Example 2: .. seealso:: More about the extension to recorder map in :ref:`sardana-writing-recorders`. +.. _scanuser: + +ScanUser +~~~~~~~~ +*Not mandatory, set by user* + +Its value is of type string. Its value is delivered to the recorders which +may use it, for example, as a user contact information. If not set, the OS +user executing the Sardana server (which executes the scan) will be passed to +the recorders instead. + .. _sharedmemory: SharedMemory From dbe63dc7275e75442f863bb5391252ba2e13ea7e Mon Sep 17 00:00:00 2001 From: zreszela Date: Thu, 10 Dec 2020 23:43:54 +0100 Subject: [PATCH 29/62] Document showscan online information panels. --- .../_static/showscan-online-infopanels.png | Bin 0 -> 53153 bytes doc/source/users/taurus/showscan.rst | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 doc/source/_static/showscan-online-infopanels.png diff --git a/doc/source/_static/showscan-online-infopanels.png b/doc/source/_static/showscan-online-infopanels.png new file mode 100644 index 0000000000000000000000000000000000000000..8fb460471c05c3b40c2524d7d4c7fbeac33a7d82 GIT binary patch literal 53153 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
GC+#8Vyye5>Ey^m>%= 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

*!=-1dKUjUD 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 A3jhEB literal 0 HcmV?d00001 diff --git a/doc/source/users/taurus/showscan.rst b/doc/source/users/taurus/showscan.rst index df040ed9e4..e2f208ae70 100644 --- a/doc/source/users/taurus/showscan.rst +++ b/doc/source/users/taurus/showscan.rst @@ -48,6 +48,15 @@ plot per curve or group curves by the selected x axis Showscan online plotting three physical counters against the motor's position on separate plots. +Finally, the *scan point* and the *scan information* panels are available +and offer online updates on the channel values of the current scan point +and some general scan information e.g. scan file, start and end time, etc. +respectively. + +.. figure:: /_static/showscan-online-infopanels.png + + Showscan online plotting with separate plots and information panels. + ---------------- Showscan offline ---------------- From 21f28bebd05fe1922f24f07362b5e771519edc4c Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 11 Dec 2020 09:58:35 +0100 Subject: [PATCH 30/62] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6dab22ed0..7966ad9c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ This file follows the formats and conventions from [keepachangelog.com] ### Added +* *scan information* and *scan point* forms to the *showscan online* widget (#1386) +* `ScanPlotWidget`, `ScanPlotWindow`, `ScanInfoForm`, `ScanPointForm` and `ScanWindow` + widget classes for easier composition of custom GUIs involving online scan plotting (#1386) * Initial delay in position domain to the synchronization description in *ct* like continuous scans (#1428) * Avoid double printing of user units in PMTV: read widget and units widget (#1424) From 831f26db98b279a03b966ad954b4d5e8af1c2b8c Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 11 Dec 2020 11:13:51 +0100 Subject: [PATCH 31/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7966ad9c7e..040098cf0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ This file follows the formats and conventions from [keepachangelog.com] * *scan information* and *scan point* forms to the *showscan online* widget (#1386) * `ScanPlotWidget`, `ScanPlotWindow`, `ScanInfoForm`, `ScanPointForm` and `ScanWindow` widget classes for easier composition of custom GUIs involving online scan plotting (#1386) +* Add `ScanUser` environment variable (#1355) * Initial delay in position domain to the synchronization description in *ct* like continuous scans (#1428) * Avoid double printing of user units in PMTV: read widget and units widget (#1424) From f072f75249fc22cda273c05483936e3c30e4f03e Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 11 Dec 2020 13:55:54 +0100 Subject: [PATCH 32/62] Document deterministic scan property of SScan --- doc/source/devel/api/sardana/macroserver/scan.rst | 4 ++-- src/sardana/macroserver/scan/gscan.py | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/source/devel/api/sardana/macroserver/scan.rst b/doc/source/devel/api/sardana/macroserver/scan.rst index 2a969b062e..0663819c11 100644 --- a/doc/source/devel/api/sardana/macroserver/scan.rst +++ b/doc/source/devel/api/sardana/macroserver/scan.rst @@ -26,8 +26,8 @@ GScan :show-inheritance: :members: -GScan ------ +Scan +---- .. inheritance-diagram:: SScan :parts: 1 diff --git a/src/sardana/macroserver/scan/gscan.py b/src/sardana/macroserver/scan/gscan.py index 466050eda1..e4641c1f2e 100644 --- a/src/sardana/macroserver/scan/gscan.py +++ b/src/sardana/macroserver/scan/gscan.py @@ -1067,6 +1067,15 @@ def __init__(self, macro, generator=None, moveables=[], env={}, @property def deterministic_scan(self): + """Check if the scan is a deterministic scan. + + Scan is considered as deterministic scan if + the `~sardana.macroserver.macro.Macro` specialization owning + the scan object contains ``nb_points`` and ``integ_time`` attributes. + + Scan flow depends on this property (some optimizations are applied). + These can be disabled by setting this property to `False`. + """ if self._deterministic_scan is None: macro = self.macro if hasattr(macro, "nb_points") and hasattr(macro, "integ_time"): From f1715588ff83dfe284909438974bea3b61192073 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 11 Dec 2020 15:43:19 +0100 Subject: [PATCH 33/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040098cf0e..58808c94f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This file follows the formats and conventions from [keepachangelog.com] * `ScanPlotWidget`, `ScanPlotWindow`, `ScanInfoForm`, `ScanPointForm` and `ScanWindow` widget classes for easier composition of custom GUIs involving online scan plotting (#1386) * Add `ScanUser` environment variable (#1355) +* Allow to programmatically disable *deterministic scan* optimization (#1426, #1427) * Initial delay in position domain to the synchronization description in *ct* like continuous scans (#1428) * Avoid double printing of user units in PMTV: read widget and units widget (#1424) From 1f733ce471267a52c706a35228631f9cd1450c4f Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 11 Dec 2020 23:33:43 +0100 Subject: [PATCH 34/62] Do not use int ids for MacroNode MacroServer assigns uuid as macro ids when these were not passed in the XML requesting the macro execution. The same applies to the spock and macrobutton. Make the sequencer and the macroexecutor follow the same behavior instead of generating consecutive int number ids. --- .../taurus/core/tango/sardana/macro.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/sardana/taurus/core/tango/sardana/macro.py b/src/sardana/taurus/core/tango/sardana/macro.py index d77feb4f91..4c497d121d 100644 --- a/src/sardana/taurus/core/tango/sardana/macro.py +++ b/src/sardana/taurus/core/tango/sardana/macro.py @@ -32,6 +32,7 @@ import os import copy +import uuid import types import tempfile @@ -833,7 +834,6 @@ def fromList(self, params): class MacroNode(BranchNode): """Class to represent macro element.""" - count = 0 def __init__(self, parent=None, name=None, params_def=None, macro_info=None): @@ -867,7 +867,7 @@ def id(self): """ Getter of macro's id property - :return: (int) + :return: (str) .. seealso: :meth:`MacroNode.setId`, assignId """ @@ -878,7 +878,7 @@ def setId(self, id): """ Setter of macro's id property - :param id: (int) new macro's id + :param id: (str) new macro's id See Also: id, assignId """ @@ -890,16 +890,13 @@ def assignId(self): If macro didn't have an assigned id it assigns it and return macro's id. - :return: (int) + :return: (str) See Also: id, setId """ - id = self.id() - if id is not None: - return id - MacroNode.count += 1 - self.setId(MacroNode.count) - return MacroNode.count + id_ = str(uuid.uuid1()) + self.setId(id_) + return id_ def name(self): return self._name @@ -1160,7 +1157,7 @@ def toXml(self, withId=True): if withId: id_ = self.id() if id_ is not None: - macroElement.set("id", str(self.id())) + macroElement.set("id", self.id()) for hookPlace in self.hookPlaces(): hookElement = etree.SubElement(macroElement, "hookPlace") hookElement.text = hookPlace From ca6a912f1320705077e3523578c6192168ea78e0 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Sat, 12 Dec 2020 09:04:13 +0100 Subject: [PATCH 35/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58808c94f8..33b25e7fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ This file follows the formats and conventions from [keepachangelog.com] instead of reading only at the end (#1442, #1448) * Avoid problems when defining different, e.g. shape, standard attributes, e.g. pseudo counter's value, in controllers (#1440, #1446) +* Problems with macro id's when `sequencer` executes from _plain text_ files (#1215, #1216) * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) From 73f645786ff8b751707b34c0bcd5e5e7dda05dd1 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Tue, 15 Dec 2020 23:30:38 +0100 Subject: [PATCH 36/62] Update CHANGELOG.md --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b25e7fd6..dc1a00da5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ This file follows the formats and conventions from [keepachangelog.com] * Initial delay in position domain to the synchronization description in *ct* like continuous scans (#1428) * Avoid double printing of user units in PMTV: read widget and units widget (#1424) +* Documentation example on how to more efficiently access Tango with PyTango + in macros/controllers (#1456) ### Fixed @@ -23,7 +25,8 @@ This file follows the formats and conventions from [keepachangelog.com] instead of reading only at the end (#1442, #1448) * Avoid problems when defining different, e.g. shape, standard attributes, e.g. pseudo counter's value, in controllers (#1440, #1446) -* Problems with macro id's when `sequencer` executes from _plain text_ files (#1215, #1216) +* Problems with macro id's when `sequencer` executes from _plain text_ files (#1215, #1216) +* `sequencer` loading of plain text sequences in spock syntax with macro functions (#1422) * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) From 62823733cfa886aec6dd3baf17506efe786c1862 Mon Sep 17 00:00:00 2001 From: zreszela Date: Thu, 17 Dec 2020 18:18:49 +0100 Subject: [PATCH 37/62] Fix createMacro and prepareMacro docstring Fix copy&paste mistake in docstring. Fixes #1444 --- src/sardana/macroserver/macro.py | 80 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py index e637b32601..55c1ef822c 100644 --- a/src/sardana/macroserver/macro.py +++ b/src/sardana/macroserver/macro.py @@ -1132,35 +1132,35 @@ def createMacro(self, *pars): Several different parameter formats are supported:: # several parameters: - self.execMacro('ascan', 'th', '0', '100', '10', '1.0') - self.execMacro('mv', [[motor.getName(), '0']]) - self.execMacro('mv', motor.getName(), '0') # backwards compatibility - see note - self.execMacro('ascan', 'th', 0, 100, 10, 1.0) - self.execMacro('mv', [[motor.getName(), 0]]) - self.execMacro('mv', motor.getName(), 0) # backwards compatibility - see note + self.createMacro('ascan', 'th', '0', '100', '10', '1.0') + self.createMacro('mv', [[motor.getName(), '0']]) + self.createMacro('mv', motor.getName(), '0') # backwards compatibility - see note + self.createMacro('ascan', 'th', 0, 100, 10, 1.0) + self.createMacro('mv', [[motor.getName(), 0]]) + self.createMacro('mv', motor.getName(), 0) # backwards compatibility - see note th = self.getObj('th') - self.execMacro('ascan', th, 0, 100, 10, 1.0) - self.execMacro('mv', [[th, 0]]) - self.execMacro('mv', th, 0) # backwards compatibility - see note + self.createMacro('ascan', th, 0, 100, 10, 1.0) + self.createMacro('mv', [[th, 0]]) + self.createMacro('mv', th, 0) # backwards compatibility - see note # a sequence of parameters: - self.execMacro(['ascan', 'th', '0', '100', '10', '1.0') - self.execMacro(['mv', [[motor.getName(), '0']]]) - self.execMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note - self.execMacro(('ascan', 'th', 0, 100, 10, 1.0)) - self.execMacro(['mv', [[motor.getName(), 0]]]) - self.execMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note + self.createMacro(['ascan', 'th', '0', '100', '10', '1.0']) + self.createMacro(['mv', [[motor.getName(), '0']]]) + self.createMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note + self.createMacro(('ascan', 'th', 0, 100, 10, 1.0)) + self.createMacro(['mv', [[motor.getName(), 0]]]) + self.createMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note th = self.getObj('th') - self.execMacro(['ascan', th, 0, 100, 10, 1.0]) - self.execMacro(['mv', [[th, 0]]]) - self.execMacro(['mv', th, 0]) # backwards compatibility - see note + self.createMacro(['ascan', th, 0, 100, 10, 1.0]) + self.createMacro(['mv', [[th, 0]]]) + self.createMacro(['mv', th, 0]) # backwards compatibility - see note # a space separated string of parameters (this is not compatible # with multiple or nested repeat parameters, furthermore the repeat # parameter must be the last one): - self.execMacro('ascan th 0 100 10 1.0') - self.execMacro('mv %s 0' % motor.getName()) + self.createMacro('ascan th 0 100 10 1.0') + self.createMacro('mv %s 0' % motor.getName()) .. note:: From Sardana 2.0 the repeat parameter values must be passed as lists of items. An item of a repeat parameter containing more @@ -1203,34 +1203,34 @@ def prepareMacro(self, *args, **kwargs): Several different parameter formats are supported:: # several parameters: - self.execMacro('ascan', 'th', '0', '100', '10', '1.0') - self.execMacro('mv', [[motor.getName(), '0']]) - self.execMacro('mv', motor.getName(), '0') # backwards compatibility - see note - self.execMacro('ascan', 'th', 0, 100, 10, 1.0) - self.execMacro('mv', [[motor.getName(), 0]]) - self.execMacro('mv', motor.getName(), 0) # backwards compatibility - see note + self.prepareMacro('ascan', 'th', '0', '100', '10', '1.0') + self.prepareMacro('mv', [[motor.getName(), '0']]) + self.prepareMacro('mv', motor.getName(), '0') # backwards compatibility - see note + self.prepareMacro('ascan', 'th', 0, 100, 10, 1.0) + self.prepareMacro('mv', [[motor.getName(), 0]]) + self.prepareMacro('mv', motor.getName(), 0) # backwards compatibility - see note th = self.getObj('th') - self.execMacro('ascan', th, 0, 100, 10, 1.0) - self.execMacro('mv', [[th, 0]]) - self.execMacro('mv', th, 0) # backwards compatibility - see note + self.prepareMacro('ascan', th, 0, 100, 10, 1.0) + self.prepareMacro('mv', [[th, 0]]) + self.prepareMacro('mv', th, 0) # backwards compatibility - see note # a sequence of parameters: - self.execMacro(['ascan', 'th', '0', '100', '10', '1.0']) - self.execMacro(['mv', [[motor.getName(), '0']]]) - self.execMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note - self.execMacro(('ascan', 'th', 0, 100, 10, 1.0)) - self.execMacro(['mv', [[motor.getName(), 0]]]) - self.execMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note + self.prepareMacro(['ascan', 'th', '0', '100', '10', '1.0']) + self.prepareMacro(['mv', [[motor.getName(), '0']]]) + self.prepareMacro(['mv', motor.getName(), '0']) # backwards compatibility - see note + self.prepareMacro(('ascan', 'th', 0, 100, 10, 1.0)) + self.prepareMacro(['mv', [[motor.getName(), 0]]]) + self.prepareMacro(['mv', motor.getName(), 0]) # backwards compatibility - see note th = self.getObj('th') - self.execMacro(['ascan', th, 0, 100, 10, 1.0]) - self.execMacro(['mv', [[th, 0]]]) - self.execMacro(['mv', th, 0]) # backwards compatibility - see note + self.prepareMacro(['ascan', th, 0, 100, 10, 1.0]) + self.prepareMacro(['mv', [[th, 0]]]) + self.prepareMacro(['mv', th, 0]) # backwards compatibility - see note # a space separated string of parameters (this is not compatible # with multiple or nested repeat parameters, furthermore the repeat # parameter must be the last one): - self.execMacro('ascan th 0 100 10 1.0') - self.execMacro('mv %s 0' % motor.getName()) + self.prepareMacro('ascan th 0 100 10 1.0') + self.prepareMacro('mv %s 0' % motor.getName()) .. note:: From Sardana 2.0 the repeat parameter values must be passed as lists of items. An item of a repeat parameter containing more From 0dfd9dbd23dacaba27bcd69f48c0ad39c16654f5 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Thu, 17 Dec 2020 22:23:49 +0100 Subject: [PATCH 38/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1a00da5c..b68e6351b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ This file follows the formats and conventions from [keepachangelog.com] * `sequencer` loading of plain text sequences in spock syntax with macro functions (#1422) * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) +* `createMacro()` and `prepareMacro()` docstring (#1460, #1444) ## [3.0.3] 2020-09-18 From bbfcc1e9a6fbbd639a44db05d6f680850575c293 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 18 Dec 2020 11:15:50 +0100 Subject: [PATCH 39/62] Add InterruptException to sardana.macroserver.macro module As Stop and Abort exception interrupt exceptions may be necessary when programming macros in order to properly implement broad exception catching. --- src/sardana/macroserver/macro.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py index e637b32601..21f50c2cf8 100644 --- a/src/sardana/macroserver/macro.py +++ b/src/sardana/macroserver/macro.py @@ -33,7 +33,8 @@ __all__ = ["OverloadPrint", "PauseEvent", "Hookable", "ExecMacroHook", "MacroFinder", "Macro", "macro", "iMacro", "imacro", "MacroFunc", "Type", "Table", "List", "ViewOption", - "LibraryError", "Optional"] + "LibraryError", "Optional", "StopException", "AbortException", + "InterruptException"] __docformat__ = 'restructuredtext' @@ -60,7 +61,7 @@ from sardana.macroserver.msparameter import Type, ParamType, Optional from sardana.macroserver.msexception import StopException, AbortException, \ ReleaseException, MacroWrongParameterType, UnknownEnv, UnknownMacro, \ - LibraryError + LibraryError, InterruptException from sardana.macroserver.msoptions import ViewOption from sardana.taurus.core.tango.sardana.pool import PoolElement From d46387c4f6462e69d2258c63990baa38fcfdb915 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 18 Dec 2020 11:16:14 +0100 Subject: [PATCH 40/62] Add interrupt exception to Macro API documentation --- doc/source/devel/api/api_macro.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/source/devel/api/api_macro.rst b/doc/source/devel/api/api_macro.rst index 3ac6de1219..810ad1bff1 100644 --- a/doc/source/devel/api/api_macro.rst +++ b/doc/source/devel/api/api_macro.rst @@ -42,3 +42,23 @@ imacro decorator :members: :undoc-members: +StopException +------------- + +.. autoclass:: StopException + :members: + :undoc-members: + +AbortException +-------------- + +.. autoclass:: AbortException + :members: + :undoc-members: + +InterruptException +------------------ + +.. autoclass:: InterruptException + :members: + :undoc-members: From 484af30b4865ec4bf1014389c82b3dd0f4455b02 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 18 Dec 2020 11:16:54 +0100 Subject: [PATCH 41/62] Document exception handling in macros --- .../devel/howto_macros/macros_general.rst | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/doc/source/devel/howto_macros/macros_general.rst b/doc/source/devel/howto_macros/macros_general.rst index 3fde9e05f4..cf863ab4e6 100644 --- a/doc/source/devel/howto_macros/macros_general.rst +++ b/doc/source/devel/howto_macros/macros_general.rst @@ -866,6 +866,55 @@ of user's interruption you must override the withing the :meth:`~sardana.macroserver.macro.Macro.on_stop` or :meth:`~sardana.macroserver.macro.Macro.on_abort`. +.. _sardana-macro-exception-handling: + +Handling exceptions +------------------- + +Please refer to the +`Python Errors and Exceptions `_ +documentation on how to deal with exceptions in your macro code. + +.. important:: + :ref:`sardana-macro-handling-macro-stop-and-abort` is internally implemented + using Python exceptions. So, your ``except`` clause can not simply catch any + exception type without re-raising it - this would ignore the macro stop/abort + request done in the ``try ... except`` block. If you still would like to + use the broad catching, you need to catch and raise the stop/abort exception + first: + + .. code-block:: python + :emphasize-lines: 7 + + import time + + from sardana.macroserver.macro import macro, StopException + + @macro() + def exception_macro(self): + self.output("Starting stoppable process") + try: + for i in range(10): + self.output("In iteration: {}".format(i)) + time.sleep(1) + except StopException: + raise + except Exception: + self.warning("Exception, but we continue") + self.output("After 'try ... except' block") + + If you do not program lines 12-13 and you stop your macro within + the ``try ... except`` block then the macro will continue and print the + output from line 16. + + You may choose to catch and re-raise: + `~sardana.macroserver.macro.StopException`, + `~sardana.macroserver.macro.AbortException` or + `~sardana.macroserver.macro.InterruptException`. The last one will + take care of stopping and aborting at the same time. + + + .. _sardana-macro-adding-hooks-support: Adding hooks support From 0ee9d1560b89a600512037cf68c2ae74646a3ea7 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 18 Dec 2020 11:17:14 +0100 Subject: [PATCH 42/62] Add table of contents to how-to macros --- doc/source/devel/howto_macros/macros_general.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/devel/howto_macros/macros_general.rst b/doc/source/devel/howto_macros/macros_general.rst index cf863ab4e6..07cda863ed 100644 --- a/doc/source/devel/howto_macros/macros_general.rst +++ b/doc/source/devel/howto_macros/macros_general.rst @@ -13,6 +13,10 @@ Writing macros This chapter provides the necessary information to write macros in sardana. The complete macro :term:`API` can be found :ref:`here `. +.. contents:: Table of contents + :depth: 3 + :backlinks: entry + What is a macro --------------- From caeddcbe2cfe90efa4b8a19bcf6be17b085b3157 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 18 Dec 2020 13:33:53 +0100 Subject: [PATCH 43/62] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b68e6351b4..95229e6b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ This file follows the formats and conventions from [keepachangelog.com] * Initial delay in position domain to the synchronization description in *ct* like continuous scans (#1428) * Avoid double printing of user units in PMTV: read widget and units widget (#1424) +* Document how to properly deal with exceptions in macros in order to not interfer + with macro stopping/aborting (#1461) * Documentation example on how to more efficiently access Tango with PyTango in macros/controllers (#1456) From 607f70177fe351d2ac9e934583333ed805594d15 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Mon, 21 Dec 2020 12:02:38 +0100 Subject: [PATCH 44/62] Allow running Spock without Qt bindings --- src/sardana/spock/inputhandler.py | 110 +-------------- src/sardana/spock/ipython_01_00/genutils.py | 23 +++- src/sardana/spock/qtinputhandler.py | 142 ++++++++++++++++++++ src/sardana/spock/spockms.py | 9 +- 4 files changed, 165 insertions(+), 119 deletions(-) create mode 100644 src/sardana/spock/qtinputhandler.py diff --git a/src/sardana/spock/inputhandler.py b/src/sardana/spock/inputhandler.py index 7b9b6f93c3..73add6ef0c 100644 --- a/src/sardana/spock/inputhandler.py +++ b/src/sardana/spock/inputhandler.py @@ -25,18 +25,10 @@ """Spock submodule. It contains an input handler""" -__all__ = ['SpockInputHandler', 'InputHandler'] +__all__ = ['SpockInputHandler'] __docformat__ = 'restructuredtext' -import sys -from multiprocessing import Process, Pipe - -from taurus.core import TaurusManager -from taurus.core.util.singleton import Singleton -from taurus.external.qt import Qt, compat -from taurus.qt.qtgui.dialog import TaurusMessageBox, TaurusInputDialog - from sardana.taurus.core.tango.sardana.macroserver import BaseInputHandler from sardana.spock import genutils @@ -67,103 +59,3 @@ def input(self, input_data=None): def input_timeout(self, input_data): print("SpockInputHandler input timeout") - - -class MessageHandler(Qt.QObject): - - messageArrived = Qt.pyqtSignal(compat.PY_OBJECT) - - def __init__(self, conn, parent=None): - Qt.QObject.__init__(self, parent) - self._conn = conn - self._dialog = None - self.messageArrived.connect(self.on_message) - - def handle_message(self, input_data): - self.messageArrived.emit(input_data) - - def on_message(self, input_data): - msg_type = input_data['type'] - if msg_type == 'input': - if 'macro_name' in input_data and 'title' not in input_data: - input_data['title'] = input_data['macro_name'] - self._dialog = dialog = TaurusInputDialog(input_data=input_data) - dialog.activateWindow() - dialog.exec_() - ok = dialog.result() - value = dialog.value() - ret = dict(input=None, cancel=False) - if ok: - ret['input'] = value - else: - ret['cancel'] = True - self._conn.send(ret) - elif msg_type == 'timeout': - dialog = self._dialog - if dialog: - dialog.close() - - -class InputHandler(Singleton, BaseInputHandler): - - def __init__(self): - # don't call super __init__ on purpose - pass - - def init(self, *args, **kwargs): - self._conn, child_conn = Pipe() - self._proc = proc = Process(target=self.safe_run, - name="SpockInputHandler", args=(child_conn,)) - proc.daemon = True - proc.start() - - def input(self, input_data=None): - # parent process - data_type = input_data.get('data_type', 'String') - if isinstance(data_type, str): - ms = genutils.get_macro_server() - interfaces = ms.getInterfaces() - if data_type in interfaces: - input_data['data_type'] = [ - elem.name for elem in list(interfaces[data_type].values())] - self._conn.send(input_data) - ret = self._conn.recv() - return ret - - def input_timeout(self, input_data): - # parent process - self._conn.send(input_data) - - def safe_run(self, conn): - # child process - try: - return self.run(conn) - except Exception as e: - msgbox = TaurusMessageBox(*sys.exc_info()) - conn.send((e, False)) - msgbox.exec_() - - def run(self, conn): - # child process - self._conn = conn - app = Qt.QApplication.instance() - if app is None: - app = Qt.QApplication(['spock']) - app.setQuitOnLastWindowClosed(False) - self._msg_handler = MessageHandler(conn) - TaurusManager().addJob(self.run_forever, None) - app.exec_() - conn.close() - print("Quit input handler") - - def run_forever(self): - # child process - message, conn = True, self._conn - while message: - message = conn.recv() - if not message: - continue - self._msg_handler.handle_message(message) - app = Qt.QApplication.instance() - if app: - app.quit() diff --git a/src/sardana/spock/ipython_01_00/genutils.py b/src/sardana/spock/ipython_01_00/genutils.py index 1c26d28af8..df85a3882d 100644 --- a/src/sardana/spock/ipython_01_00/genutils.py +++ b/src/sardana/spock/ipython_01_00/genutils.py @@ -82,7 +82,10 @@ from taurus.core.util.codecs import CodecFactory # make sure Qt is properly initialized -from taurus.external.qt import Qt +try: + from taurus.external.qt import Qt +except ImportError: + pass from sardana.spock import exception from sardana.spock import colors @@ -110,7 +113,11 @@ def get_gui_mode(): - return 'qt' + try: + import taurus.external.qt.Qt + return 'qt' + except ImportError: + return None def get_pylab_mode(): @@ -1159,7 +1166,8 @@ def out_prompt_tokens(self): term_app = config.TerminalIPythonApp term_app.display_banner = True term_app.gui = gui_mode - term_app.pylab = 'qt' + if gui_mode == 'qt': + term_app.pylab = 'qt' term_app.pylab_import_all = False #term_app.nosep = False #term_app.classic = True @@ -1280,8 +1288,13 @@ def mainloop(app=None, user_ns=None): def prepare_input_handler(): # initialize input handler as soon as possible - import sardana.spock.inputhandler - _ = sardana.spock.inputhandler.InputHandler() + + try: + import sardana.spock.qtinputhandler + _ = sardana.spock.inputhandler.InputHandler() + except ImportError: + import sardana.spock.inputhandler + _ = sardana.spock.inputhandler.SpockInputHandler() def prepare_cmdline(argv=None): diff --git a/src/sardana/spock/qtinputhandler.py b/src/sardana/spock/qtinputhandler.py new file mode 100644 index 0000000000..c1dea5d8eb --- /dev/null +++ b/src/sardana/spock/qtinputhandler.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python + +############################################################################## +## +# This file is part of Sardana +## +# http://www.sardana-controls.org/ +## +# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain +## +# Sardana is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +## +# Sardana is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +## +# You should have received a copy of the GNU Lesser General Public License +# along with Sardana. If not, see . +## +############################################################################## + +"""Spock submodule. It contains an input handler""" + +__all__ = ['InputHandler'] + +__docformat__ = 'restructuredtext' + +import sys +from multiprocessing import Process, Pipe + +from taurus.core import TaurusManager +from taurus.core.util.singleton import Singleton +from taurus.external.qt import Qt, compat +from taurus.qt.qtgui.dialog import TaurusMessageBox, TaurusInputDialog + +from sardana.taurus.core.tango.sardana.macroserver import BaseInputHandler + +from sardana.spock import genutils + + +class MessageHandler(Qt.QObject): + + messageArrived = Qt.pyqtSignal(compat.PY_OBJECT) + + def __init__(self, conn, parent=None): + Qt.QObject.__init__(self, parent) + self._conn = conn + self._dialog = None + self.messageArrived.connect(self.on_message) + + def handle_message(self, input_data): + self.messageArrived.emit(input_data) + + def on_message(self, input_data): + msg_type = input_data['type'] + if msg_type == 'input': + if 'macro_name' in input_data and 'title' not in input_data: + input_data['title'] = input_data['macro_name'] + self._dialog = dialog = TaurusInputDialog(input_data=input_data) + dialog.activateWindow() + dialog.exec_() + ok = dialog.result() + value = dialog.value() + ret = dict(input=None, cancel=False) + if ok: + ret['input'] = value + else: + ret['cancel'] = True + self._conn.send(ret) + elif msg_type == 'timeout': + dialog = self._dialog + if dialog: + dialog.close() + + +class InputHandler(Singleton, BaseInputHandler): + + def __init__(self): + # don't call super __init__ on purpose + pass + + def init(self, *args, **kwargs): + self._conn, child_conn = Pipe() + self._proc = proc = Process(target=self.safe_run, + name="SpockInputHandler", args=(child_conn,)) + proc.daemon = True + proc.start() + + def input(self, input_data=None): + # parent process + data_type = input_data.get('data_type', 'String') + if isinstance(data_type, str): + ms = genutils.get_macro_server() + interfaces = ms.getInterfaces() + if data_type in interfaces: + input_data['data_type'] = [ + elem.name for elem in list(interfaces[data_type].values())] + self._conn.send(input_data) + ret = self._conn.recv() + return ret + + def input_timeout(self, input_data): + # parent process + self._conn.send(input_data) + + def safe_run(self, conn): + # child process + try: + return self.run(conn) + except Exception as e: + msgbox = TaurusMessageBox(*sys.exc_info()) + conn.send((e, False)) + msgbox.exec_() + + def run(self, conn): + # child process + self._conn = conn + app = Qt.QApplication.instance() + if app is None: + app = Qt.QApplication(['spock']) + app.setQuitOnLastWindowClosed(False) + self._msg_handler = MessageHandler(conn) + TaurusManager().addJob(self.run_forever, None) + app.exec_() + conn.close() + print("Quit input handler") + + def run_forever(self): + # child process + message, conn = True, self._conn + while message: + message = conn.recv() + if not message: + continue + self._msg_handler.handle_message(message) + app = Qt.QApplication.instance() + if app: + app.quit() diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py index adbcf9853a..b76e5a1a88 100644 --- a/src/sardana/spock/spockms.py +++ b/src/sardana/spock/spockms.py @@ -38,19 +38,21 @@ from sardana.sardanautils import is_pure_str, is_non_str_seq from sardana.spock import genutils from sardana.util.parser import ParamParser -from sardana.spock.inputhandler import SpockInputHandler, InputHandler from sardana import sardanacustomsettings CHANGE_EVTS = TaurusEventType.Change, TaurusEventType.Periodic if genutils.get_gui_mode() == 'qt': + from taurus.external.qt import Qt from sardana.taurus.qt.qtcore.tango.sardana.macroserver import QDoor, QMacroServer + from sardana.spock.qtinputhandler import InputHandler BaseDoor = QDoor BaseMacroServer = QMacroServer BaseGUIViewer = object else: from sardana.taurus.core.tango.sardana.macroserver import BaseDoor, BaseMacroServer + from sardana.spock.inputhandler import SpockInputHandler BaseGUIViewer = object @@ -290,7 +292,7 @@ def __init__(self, name, **kw): self.call__init__(BaseDoor, name, **kw) def create_input_handler(self): - return SpockInputHandler(self) + return SpockInputHandler() def get_color_mode(self): return genutils.get_color_mode() @@ -554,9 +556,6 @@ def _processRecordData(self, data): return BaseDoor._processRecordData(self, data) -from taurus.external.qt import Qt - - class QSpockDoor(SpockBaseDoor): def __init__(self, name, **kw): From 52a259aed1f5af446d63a3ee7c2ddaa321ab42db Mon Sep 17 00:00:00 2001 From: Abdullah Amjad <13bscsaamjad@seecs.edu.pk> Date: Mon, 11 Jan 2021 16:11:06 +0100 Subject: [PATCH 45/62] fixed signature of PrepareOne method on TriggerGateController (#1468) Co-authored-by: Abdullah --- src/sardana/pool/controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sardana/pool/controller.py b/src/sardana/pool/controller.py index 4895651f8f..3574399f03 100644 --- a/src/sardana/pool/controller.py +++ b/src/sardana/pool/controller.py @@ -914,12 +914,13 @@ def __init__(self, inst, props, *args, **kwargs): # TODO: Implement a Preparable interface and move this method # and the Loadable.PrepareOne() there. - def PrepareOne(self, nb_starts): + def PrepareOne(self, axis, nb_starts): """**Controller API**. Override if necessary. Called to prepare the trigger/gate axis with the measurement parameters. Default implementation does nothing. + :param int axis: axis :param int nb_starts: number of starts """ pass From 6419df1cbbc777b8229db132c5fadf119529f6c5 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Mon, 11 Jan 2021 16:18:14 +0100 Subject: [PATCH 46/62] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95229e6b8a..7351706761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ This file follows the formats and conventions from [keepachangelog.com] * *scan information* and *scan point* forms to the *showscan online* widget (#1386) * `ScanPlotWidget`, `ScanPlotWindow`, `ScanInfoForm`, `ScanPointForm` and `ScanWindow` widget classes for easier composition of custom GUIs involving online scan plotting (#1386) +* Include trigger/gate elements in the per-measurement preparation (#1432, #1443, #1468) + * Add `PrepareOne()` to TriggerGate controller. + * Call TriggerGate controller preparation methods in the _acquision action_ * Add `ScanUser` environment variable (#1355) * Allow to programmatically disable *deterministic scan* optimization (#1426, #1427) * Initial delay in position domain to the synchronization description From 498724f14b97d5e95935a031e6980d988f83bc17 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Thu, 14 Jan 2021 09:59:50 +0100 Subject: [PATCH 47/62] Fix bug in string formatting Fix ValueError (unsupported format character ''') by properly formatting string. --- src/sardana/pool/poolpseudomotor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sardana/pool/poolpseudomotor.py b/src/sardana/pool/poolpseudomotor.py index 7819b4cdea..e63fb214ab 100644 --- a/src/sardana/pool/poolpseudomotor.py +++ b/src/sardana/pool/poolpseudomotor.py @@ -135,7 +135,7 @@ def get_physical_write_positions(self): # because of a cold start pos_attr.update(propagate=0) if pos_attr.in_error(): - raise PoolException("Cannot get '%' position" % pos_attr.obj.name, + raise PoolException("Cannot get '%s' position" % pos_attr.obj.name, exc_info=pos_attr.exc_info) value = pos_attr.value ret.append(value) @@ -149,7 +149,7 @@ def get_physical_positions(self): if not pos_attr.has_value(): pos_attr.update(propagate=0) if pos_attr.in_error(): - raise PoolException("Cannot get '%' position" % pos_attr.obj.name, + raise PoolException("Cannot get '%s' position" % pos_attr.obj.name, exc_info=pos_attr.exc_info) ret.append(pos_attr.value) return ret From 2567e33faccb7edb90ea510f9dd60ec17a23be4c Mon Sep 17 00:00:00 2001 From: zreszela Date: Thu, 14 Jan 2021 12:56:39 +0100 Subject: [PATCH 48/62] Complement and fix docs on setting ORBendPoint * document the possibility to fix IP address * fix examples to use "giop:tcp:" prefix --- doc/source/users/configuration/server.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/doc/source/users/configuration/server.rst b/doc/source/users/configuration/server.rst index f7cf79fe0e..6859fd8f6f 100644 --- a/doc/source/users/configuration/server.rst +++ b/doc/source/users/configuration/server.rst @@ -7,25 +7,30 @@ Sardana system can :ref:`run as one or many Tango device servers`. Tango device servers listens on a TCP port for the CORBA requests. Usually it is fine to use the randomly assigned port (default behavior) but sometimes -it may be necessary to use a fixed port number. For example, when the server -needs to be accessed from another isolated network and we want to open -connections only for the given ports. +it may be necessary to use a fixed port number or even IP address. +For example, when the server needs to be accessed from another isolated +network and we want to open connections only for the given ports or IPs. -There are three possibilities to assign the port explicitly (the order -indicates the precedence): +There are three possibilities to assign the IP and/or port in format of the +ORBendPoint explicitly (the order indicates the precedence): + +.. note:: + The ORBendPoint is in the following format: ``giop:tcp::`` + and both IP and port are optional, so you could only fix the IP, + only fix the port, fix both of them or none of them. - using OS environment variable ``ORBendPoint`` e.g. .. code-block:: bash - $ export ORBendPoint=28366 - $ Pool demo1 -ORBendPoint 28366 + $ export ORBendPoint=giop:tcp:192.168.0.100:28366 + $ Pool demo1 - using Tango device server command line argument ``-ORBendPoint`` .. code-block:: bash - $ Pool demo1 -ORBendPoint 28366 + $ Pool demo1 -ORBendPoint giop:tcp:192.168.0.100:28366 - using Tango DB free property with object name: ``ORBendPoint`` and property name: ``/``) @@ -34,7 +39,7 @@ indicates the precedence): import tango db = tango.Database() - db.put_property("ORBendPoint", {"Pool/demo1": 28366}) + db.put_property("ORBendPoint", {"Pool/demo1": "giop:tcp:192.168.0.100:28366"}) .. note:: From f2f73ac20b1f397450109a582e264577cfabd011 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Thu, 14 Jan 2021 14:50:59 +0100 Subject: [PATCH 49/62] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7351706761..f09a6ab13b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ This file follows the formats and conventions from [keepachangelog.com] * Avoid double printing of user units in PMTV: read widget and units widget (#1424) * Document how to properly deal with exceptions in macros in order to not interfer with macro stopping/aborting (#1461) +* Documentation on how to start Tango servers on fixed IP - ORBendPoint (#1470) * Documentation example on how to more efficiently access Tango with PyTango in macros/controllers (#1456) @@ -35,6 +36,7 @@ This file follows the formats and conventions from [keepachangelog.com] * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) * `createMacro()` and `prepareMacro()` docstring (#1460, #1444) +* String formatting when rising exceptions in pseudomotors (#1469) ## [3.0.3] 2020-09-18 From ded02b6c3b368f0506f4357e422f91ec1e27abc4 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Fri, 15 Jan 2021 08:12:54 +0100 Subject: [PATCH 50/62] Explicitly require Qt bindings when Qt input handler for Spock is requested --- src/sardana/spock/ipython_01_00/genutils.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/sardana/spock/ipython_01_00/genutils.py b/src/sardana/spock/ipython_01_00/genutils.py index df85a3882d..8009c8e128 100644 --- a/src/sardana/spock/ipython_01_00/genutils.py +++ b/src/sardana/spock/ipython_01_00/genutils.py @@ -1289,12 +1289,15 @@ def mainloop(app=None, user_ns=None): def prepare_input_handler(): # initialize input handler as soon as possible - try: - import sardana.spock.qtinputhandler - _ = sardana.spock.inputhandler.InputHandler() - except ImportError: - import sardana.spock.inputhandler - _ = sardana.spock.inputhandler.SpockInputHandler() + from sardana import sardanacustomsettings + + if sardanacustomsettings.SPOCK_INPUT_HANDLER == "Qt": + + try: + import sardana.spock.qtinputhandler + _ = sardana.spock.qtinputhandler.InputHandler() + except ImportError: + raise Exception("Cannot use Spock Qt input handler!") def prepare_cmdline(argv=None): From 1c69766fd850747ed94824224290e265e5d8f7f9 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Fri, 15 Jan 2021 08:27:59 +0100 Subject: [PATCH 51/62] Move imports --- src/sardana/spock/spockms.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py index b76e5a1a88..95b67b149b 100644 --- a/src/sardana/spock/spockms.py +++ b/src/sardana/spock/spockms.py @@ -46,13 +46,11 @@ if genutils.get_gui_mode() == 'qt': from taurus.external.qt import Qt from sardana.taurus.qt.qtcore.tango.sardana.macroserver import QDoor, QMacroServer - from sardana.spock.qtinputhandler import InputHandler BaseDoor = QDoor BaseMacroServer = QMacroServer BaseGUIViewer = object else: from sardana.taurus.core.tango.sardana.macroserver import BaseDoor, BaseMacroServer - from sardana.spock.inputhandler import SpockInputHandler BaseGUIViewer = object @@ -292,6 +290,8 @@ def __init__(self, name, **kw): self.call__init__(BaseDoor, name, **kw) def create_input_handler(self): + from sardana.spock.inputhandler import SpockInputHandler + return SpockInputHandler() def get_color_mode(self): @@ -574,6 +574,9 @@ def recordDataReceived(self, s, t, v): return res def create_input_handler(self): + from sardana.spock.inputhandler import SpockInputHandler + from sardana.spock.qtinputhandler import InputHandler + inputhandler = getattr(sardanacustomsettings, 'SPOCK_INPUT_HANDLER', "CLI") From 5a3dd44d88c75bc5f999c1657fa598a11786f77e Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 15 Jan 2021 10:59:32 +0100 Subject: [PATCH 52/62] Make sardanacustomsettings access more robust --- src/sardana/spock/ipython_01_00/genutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sardana/spock/ipython_01_00/genutils.py b/src/sardana/spock/ipython_01_00/genutils.py index 8009c8e128..c92ca164df 100644 --- a/src/sardana/spock/ipython_01_00/genutils.py +++ b/src/sardana/spock/ipython_01_00/genutils.py @@ -1291,7 +1291,7 @@ def prepare_input_handler(): from sardana import sardanacustomsettings - if sardanacustomsettings.SPOCK_INPUT_HANDLER == "Qt": + if getattr(sardanacustomsettings, "SPOCK_INPUT_HANDLER", "CLI") == "Qt": try: import sardana.spock.qtinputhandler From 8b864a146d89a0581c641935da863f302dbd718f Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Fri, 15 Jan 2021 12:35:34 +0100 Subject: [PATCH 53/62] Remove (hopefully) redundant import --- src/sardana/spock/spockms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py index 95b67b149b..88c0ef120a 100644 --- a/src/sardana/spock/spockms.py +++ b/src/sardana/spock/spockms.py @@ -44,7 +44,6 @@ if genutils.get_gui_mode() == 'qt': - from taurus.external.qt import Qt from sardana.taurus.qt.qtcore.tango.sardana.macroserver import QDoor, QMacroServer BaseDoor = QDoor BaseMacroServer = QMacroServer From 530a0730f12cafac00d37fec7c7b1bc6c19383ba Mon Sep 17 00:00:00 2001 From: stanislaw55 <32959446+stanislaw55@users.noreply.github.com> Date: Fri, 15 Jan 2021 12:36:43 +0100 Subject: [PATCH 54/62] Use more robust access to custom settings Co-authored-by: reszelaz --- src/sardana/spock/ipython_01_00/genutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sardana/spock/ipython_01_00/genutils.py b/src/sardana/spock/ipython_01_00/genutils.py index 8009c8e128..c92ca164df 100644 --- a/src/sardana/spock/ipython_01_00/genutils.py +++ b/src/sardana/spock/ipython_01_00/genutils.py @@ -1291,7 +1291,7 @@ def prepare_input_handler(): from sardana import sardanacustomsettings - if sardanacustomsettings.SPOCK_INPUT_HANDLER == "Qt": + if getattr(sardanacustomsettings, "SPOCK_INPUT_HANDLER", "CLI") == "Qt": try: import sardana.spock.qtinputhandler From 567db38568c6661ada6a6f7953a133f443e14a38 Mon Sep 17 00:00:00 2001 From: zreszela Date: Fri, 15 Jan 2021 12:51:48 +0100 Subject: [PATCH 55/62] Make write of integration time more robust Write of the integration time may fail e.g. problems with reading latency_time of the controller and we fill the cache anyway. This causes problems with not initialized synchronization description. Fill integration time cache only if the write worked. --- src/sardana/taurus/core/tango/sardana/pool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sardana/taurus/core/tango/sardana/pool.py b/src/sardana/taurus/core/tango/sardana/pool.py index 91c2bfa124..8f476ad29e 100644 --- a/src/sardana/taurus/core/tango/sardana/pool.py +++ b/src/sardana/taurus/core/tango/sardana/pool.py @@ -2460,8 +2460,8 @@ def setIntegrationTime(self, ctime): def putIntegrationTime(self, ctime): if self._last_integ_time == ctime: return - self._last_integ_time = ctime self.getIntegrationTimeObj().write(ctime) + self._last_integ_time = ctime def getAcquisitionModeObj(self): return self._getAttrEG('AcquisitionMode') From 7cc9229347e69ad798c4c91f65e1bc0105851629 Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Fri, 15 Jan 2021 14:35:23 +0100 Subject: [PATCH 56/62] Add checks for magics using Qt --- src/sardana/spock/magic.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sardana/spock/magic.py b/src/sardana/spock/magic.py index 0f4b1c6126..eff733ee5c 100644 --- a/src/sardana/spock/magic.py +++ b/src/sardana/spock/magic.py @@ -40,6 +40,15 @@ def expconf(self, parameter_s=''): """Launches a GUI for configuring the environment variables for the experiments (scans)""" + + try: + from taurus.external.qt import qt + except ImportError: + print("Qt binding is not available. ExpConf cannot work without it." + "(hint: maybe you want to use experiment configuration macros? " + "https://sardana-controls.org/users/standard_macro_catalog.html#experiment-configuration-macros)") + return + try: from sardana.taurus.qt.qtgui.extra_sardana import ExpDescriptionEditor except: @@ -81,6 +90,13 @@ def showscan(self, parameter_s=''): Where *online* means plot the scan as it runs and *offline* means - extract the scan data from the file - works only with HDF5 files. """ + + try: + from taurus.external.qt import qt + except ImportError: + print("Qt binding is not available. Showscan cannot work without it.") + return + params = parameter_s.split() door = get_door() scan_nb = None @@ -121,6 +137,12 @@ def showscan(self, parameter_s=''): def spsplot(self, parameter_s=''): + try: + from taurus.external.qt import qt + except ImportError: + print("Qt binding is not available. SPSplot cannot work without it.") + return + get_door().plot() From c646206b3f532e717be073c4c01e33a6e6358d75 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Fri, 15 Jan 2021 14:37:13 +0100 Subject: [PATCH 57/62] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f09a6ab13b..ddd6e3810b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,8 @@ This file follows the formats and conventions from [keepachangelog.com] * `sequencer` loading of plain text sequences in spock syntax with macro functions (#1422) * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) -* `createMacro()` and `prepareMacro()` docstring (#1460, #1444) +* `createMacro()` and `prepareMacro()` docstring (#1460, #1444) +* Make write of MeasurementGroup (Taurus extension) integration time more robust (#1473) * String formatting when rising exceptions in pseudomotors (#1469) ## [3.0.3] 2020-09-18 From fafce7ce6484e0c328d64674349ec33d2efd953a Mon Sep 17 00:00:00 2001 From: Stanislaw Cabala Date: Fri, 15 Jan 2021 16:37:17 +0100 Subject: [PATCH 58/62] Fix broken import --- src/sardana/spock/magic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sardana/spock/magic.py b/src/sardana/spock/magic.py index eff733ee5c..b6e2ea1f30 100644 --- a/src/sardana/spock/magic.py +++ b/src/sardana/spock/magic.py @@ -42,7 +42,7 @@ def expconf(self, parameter_s=''): for the experiments (scans)""" try: - from taurus.external.qt import qt + from taurus.external.qt import Qt except ImportError: print("Qt binding is not available. ExpConf cannot work without it." "(hint: maybe you want to use experiment configuration macros? " @@ -92,7 +92,7 @@ def showscan(self, parameter_s=''): """ try: - from taurus.external.qt import qt + from taurus.external.qt import Qt except ImportError: print("Qt binding is not available. Showscan cannot work without it.") return @@ -138,7 +138,7 @@ def showscan(self, parameter_s=''): def spsplot(self, parameter_s=''): try: - from taurus.external.qt import qt + from taurus.external.qt import Qt except ImportError: print("Qt binding is not available. SPSplot cannot work without it.") return From 717287222dd1cc9df35521c1c25152a3d949cc5d Mon Sep 17 00:00:00 2001 From: zreszela Date: Tue, 19 Jan 2021 16:14:38 +0100 Subject: [PATCH 59/62] Add Qt check for macro plotting --- src/sardana/spock/spockms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sardana/spock/spockms.py b/src/sardana/spock/spockms.py index 88c0ef120a..78c511c8c5 100644 --- a/src/sardana/spock/spockms.py +++ b/src/sardana/spock/spockms.py @@ -514,6 +514,11 @@ def processRecordData(self, data): and data['type'] == 'function'): func_name = data['func_name'] if func_name.startswith("pyplot."): + try: + from taurus.external.qt import Qt + except ImportError: + print("Qt binding is not available. Macro plotting cannot work without it.") + return func_name = self.MathFrontend + "." + func_name args = data['args'] kwargs = data['kwargs'] From 413fbd611ed32986ccf052fbbca0778ed40e0b65 Mon Sep 17 00:00:00 2001 From: reszelaz Date: Wed, 20 Jan 2021 09:30:07 +0100 Subject: [PATCH 60/62] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd6e3810b..f29e1099aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ This file follows the formats and conventions from [keepachangelog.com] e.g. pseudo counter's value, in controllers (#1440, #1446) * Problems with macro id's when `sequencer` executes from _plain text_ files (#1215, #1216) * `sequencer` loading of plain text sequences in spock syntax with macro functions (#1422) +* Allow running Spock without Qt bindings (#1462, #1463) * Recorders tests helpers (#1439) * Disable flake8 job in travis CI (#1455) * `createMacro()` and `prepareMacro()` docstring (#1460, #1444) From 21599de89fcb394e9c8fc7ec753613cf65254f5c Mon Sep 17 00:00:00 2001 From: Jose Tiago Macara Coutinho Date: Wed, 20 Jan 2021 12:51:59 +0100 Subject: [PATCH 61/62] Add macro name and line to macro status 'start' event --- src/sardana/macroserver/macro.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sardana/macroserver/macro.py b/src/sardana/macroserver/macro.py index abff4a2d0a..afb91c0d3a 100644 --- a/src/sardana/macroserver/macro.py +++ b/src/sardana/macroserver/macro.py @@ -538,6 +538,8 @@ def __init__(self, *args, **kwargs): self._id = kwargs.get('id') self._desc = "Macro '%s'" % self._macro_line self._macro_status = {'id': self._id, + 'name': self._name, + 'macro_line': self._macro_line, 'range': (0.0, 100.0), 'state': 'start', 'step': 0.0} @@ -2334,6 +2336,12 @@ def exec_(self): # make sure a 0.0 progress is sent yield macro_status + # Avoid repeating same information on subsequent events. If, in the + # future, clients that connect in the middle of macro execution need + # this information, just simply remove the lines below + del macro_status['name'] + del macro_status['macro_line'] + # allow any macro to be paused at the beginning of its execution self.pausePoint() From f1a044e54ea655a8fb559aaceb8304085460c3e1 Mon Sep 17 00:00:00 2001 From: zreszela Date: Wed, 20 Jan 2021 22:59:40 +0100 Subject: [PATCH 62/62] Fix createMacro test Test is running twice the same macro object (created only once). This looks like a copy&paste error. Remove the second run. --- src/sardana/macroserver/test/res/macros/testmacros.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sardana/macroserver/test/res/macros/testmacros.py b/src/sardana/macroserver/test/res/macros/testmacros.py index 638315865b..450db21879 100644 --- a/src/sardana/macroserver/test/res/macros/testmacros.py +++ b/src/sardana/macroserver/test/res/macros/testmacros.py @@ -88,7 +88,6 @@ def run(self, *args): params = (99, 1., 2.) expected_params = (99, [1., 2.]) - self.runMacro(macro) macro, pars = self.createMacro('pt6_base', *params) self.runMacro(macro) result = macro.data