From 324506648d38d083460b6d330b772fb65ef97a1c Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Fri, 30 Oct 2020 09:28:05 +0100 Subject: [PATCH 01/10] adding a trigger_interface --- hardware/trigger_device/dummy_trigger.py | 113 +++++++++++++++++ hardware/trigger_device/ni_trigger.py | 147 +++++++++++++++++++++++ interface/trigger_interface.py | 69 +++++++++++ 3 files changed, 329 insertions(+) create mode 100644 hardware/trigger_device/dummy_trigger.py create mode 100644 hardware/trigger_device/ni_trigger.py create mode 100644 interface/trigger_interface.py diff --git a/hardware/trigger_device/dummy_trigger.py b/hardware/trigger_device/dummy_trigger.py new file mode 100644 index 0000000000..7b079b5529 --- /dev/null +++ b/hardware/trigger_device/dummy_trigger.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +""" +Qudi is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Qudi 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Qudi. If not, see . + +Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the +top-level directory of this distribution and at +""" + +from core.module import Base +from core.configoption import ConfigOption +import numpy as np +import time + +from interface.trigger_interface import TriggerInterface + + +class DummyTrigger(Base, TriggerInterface): + """ This is a dummy to simulate a simple trigger. + """ + + # ConfigOptions + # names_of_triggers defined as list of strings + _names_of_triggers = ConfigOption(name='names_of_triggers', default=['one', 'two'], missing='nothing') + + # trigger_length in seconds + _trigger_length = ConfigOption(name='trigger_length', default=0.5, missing='nothing') + + def on_activate(self): + """ Activate the module and fill status variables. + """ + pass + + def on_deactivate(self): + """ Deactivate the module and clean up. + """ + pass + + @property + def number_of_triggers(self): + """ The number of triggers provided by this hardware file. + @return int: number of triggers + """ + return len(self._names_of_triggers) + + @property + def names_of_triggers(self): + """ Names of the triggers as list of strings. + @return list(str): names of the triggers + """ + return self._names_of_triggers.copy() + + def trigger(self, trigger=None): + """ Triggers the hardware. + The trigger is performed either + on all channels, if trigger is None, + on a single channel, if trigger is a single channel name or + on a list of channels, if trigger is a list of channel names. + @param [None/str/list(str)] trigger: trigger name to be triggered + @return int: negative error code or 0 at success + """ + if trigger is None: + trigger = self.names_of_triggers + elif isinstance(trigger, str): + if trigger in self.names_of_triggers: + trigger = [trigger] + else: + self.log.error(f'trigger name "{trigger}" was requested, but the options are: {self.names_of_triggers}') + return -1 + elif isinstance(trigger, (list, tuple, np.ndarray, set)): + for index, item in enumerate(trigger): + if item not in self.names_of_triggers: + self.log.error(f'trigger name "{trigger}" was requested, ' + f'but the options are: {self.names_of_triggers}') + del trigger[index] + else: + self.log.error(f'The trigger name was {trigger} but either has to be one of {self.names_of_triggers} ' + f'or a list of trigger names.') + return -2 + + for item in trigger: + self.log.info(f'Trigger on channel: {item}.') + + time.sleep(self.trigger_length) + return 0 + + @property + def trigger_length(self): + """ Returns the length of all the triggers of this hardware in seconds. + @return int: length of the trigger + """ + return self._trigger_length + + @trigger_length.setter + def trigger_length(self, value): + """ Sets the trigger length in seconds. + @param float value: length of the trigger to be set + """ + if not isinstance(value, (int, float)): + self.log.error(f'trigger_length has to be of type float but was {value}.') + return + self._trigger_length = float(value) diff --git a/hardware/trigger_device/ni_trigger.py b/hardware/trigger_device/ni_trigger.py new file mode 100644 index 0000000000..ce9addf639 --- /dev/null +++ b/hardware/trigger_device/ni_trigger.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- + +""" +Qudi is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Qudi 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Qudi. If not, see . + +Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the +top-level directory of this distribution and at +""" + +import nidaqmx +import time +import re +import numpy as np + +from core.module import Base +from core.configoption import ConfigOption +from core.statusvariable import StatusVar + +from interface.trigger_interface import TriggerInterface + + +class NITriggers(Base, TriggerInterface): + """ + A simple triggering device based on an arbitrary NI Card that has digital output channels. + + Example config for copy-paste: + + ni_trigger: + module.Class: 'trigger_device.ni_trigger.NITriggers' + trigger_channel: 'Dev1/port0/line9:8' + + """ + _trigger_channel = ConfigOption(name='trigger_channel', default='Dev1/PFI15', missing='warn') + _names_of_triggers = ConfigOption(name='names_of_triggers', default=None, missing='nothing') + _trigger_length = StatusVar(name='trigger_length', default=1) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._channels = dict() + + def on_activate(self): + """ Prepare module, connect to hardware. + """ + channels = list() + if not self._trigger_channel.__contains__(':'): + channels.append(self._trigger_channel) + else: + int_parts = re.split('\D', self._trigger_channel) + start_number = int(int_parts[-2]) + stop_number = int(int_parts[-1]) + front_part = self._trigger_channel.split(int_parts[-2])[0] + + for number in range(start_number, + stop_number + 1 if start_number < stop_number else stop_number - 1, + 1 if start_number < stop_number else -1): + channels.append(front_part + str(number)) + + if isinstance(self._names_of_triggers, str) and len(channels) == 1: + self._channels = {self._names_of_switches: channels[0]} + else: + try: + self._channels = {str(name): channels[index] for index, name in enumerate(self._names_of_triggers)} + except TypeError: + self._channels = {name: name for name in channels} + + def on_deactivate(self): + """ Deactivate the module and clean up. + """ + pass + + @property + def number_of_triggers(self): + """ The number of triggers provided by this hardware file. + @return int: number of triggers + """ + return len(self.names_of_triggers) + + @property + def names_of_triggers(self): + """ Names of the triggers as list of strings. + @return list(str): names of the triggers + """ + return list(self._channels) + + def trigger(self, trigger=None): + """ Triggers the hardware. + The trigger is performed either + on all channels, if trigger is None, + on a single channel, if trigger is a single channel name or + on a list of channels, if trigger is a list of channel names. + @param [None/str/list(str)] trigger: trigger name to be triggered + @return int: negative error code or 0 at success + """ + if trigger is None: + trigger = self.names_of_triggers + elif isinstance(trigger, str): + if trigger in self.names_of_triggers: + trigger = [trigger] + else: + self.log.error(f'trigger name "{trigger}" was requested, but the options are: {self.names_of_triggers}') + return -1 + elif isinstance(trigger, (list, tuple, np.ndarray, set)): + for index, item in enumerate(trigger): + if item not in self.names_of_triggers: + self.log.error(f'trigger name "{trigger}" was requested, ' + f'but the options are: {self.names_of_triggers}') + del trigger[index] + else: + self.log.error(f'The trigger name was {trigger} but either has to be one of {self.names_of_triggers} ' + f'or a list of trigger names.') + return -2 + + if trigger: + with nidaqmx.Task('NITriggerTask') as trigger_task: + for item in trigger: + trigger_task.do_channels.add_do_chan(self._channels[item]) + trigger_task.write(True, auto_start=True) + time.sleep(self._trigger_length) + trigger_task.write(False) + + @property + def trigger_length(self): + """ Returns the length of all the triggers of this hardware in seconds. + @return int: length of the trigger + """ + return self._trigger_length + + @trigger_length.setter + def trigger_length(self, value): + """ Sets the trigger length in seconds. + @param float value: length of the trigger to be set + """ + if not isinstance(value, (int, float)): + self.log.error(f'trigger_length has to be of type float but was {value}.') + return + self._trigger_length = float(value) diff --git a/interface/trigger_interface.py b/interface/trigger_interface.py new file mode 100644 index 0000000000..ae7e34f4d0 --- /dev/null +++ b/interface/trigger_interface.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +""" +Qudi is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Qudi 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Qudi. If not, see . + +Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the +top-level directory of this distribution and at +""" + +from core.interface import abstract_interface_method +from core.meta import InterfaceMetaclass + + +class TriggerInterface(metaclass=InterfaceMetaclass): + + @property + @abstract_interface_method + def number_of_triggers(self): + """ The number of triggers provided by this hardware file. + @return int: number of triggers + """ + pass + + @property + @abstract_interface_method + def names_of_triggers(self): + """ Names of the triggers as list of strings. + @return list(str): names of the triggers + """ + pass + + @abstract_interface_method + def trigger(self, trigger=None): + """ Triggers the hardware. + The trigger is performed either + on all channels, if trigger is None, + on a single channel, if trigger is a single channel name or + on a list of channels, if trigger is a list of channel names. + @param [None/str/list(str)] trigger: trigger name to be triggered + @return int: negative error code or 0 at success + """ + pass + + @property + @abstract_interface_method + def trigger_length(self): + """ Returns the length of all the triggers of this hardware in seconds. + @return int: length of the trigger + """ + pass + + @trigger_length.setter + @abstract_interface_method + def trigger_length(self, value): + """ Sets the trigger length in seconds. + @param float value: length of the trigger to be set + """ + pass From daa68255e68c7a723e1c698a7d4e6ae988a7497c Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Fri, 30 Oct 2020 09:35:41 +0100 Subject: [PATCH 02/10] tweaks to config options --- hardware/trigger_device/dummy_trigger.py | 4 +++- hardware/trigger_device/ni_trigger.py | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/hardware/trigger_device/dummy_trigger.py b/hardware/trigger_device/dummy_trigger.py index 7b079b5529..4c1d0e9a66 100644 --- a/hardware/trigger_device/dummy_trigger.py +++ b/hardware/trigger_device/dummy_trigger.py @@ -20,6 +20,7 @@ from core.module import Base from core.configoption import ConfigOption +from core.statusvariable import StatusVar import numpy as np import time @@ -34,8 +35,9 @@ class DummyTrigger(Base, TriggerInterface): # names_of_triggers defined as list of strings _names_of_triggers = ConfigOption(name='names_of_triggers', default=['one', 'two'], missing='nothing') + # StatusVariables # trigger_length in seconds - _trigger_length = ConfigOption(name='trigger_length', default=0.5, missing='nothing') + _trigger_length = StatusVar(name='trigger_length', default=0.5) def on_activate(self): """ Activate the module and fill status variables. diff --git a/hardware/trigger_device/ni_trigger.py b/hardware/trigger_device/ni_trigger.py index ce9addf639..67ee99510f 100644 --- a/hardware/trigger_device/ni_trigger.py +++ b/hardware/trigger_device/ni_trigger.py @@ -41,8 +41,15 @@ class NITriggers(Base, TriggerInterface): trigger_channel: 'Dev1/port0/line9:8' """ + + # ConfigOptions + # trigger channels of the NI Card to be used for triggering. This can either be a single channel or multiple lines. _trigger_channel = ConfigOption(name='trigger_channel', default='Dev1/PFI15', missing='warn') - _names_of_triggers = ConfigOption(name='names_of_triggers', default=None, missing='nothing') + + # names_of_triggers defined as list of strings + _names_of_triggers = ConfigOption(name='names_of_triggers', default=['one', 'two'], missing='nothing') + + # names_of_triggers defined as list of strings _trigger_length = StatusVar(name='trigger_length', default=1) def __init__(self, *args, **kwargs): From dc3167aa7e6b20e5599691a7dcbfc1260d8f7ce0 Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Fri, 30 Oct 2020 10:59:21 +0100 Subject: [PATCH 03/10] adding logic and gui for triggers --- config/example/default.cfg | 13 ++++ gui/trigger/trigger_gui.py | 126 ++++++++++++++++++++++++++++++++++ gui/trigger/ui_trigger_gui.ui | 81 ++++++++++++++++++++++ logic/trigger_logic.py | 80 +++++++++++++++++++++ 4 files changed, 300 insertions(+) create mode 100644 gui/trigger/trigger_gui.py create mode 100644 gui/trigger/ui_trigger_gui.ui create mode 100644 logic/trigger_logic.py diff --git a/config/example/default.cfg b/config/example/default.cfg index e11cd123b1..6dbf85fa10 100644 --- a/config/example/default.cfg +++ b/config/example/default.cfg @@ -81,6 +81,9 @@ hardware: mydummyswitch2: module.Class: 'switches.switch_dummy.SwitchDummy' + mydummytrigger: + module.Class: 'trigger_device.dummy_trigger.DummyTrigger' + myspectrometer: module.Class: 'spectrometer.spectrometer_dummy.SpectrometerInterfaceDummy' connect: @@ -185,6 +188,11 @@ logic: switch1: 'mydummyswitch1' switch2: 'mydummyswitch2' + triggerlogic: + module.Class: 'trigger_logic.TriggerLogic' + connect: + trigger_hardware: 'mydummytrigger' + scannerlogic: module.Class: 'confocal_logic.ConfocalLogic' connect: @@ -401,6 +409,11 @@ gui: connect: switchlogic: 'switchlogic' + trigger: + module.Class: 'trigger.trigger_gui.TriggerGui' + connect: + trigger_logic: 'triggerlogic' + taskrunner: module.Class: 'taskrunner.taskgui.TaskGui' connect: diff --git a/gui/trigger/trigger_gui.py b/gui/trigger/trigger_gui.py new file mode 100644 index 0000000000..bd4a4243dc --- /dev/null +++ b/gui/trigger/trigger_gui.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +""" +This file contains the Qudi console GUI module. + +Qudi is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Qudi 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Qudi. If not, see . + +Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the +top-level directory of this distribution and at +""" + +import os +from core.connector import Connector +from gui.guibase import GUIBase +from qtpy import QtWidgets, QtCore +from qtpy import uic +import sip + + +class TriggerMainWindow(QtWidgets.QMainWindow): + """ Create the Main Window based on the *.ui file. """ + + def __init__(self, **kwargs): + # Get the path to the *.ui file + this_dir = os.path.dirname(__file__) + ui_file = os.path.join(this_dir, 'ui_trigger_gui.ui') + + # Load it + super().__init__(**kwargs) + uic.loadUi(ui_file, self) + self.show() + + +class TriggerGui(GUIBase): + """ A graphical interface to trigger a hardware by hand. + """ + + # declare connectors + trigger_logic = Connector(interface='TriggerLogic') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._mw = TriggerMainWindow() + self._widgets = dict() + + def on_activate(self): + """ Create all UI objects and show the window. + """ + self.restoreWindowPos(self._mw) + self._populate_triggers() + self.show() + + def on_deactivate(self): + """ Hide window empty the GUI and disconnect signals + """ + + self._depopulate_triggers() + + self.saveWindowPos(self._mw) + self._mw.close() + + def show(self): + """ Make sure that the window is visible and at the top. + """ + self._mw.show() + + def _populate_triggers(self): + """ Dynamically build the gui. + @return: None + """ + # For each trigger the logic has, a button needs to be shown. + self._mw.trigger_groupBox.setTitle('Triggers') + self._mw.trigger_groupBox.setAlignment(QtCore.Qt.AlignLeft) + self._mw.trigger_groupBox.setFlat(False) + vertical_layout = QtWidgets.QVBoxLayout(self._mw.trigger_groupBox) + self._widgets = dict() + for trigger in self.trigger_logic().names_of_triggers: + widget = QtWidgets.QPushButton(trigger) + widget.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) + widget.setCheckable(True) + widget.setChecked(False) + widget.setFocusPolicy(QtCore.Qt.NoFocus) + + widget.clicked.connect(lambda button_state, trigger_origin=trigger: + self._button_toggled(trigger_origin, button_state)) + + self._widgets[trigger] = widget + vertical_layout.addWidget(widget) + + self._mw.trigger_groupBox.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.MinimumExpanding) + self._mw.trigger_groupBox.updateGeometry() + + def _depopulate_triggers(self): + """ Delete all the buttons from the group box and remove the layout. + @return: None + """ + for widgets in self._widgets.values(): + widgets.clicked.disconnect() + self._widgets = dict() + + vertical_layout = self._mw.trigger_groupBox.layout() + if vertical_layout is not None: + for i in reversed(range(vertical_layout.count())): + vertical_layout.itemAt(i).widget().setParent(None) + sip.delete(vertical_layout) + + def _button_toggled(self, trigger, is_set): + """ Helper function that is connected to the GUI interaction. + A GUI change is transmitted to the logic and the visual indicators are changed. + @param str trigger: name of the trigger toggled + @param bool is_set: indicator for the state of the button, ignored in this case + @return: None + """ + self.trigger_logic().trigger(trigger) + self._widgets[trigger].setChecked(False) diff --git a/gui/trigger/ui_trigger_gui.ui b/gui/trigger/ui_trigger_gui.ui new file mode 100644 index 0000000000..9a9e2f8561 --- /dev/null +++ b/gui/trigger/ui_trigger_gui.ui @@ -0,0 +1,81 @@ + + + MainWindow + + + + 0 + 0 + 105 + 73 + + + + qudi: Triggers + + + + + + + GroupBox + + + + + + + + + 0 + 0 + 105 + 25 + + + + + Menu + + + + + + + + + ../../artwork/icons/oxygen/22x22/application-exit.png../../artwork/icons/oxygen/22x22/application-exit.png + + + Close + + + + + true + + + Show &Radio Buttons + + + + + + + actionClose + triggered() + MainWindow + close() + + + -1 + -1 + + + 232 + 299 + + + + + diff --git a/logic/trigger_logic.py b/logic/trigger_logic.py new file mode 100644 index 0000000000..9ead4aa258 --- /dev/null +++ b/logic/trigger_logic.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +Qudi is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Qudi 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Qudi. If not, see . + +Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the +top-level directory of this distribution and at +""" + +from logic.generic_logic import GenericLogic +from core.connector import Connector +from interface.trigger_interface import TriggerInterface + + +class TriggerLogic(GenericLogic, TriggerInterface): + """ Logic module for interacting with the hardware switches. + This logic has the same structure as the SwitchInterface but supplies additional functionality: + - switches can either be manipulated by index or by their names + - signals are generated on state changes + """ + + # connector for one trigger, if multiple switches are needed use the TriggerCombinerInterfuse. + trigger_hardware = Connector(interface='TriggerInterface') + + def on_activate(self): + """ Prepare logic module for work. + """ + + def on_deactivate(self): + """ Deactivate module. + """ + + @property + def number_of_triggers(self): + """ The number of triggers provided by this hardware file. + @return int: number of triggers + """ + return self.trigger_hardware().number_of_triggers + + @property + def names_of_triggers(self): + """ Names of the triggers as list of strings. + @return list(str): names of the triggers + """ + return self.trigger_hardware().names_of_triggers + + def trigger(self, trigger=None): + """ Triggers the hardware. + The trigger is performed either + on all channels, if trigger is None, + on a single channel, if trigger is a single channel name or + on a list of channels, if trigger is a list of channel names. + @param [None/str/list(str)] trigger: trigger name to be triggered + @return int: negative error code or 0 at success + """ + return self.trigger_hardware().trigger(trigger) + + @property + def trigger_length(self): + """ Returns the length of all the triggers of this hardware in seconds. + @return int: length of the trigger + """ + return self.trigger_hardware().trigger_length + + @trigger_length.setter + def trigger_length(self, value): + """ Sets the trigger length in seconds. + @param float value: length of the trigger to be set + """ + self.trigger_hardware().trigger_length = value From 8371487da1c721c88d41fe0191b962540cd0a60d Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Thu, 12 Nov 2020 10:38:54 +0100 Subject: [PATCH 04/10] renaming folder for trigger hardware --- config/example/default.cfg | 2 +- hardware/{trigger_device => trigger}/dummy_trigger.py | 0 hardware/{trigger_device => trigger}/ni_trigger.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename hardware/{trigger_device => trigger}/dummy_trigger.py (100%) rename hardware/{trigger_device => trigger}/ni_trigger.py (100%) diff --git a/config/example/default.cfg b/config/example/default.cfg index 6dbf85fa10..4a63415fed 100644 --- a/config/example/default.cfg +++ b/config/example/default.cfg @@ -82,7 +82,7 @@ hardware: module.Class: 'switches.switch_dummy.SwitchDummy' mydummytrigger: - module.Class: 'trigger_device.dummy_trigger.DummyTrigger' + module.Class: 'trigger.dummy_trigger.DummyTrigger' myspectrometer: module.Class: 'spectrometer.spectrometer_dummy.SpectrometerInterfaceDummy' diff --git a/hardware/trigger_device/dummy_trigger.py b/hardware/trigger/dummy_trigger.py similarity index 100% rename from hardware/trigger_device/dummy_trigger.py rename to hardware/trigger/dummy_trigger.py diff --git a/hardware/trigger_device/ni_trigger.py b/hardware/trigger/ni_trigger.py similarity index 100% rename from hardware/trigger_device/ni_trigger.py rename to hardware/trigger/ni_trigger.py From 4f3dd3779a79f91fc405fbb608d4a7bf045667b8 Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Thu, 12 Nov 2020 11:00:31 +0100 Subject: [PATCH 05/10] adding signal to logic --- hardware/trigger/dummy_trigger.py | 4 ++-- logic/trigger_logic.py | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hardware/trigger/dummy_trigger.py b/hardware/trigger/dummy_trigger.py index 4c1d0e9a66..97de783475 100644 --- a/hardware/trigger/dummy_trigger.py +++ b/hardware/trigger/dummy_trigger.py @@ -83,8 +83,8 @@ def trigger(self, trigger=None): elif isinstance(trigger, (list, tuple, np.ndarray, set)): for index, item in enumerate(trigger): if item not in self.names_of_triggers: - self.log.error(f'trigger name "{trigger}" was requested, ' - f'but the options are: {self.names_of_triggers}') + self.log.warning(f'trigger name "{item}" was requested, ' + f'but the options are: {self.names_of_triggers}') del trigger[index] else: self.log.error(f'The trigger name was {trigger} but either has to be one of {self.names_of_triggers} ' diff --git a/logic/trigger_logic.py b/logic/trigger_logic.py index 9ead4aa258..571bac2c3d 100644 --- a/logic/trigger_logic.py +++ b/logic/trigger_logic.py @@ -20,6 +20,7 @@ from logic.generic_logic import GenericLogic from core.connector import Connector from interface.trigger_interface import TriggerInterface +from qtpy import QtCore class TriggerLogic(GenericLogic, TriggerInterface): @@ -29,6 +30,8 @@ class TriggerLogic(GenericLogic, TriggerInterface): - signals are generated on state changes """ + sig_trigger_done = QtCore.Signal(int, object) + # connector for one trigger, if multiple switches are needed use the TriggerCombinerInterfuse. trigger_hardware = Connector(interface='TriggerInterface') @@ -63,7 +66,9 @@ def trigger(self, trigger=None): @param [None/str/list(str)] trigger: trigger name to be triggered @return int: negative error code or 0 at success """ - return self.trigger_hardware().trigger(trigger) + results = self.trigger_hardware().trigger(trigger) + self.sig_trigger_done.emit(results, trigger) + return results @property def trigger_length(self): From 997552b49368439c2984126b2492873aae7e11a0 Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Thu, 12 Nov 2020 11:38:11 +0100 Subject: [PATCH 06/10] adding trigger_combiner_interfuse --- config/example/default.cfg | 13 +- gui/trigger/trigger_gui.py | 16 ++- hardware/trigger/dummy_trigger.py | 3 +- logic/interfuse/trigger_combiner_interfuse.py | 126 ++++++++++++++++++ 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 logic/interfuse/trigger_combiner_interfuse.py diff --git a/config/example/default.cfg b/config/example/default.cfg index 4a63415fed..1bd2977cbf 100644 --- a/config/example/default.cfg +++ b/config/example/default.cfg @@ -84,6 +84,11 @@ hardware: mydummytrigger: module.Class: 'trigger.dummy_trigger.DummyTrigger' + mydummytrigger2: + module.Class: 'trigger.dummy_trigger.DummyTrigger' + names_of_triggers: ['bang', 'one'] + trigger_length: 0 + myspectrometer: module.Class: 'spectrometer.spectrometer_dummy.SpectrometerInterfaceDummy' connect: @@ -191,7 +196,13 @@ logic: triggerlogic: module.Class: 'trigger_logic.TriggerLogic' connect: - trigger_hardware: 'mydummytrigger' + trigger_hardware: 'triggerinterfuse' + + triggerinterfuse: + module.Class: 'interfuse.trigger_combiner_interfuse.TriggerCombinerInterfuse' + connect: + trigger1: 'mydummytrigger' + trigger2: 'mydummytrigger2' scannerlogic: module.Class: 'confocal_logic.ConfocalLogic' diff --git a/gui/trigger/trigger_gui.py b/gui/trigger/trigger_gui.py index bd4a4243dc..175c2adba3 100644 --- a/gui/trigger/trigger_gui.py +++ b/gui/trigger/trigger_gui.py @@ -51,7 +51,7 @@ class TriggerGui(GUIBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._mw = TriggerMainWindow() - self._widgets = dict() + self._widgets = list() def on_activate(self): """ Create all UI objects and show the window. @@ -83,7 +83,7 @@ def _populate_triggers(self): self._mw.trigger_groupBox.setAlignment(QtCore.Qt.AlignLeft) self._mw.trigger_groupBox.setFlat(False) vertical_layout = QtWidgets.QVBoxLayout(self._mw.trigger_groupBox) - self._widgets = dict() + self._widgets = list() for trigger in self.trigger_logic().names_of_triggers: widget = QtWidgets.QPushButton(trigger) widget.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) @@ -94,7 +94,7 @@ def _populate_triggers(self): widget.clicked.connect(lambda button_state, trigger_origin=trigger: self._button_toggled(trigger_origin, button_state)) - self._widgets[trigger] = widget + self._widgets.append([trigger, widget]) vertical_layout.addWidget(widget) self._mw.trigger_groupBox.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, @@ -105,9 +105,9 @@ def _depopulate_triggers(self): """ Delete all the buttons from the group box and remove the layout. @return: None """ - for widgets in self._widgets.values(): - widgets.clicked.disconnect() - self._widgets = dict() + for widgets in self._widgets: + widgets[1].clicked.disconnect() + self._widgets = list() vertical_layout = self._mw.trigger_groupBox.layout() if vertical_layout is not None: @@ -123,4 +123,6 @@ def _button_toggled(self, trigger, is_set): @return: None """ self.trigger_logic().trigger(trigger) - self._widgets[trigger].setChecked(False) + for widget in self._widgets: + if trigger == widget[0]: + widget[1].setChecked(False) diff --git a/hardware/trigger/dummy_trigger.py b/hardware/trigger/dummy_trigger.py index 97de783475..0f884642f8 100644 --- a/hardware/trigger/dummy_trigger.py +++ b/hardware/trigger/dummy_trigger.py @@ -92,7 +92,7 @@ def trigger(self, trigger=None): return -2 for item in trigger: - self.log.info(f'Trigger on channel: {item}.') + self.log.info(f'Trigger on channel: {self._name}.{item}.') time.sleep(self.trigger_length) return 0 @@ -113,3 +113,4 @@ def trigger_length(self, value): self.log.error(f'trigger_length has to be of type float but was {value}.') return self._trigger_length = float(value) + self.log.info(f'trigger_length set to {self._trigger_length}') diff --git a/logic/interfuse/trigger_combiner_interfuse.py b/logic/interfuse/trigger_combiner_interfuse.py new file mode 100644 index 0000000000..f8d51cc545 --- /dev/null +++ b/logic/interfuse/trigger_combiner_interfuse.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +""" +Combine two hardware triggers into one. + +Qudi is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Qudi 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Qudi. If not, see . + +Copyright (c) the Qudi Developers. See the COPYRIGHT.txt file at the +top-level directory of this distribution and at +""" + +from core.module import Base +from core.configoption import ConfigOption +from core.connector import Connector +import numpy as np + +from interface.trigger_interface import TriggerInterface + + +class TriggerCombinerInterfuse(Base, TriggerInterface): + """ Combine two hardware triggers into one. + """ + + # connectors for the switches to be combined + trigger1 = Connector(interface='TriggerInterface') + trigger2 = Connector(interface='TriggerInterface') + + def on_activate(self): + """ Activate the module and fill status variables. + """ + pass + + def on_deactivate(self): + """ Deactivate the module and clean up. + """ + pass + + @property + def number_of_triggers(self): + """ The number of triggers provided by this hardware file. + @return int: number of triggers + """ + return self.trigger1().number_of_triggers + self.trigger2().number_of_triggers + + @property + def names_of_triggers(self): + """ Names of the triggers as list of strings. + @return list(str): names of the triggers + """ + return self.trigger1().names_of_triggers + self.trigger2().names_of_triggers + + def trigger(self, trigger=None): + """ Triggers the hardware. + The trigger is performed either + on all channels, if trigger is None, + on a single channel, if trigger is a single channel name or + on a list of channels, if trigger is a list of channel names. + @param [None/str/list(str)] trigger: trigger name to be triggered + @return int: negative error code or 0 at success + """ + if trigger is None: + return self.trigger1().trigger() + self.trigger2().trigger() + elif isinstance(trigger, str): + results = 0 + if trigger in self.trigger1().names_of_triggers: + results += self.trigger1().trigger(trigger) + if trigger in self.trigger2().names_of_triggers: + results += self.trigger2().trigger(trigger) + if trigger not in self.names_of_triggers: + self.log.error(f'trigger name "{trigger}" was requested, but the options are: {self.names_of_triggers}') + return -1 + return results + elif isinstance(trigger, (list, tuple, np.ndarray, set)): + trigger1 = list() + trigger2 = list() + for index, item in enumerate(trigger): + if item in self.trigger1().names_of_triggers: + trigger1.append(item) + if item in self.trigger2().names_of_triggers: + trigger2.append(item) + if item not in self.names_of_triggers: + self.log.warning(f'trigger name "{item}" was requested, ' + f'but the options are: {self.names_of_triggers}') + results = 0 + if len(trigger1): + results += self.trigger1().trigger(trigger1) + if len(trigger2): + results += self.trigger2().trigger(trigger2) + return results + else: + self.log.error(f'The trigger name was {trigger} but either has to be one of {self.names_of_triggers} ' + f'or a list of trigger names.') + return -2 + + @property + def trigger_length(self): + """ Returns the length of all the triggers of this hardware in seconds. + @return int: length of the trigger + """ + if self.trigger1().trigger_length != self.trigger2().trigger_length: + self.log.warning(f'trigger_length of individual triggers do not match. ' + f'Trigger1: {self.trigger1().trigger_length}, ' + f'Trigger2: {self.trigger2().trigger_length}') + return max(self.trigger1().trigger_length, self.trigger2().trigger_length) + + @trigger_length.setter + def trigger_length(self, value): + """ Sets the trigger length in seconds. + @param float value: length of the trigger to be set + """ + if not isinstance(value, (int, float)): + self.log.error(f'trigger_length has to be of type float but was {value}.') + return + self.trigger1().trigger_length = float(value) + self.trigger2().trigger_length = float(value) From 07b7b380092ce536500a616b2364690e5c8bcfca Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Thu, 12 Nov 2020 11:40:14 +0100 Subject: [PATCH 07/10] adding docu --- documentation/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/changelog.md b/documentation/changelog.md index 8d50dc1fb5..6ab5c2847a 100644 --- a/documentation/changelog.md +++ b/documentation/changelog.md @@ -87,6 +87,7 @@ please use _ni_x_series_in_streamer.py_ as hardware module. * Added a hardware file to interface Thorlabs filter wheels via scripts * Bug fixes to core: made error messages sticky, respecting dependencies when restarting. * Added a config option to regulate pid logic timestep length +* Added a new trigger interface and a logic and GUI as demonstration * From d17ace11f9cd5cb20547c21ebb8d07975b015eae Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Thu, 12 Nov 2020 12:19:50 +0100 Subject: [PATCH 08/10] adding default config --- hardware/trigger/dummy_trigger.py | 7 +++++++ hardware/trigger/ni_trigger.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/hardware/trigger/dummy_trigger.py b/hardware/trigger/dummy_trigger.py index 0f884642f8..addd9e5322 100644 --- a/hardware/trigger/dummy_trigger.py +++ b/hardware/trigger/dummy_trigger.py @@ -29,6 +29,13 @@ class DummyTrigger(Base, TriggerInterface): """ This is a dummy to simulate a simple trigger. + + Example config for copy-paste: + + dummy_trigger: + module.Class: 'trigger.dummy_trigger.DummyTrigger' + names_of_triggers: ['bang', 'one'] + trigger_length: 0 """ # ConfigOptions diff --git a/hardware/trigger/ni_trigger.py b/hardware/trigger/ni_trigger.py index 67ee99510f..f5b5c8daa1 100644 --- a/hardware/trigger/ni_trigger.py +++ b/hardware/trigger/ni_trigger.py @@ -39,6 +39,8 @@ class NITriggers(Base, TriggerInterface): ni_trigger: module.Class: 'trigger_device.ni_trigger.NITriggers' trigger_channel: 'Dev1/port0/line9:8' + names_of_triggers: ['one', 'two'] + trigger_length: 0.5 """ From 1177b32f606aac97bb5f04edad1bb7b94b108294 Mon Sep 17 00:00:00 2001 From: kay-jahnke Date: Thu, 26 Nov 2020 11:26:39 +0100 Subject: [PATCH 09/10] reworking gui to get rid of warnings --- gui/trigger/trigger_gui.py | 72 +++++++++++++++++-------------- gui/trigger/ui_trigger_gui.ui | 81 ----------------------------------- 2 files changed, 39 insertions(+), 114 deletions(-) delete mode 100644 gui/trigger/ui_trigger_gui.ui diff --git a/gui/trigger/trigger_gui.py b/gui/trigger/trigger_gui.py index 175c2adba3..0af8e11976 100644 --- a/gui/trigger/trigger_gui.py +++ b/gui/trigger/trigger_gui.py @@ -19,26 +19,40 @@ top-level directory of this distribution and at """ -import os from core.connector import Connector from gui.guibase import GUIBase -from qtpy import QtWidgets, QtCore -from qtpy import uic -import sip +from qtpy import QtWidgets, QtCore, QtGui class TriggerMainWindow(QtWidgets.QMainWindow): - """ Create the Main Window based on the *.ui file. """ + """ Main Window for the TriggerGui module """ - def __init__(self, **kwargs): - # Get the path to the *.ui file - this_dir = os.path.dirname(__file__) - ui_file = os.path.join(this_dir, 'ui_trigger_gui.ui') - - # Load it - super().__init__(**kwargs) - uic.loadUi(ui_file, self) - self.show() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.setWindowTitle('qudi: Trigger GUI') + # Create main layout and central widget + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) + self.main_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) + widget = QtWidgets.QWidget() + widget.setLayout(self.main_layout) + widget.setFixedSize(1, 1) + self.setCentralWidget(widget) + + # Create QActions and menu bar + menu_bar = QtWidgets.QMenuBar() + self.setMenuBar(menu_bar) + + menu = menu_bar.addMenu('Menu') + self.action_close = QtWidgets.QAction('Close Window') + self.action_close.setCheckable(False) + self.action_close.setIcon(QtGui.QIcon('artwork/icons/oxygen/22x22/application-exit.png')) + self.addAction(self.action_close) + menu.addAction(self.action_close) + + # close window upon triggering close action + self.action_close.triggered.connect(self.close) + return class TriggerGui(GUIBase): @@ -64,7 +78,7 @@ def on_deactivate(self): """ Hide window empty the GUI and disconnect signals """ - self._depopulate_triggers() + self._delete_triggers() self.saveWindowPos(self._mw) self._mw.close() @@ -78,14 +92,10 @@ def _populate_triggers(self): """ Dynamically build the gui. @return: None """ - # For each trigger the logic has, a button needs to be shown. - self._mw.trigger_groupBox.setTitle('Triggers') - self._mw.trigger_groupBox.setAlignment(QtCore.Qt.AlignLeft) - self._mw.trigger_groupBox.setFlat(False) - vertical_layout = QtWidgets.QVBoxLayout(self._mw.trigger_groupBox) self._widgets = list() for trigger in self.trigger_logic().names_of_triggers: widget = QtWidgets.QPushButton(trigger) + widget.setMinimumWidth(100) widget.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) widget.setCheckable(True) widget.setChecked(False) @@ -95,25 +105,21 @@ def _populate_triggers(self): self._button_toggled(trigger_origin, button_state)) self._widgets.append([trigger, widget]) - vertical_layout.addWidget(widget) - self._mw.trigger_groupBox.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding) - self._mw.trigger_groupBox.updateGeometry() + self._mw.main_layout.addWidget(widget) - def _depopulate_triggers(self): + def _delete_triggers(self): """ Delete all the buttons from the group box and remove the layout. @return: None """ - for widgets in self._widgets: - widgets[1].clicked.disconnect() - self._widgets = list() - vertical_layout = self._mw.trigger_groupBox.layout() - if vertical_layout is not None: - for i in reversed(range(vertical_layout.count())): - vertical_layout.itemAt(i).widget().setParent(None) - sip.delete(vertical_layout) + for index in reversed(range(len(self._widgets))): + trigger, widget = self._widgets[index] + widget.clicked.disconnect() + self._mw.main_layout.removeWidget(widget) + widget.setParent(None) + del self._widgets[index] + widget.deleteLater() def _button_toggled(self, trigger, is_set): """ Helper function that is connected to the GUI interaction. diff --git a/gui/trigger/ui_trigger_gui.ui b/gui/trigger/ui_trigger_gui.ui deleted file mode 100644 index 9a9e2f8561..0000000000 --- a/gui/trigger/ui_trigger_gui.ui +++ /dev/null @@ -1,81 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 105 - 73 - - - - qudi: Triggers - - - - - - - GroupBox - - - - - - - - - 0 - 0 - 105 - 25 - - - - - Menu - - - - - - - - - ../../artwork/icons/oxygen/22x22/application-exit.png../../artwork/icons/oxygen/22x22/application-exit.png - - - Close - - - - - true - - - Show &Radio Buttons - - - - - - - actionClose - triggered() - MainWindow - close() - - - -1 - -1 - - - 232 - 299 - - - - - From b28f0c529581d82532b767bb05c79f0aac26a454 Mon Sep 17 00:00:00 2001 From: Jan Binder Date: Fri, 27 Nov 2020 11:06:43 +0100 Subject: [PATCH 10/10] centering the buttons --- gui/trigger/trigger_gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/trigger/trigger_gui.py b/gui/trigger/trigger_gui.py index 0af8e11976..9237987300 100644 --- a/gui/trigger/trigger_gui.py +++ b/gui/trigger/trigger_gui.py @@ -33,10 +33,10 @@ def __init__(self, *args, **kwargs): # Create main layout and central widget self.main_layout = QtWidgets.QVBoxLayout() self.main_layout.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter) - self.main_layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize) widget = QtWidgets.QWidget() widget.setLayout(self.main_layout) - widget.setFixedSize(1, 1) + widget.setMinimumWidth(50) + widget.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self.setCentralWidget(widget) # Create QActions and menu bar @@ -95,7 +95,7 @@ def _populate_triggers(self): self._widgets = list() for trigger in self.trigger_logic().names_of_triggers: widget = QtWidgets.QPushButton(trigger) - widget.setMinimumWidth(100) + widget.setMinimumWidth(50) widget.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) widget.setCheckable(True) widget.setChecked(False)