From f3960eec429639e385ce19513b077fa75e6d8df6 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 28 Jul 2024 21:54:40 -0400 Subject: [PATCH 01/18] First version of new virtual device system. Digital controls tested. Still need to add mechanism for creating virtual devices. Inverted digital outputs probably do not work. --- blacs/plugins/virtual_device/__init__.py | 123 ++++++++++++++++++ .../virtual_device/virtual_device_tab.py | 71 ++++++++++ 2 files changed, 194 insertions(+) create mode 100644 blacs/plugins/virtual_device/__init__.py create mode 100644 blacs/plugins/virtual_device/virtual_device_tab.py diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py new file mode 100644 index 00000000..2fc73ce2 --- /dev/null +++ b/blacs/plugins/virtual_device/__init__.py @@ -0,0 +1,123 @@ +##################################################################### +# # +# /plugins/virtual_device/__init__.py # +# # +# Copyright 2024, Carter Turnbaugh # +# # +##################################################################### +import logging +import os +import subprocess +import threading +import sys +import time + +from qtutils import inmain, inmain_decorator + +import labscript_utils.h5_lock +import h5py + +import labscript_utils.properties as properties +from labscript_utils.connections import ConnectionTable +from zprocess import TimeoutError +from labscript_utils.ls_zprocess import Event +from blacs.plugins import PLUGINS_DIR, callback + +from .virtual_device_tab import VirtualDeviceTab + +name = "Virtual Device" +module = "virtual_device" # should be folder name +logger = logging.getLogger('BLACS.plugin.%s'%module) + +# Try to reconnect often in case a tab restarts +CONNECT_CHECK_INTERVAL = 0.1 + +class Plugin(object): + def __init__(self, initial_settings): + self.menu = None + self.notifications = {} + self.initial_settings = initial_settings + self.BLACS = None + self.disconnected_last = False + + self.virtual_devices = initial_settings.get('virtual_devices', {}) + + self.tab_dict = {} + + self.setup_complete = False + self.close_event = threading.Event() + self.reconnect_thread = threading.Thread(target=self.reconnect, args=(self.close_event,)) + self.reconnect_thread.daemon = True + + self.tab_restart_receiver = lambda dn, s=self: self.disconnect_widgets(dn) + + @inmain_decorator(True) + def connect_widgets(self): + if not self.setup_complete: + return + for name, vd_tab in self.tab_dict.items(): + vd_tab.connect_widgets() + for _, tab in self.BLACS['ui'].blacs.tablist.items(): + if hasattr(tab, 'connect_restart_receiver'): + tab.connect_restart_receiver(self.tab_restart_receiver) + + @inmain_decorator(True) + def disconnect_widgets(self, closing_device_name): + if not self.setup_complete: + return + self.BLACS['ui'].blacs.tablist[closing_device_name].disconnect_restart_receiver(self.tab_restart_receiver) + for name, vd_tab in self.tab_dict.items(): + vd_tab.disconnect_widgets(closing_device_name) + + def reconnect(self, stop_event): + while not stop_event.wait(CONNECT_CHECK_INTERVAL): + self.connect_widgets() + + def on_tab_layout_change(self): + return + + def plugin_setup_complete(self, BLACS): + self.BLACS = BLACS + + for name, vd_tab in self.tab_dict.items(): + vd_tab.create_widgets(self.BLACS['ui'].blacs.tablist, + self.virtual_devices[name]['AO'], + self.virtual_devices[name]['DO'], + self.virtual_devices[name]['DDS']) + + self.setup_complete = True + self.reconnect_thread.start() + + def get_save_data(self): + return {'virtual_devices': { + 'v0': {'AO': [], 'DO': [('christopher', 'GPIO 09'), ('pdo_0', '0x1')], 'DDS': []}, + 'v1': {'AO': [], 'DO': [('pdo_0', '0x0'), ('pdo_0', '0x2')], 'DDS': []}, + }} + + def get_tab_classes(self): + return {k: VirtualDeviceTab for k in self.virtual_devices.keys()} + + def tabs_created(self, tab_dict): + self.tab_dict = tab_dict + + def get_callbacks(self): + return {} + + def get_menu_class(self): + return None + + def get_notification_classes(self): + return [] + + def get_setting_classes(self): + return [] + + def set_menu_instance(self, menu): + self.menu = menu + + def set_notification_instances(self, notifications): + self.notifications = notifications + + def close(self): + self.close_event.set() + self.reconnect_thread.join() diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py new file mode 100644 index 00000000..8419d0a3 --- /dev/null +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -0,0 +1,71 @@ +from qtutils.qt.QtCore import * +from qtutils.qt.QtGui import * +from qtutils.qt.QtWidgets import * + +from labscript_utils.qtwidgets.toolpalette import ToolPaletteGroup + +from blacs.tab_base_classes import PluginTab + +class VirtualDeviceTab(PluginTab): + + def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): + self._blacs_tablist = blacs_tablist + self._AOs = {(AO[0], AO[1]): None for AO in AOs} + self._DOs = {(DO[0], DO[1]): None for DO in DOs} + self._DDSs = {(DDS[0], DDS[1]): None for DDS in DDSs} + + for AO in self._AOs.keys(): + if self._AOs[AO] is None: + self._AOs[AO] = self._blacs_tablist[AO[0]]._AO[AO[1]].create_widget(None, False, None) + self._AOs[AO].last_AO = None + + for DO in self._DOs.keys(): + if self._DOs[DO] is None: + self._DOs[DO] = self._blacs_tablist[DO[0]]._DO[DO[1]].create_widget() + self._DOs[DO].last_DO = None + + dds_widgets = [] + + if len(self._AOs) > 0: + self.place_widget_group('Analog Outputs', [v for k, v in self._AOs.items()]) + if len(self._DOs) > 0: + self.place_widget_group('Digital Outputs', [v for k, v in self._DOs.items()]) + + return + + def connect_widgets(self): + for AO in self._AOs.keys(): + if self._AOs[AO] is not None: + new_AO = self._blacs_tablist[AO[0]]._AO[AO[1]] + if self._AOs[AO].get_AO() is None and self._AOs[AO].last_AO != new_AO: + self._AOs[AO].set_AO(new_AO) + for DO in self._DOs.keys(): + if self._DOs[DO] is not None: + new_DO = self._blacs_tablist[DO[0]]._DO[DO[1]] + if self._DOs[DO].get_DO() is None and self._DOs[DO].last_DO != new_DO: + self._DOs[DO].set_DO(new_DO) + + def disconnect_widgets(self, closing_device_name): + for AO in self._AOs.keys(): + if AO[0] == closing_device_name: + self._AOs[AO].last_AO = self._AOs[AO].get_AO() + self._AOs[AO].set_AO(None) + for DO in self._DOs.keys(): + if DO[0] == closing_device_name: + self._DOs[DO].last_DO = self._DOs[DO].get_DO() + self._DOs[DO].set_DO(None) + + def place_widget_group(self, name, widgets): + widget = QWidget() + toolpalettegroup = ToolPaletteGroup(widget) + + if toolpalettegroup.has_palette(name): + toolpalette = toolpalettegroup.get_palette(name) + else: + toolpalette = toolpalettegroup.append_new_palette(name) + + for output_widget in widgets: + toolpalette.addWidget(output_widget, True) + + self.get_tab_layout().addWidget(widget) + self.get_tab_layout().addItem(QSpacerItem(0,0,QSizePolicy.Minimum,QSizePolicy.MinimumExpanding)) From 832dd24953b5c34e86c30097d49e394810de6058 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Fri, 2 Aug 2024 15:17:55 -0400 Subject: [PATCH 02/18] Add ability to handle inverted digital outputs --- blacs/output_classes.py | 15 ++++++++++++--- blacs/plugins/virtual_device/__init__.py | 10 +++++++--- .../plugins/virtual_device/virtual_device_tab.py | 4 ++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/blacs/output_classes.py b/blacs/output_classes.py index a45cba29..55a328f3 100644 --- a/blacs/output_classes.py +++ b/blacs/output_classes.py @@ -284,7 +284,9 @@ def remove_widget(self,widget,call_set_AO = True,new_AO = None): # Further cleanup widget.disconnect_value_change() + widget.block_combobox_signals() widget.set_combobox_model(QStandardItemModel()) + widget.unblock_combobox_signals() def change_unit(self,unit,program=True): # These values are always stored in base units! @@ -496,7 +498,8 @@ def create_widget(self, *args, **kwargs): def add_widget(self, widget, inverted=False): if widget not in self._widget_list: widget.set_DO(self,True,False) - widget.toggled.connect(self.set_value if not inverted else lambda state: self.set_value(not state)) + widget.toggled.connect(self.set_value if not inverted else self.set_value_inverted) + widget.connection_inverted = inverted self._widget_list.append(widget) self.set_value(self._current_state,False) self._update_lock(self._locked) @@ -507,7 +510,10 @@ def remove_widget(self,widget): if widget not in self._widget_list: # TODO: Make this error better! raise RuntimeError('The widget specified was not part of the DO object') - widget.toggled.disconnect(self.set_value) + if widget.connection_inverted: + widget.toggled.disconnect(self.set_value_inverted) + else: + widget.toggled.disconnect(self.set_value) self._widget_list.remove(widget) @property @@ -551,7 +557,10 @@ def set_value(self,state,program=True): widget.blockSignals(True) widget.state = state widget.blockSignals(False) - + + def set_value_inverted(self,state,program=True): + self.set_value(not state, program) + @property def name(self): return self._hardware_name + ' - ' + self._connection_name diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 2fc73ce2..6f9b800d 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -90,8 +90,8 @@ def plugin_setup_complete(self, BLACS): def get_save_data(self): return {'virtual_devices': { - 'v0': {'AO': [], 'DO': [('christopher', 'GPIO 09'), ('pdo_0', '0x1')], 'DDS': []}, - 'v1': {'AO': [], 'DO': [('pdo_0', '0x0'), ('pdo_0', '0x2')], 'DDS': []}, + 'v0': {'AO': [], 'DO': [('christopher', 'GPIO 09', False), ('pdo_0', '1', False)], 'DDS': []}, + 'v1': {'AO': [], 'DO': [('pdo_0', '0', False), ('pdo_0', '2', True)], 'DDS': []}, }} def get_tab_classes(self): @@ -120,4 +120,8 @@ def set_notification_instances(self, notifications): def close(self): self.close_event.set() - self.reconnect_thread.join() + try: + self.reconnect_thread.join() + except RuntimeError: + # reconnect_thread did not start, fail gracefully + pass diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index 8419d0a3..e2b04656 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -11,7 +11,7 @@ class VirtualDeviceTab(PluginTab): def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): self._blacs_tablist = blacs_tablist self._AOs = {(AO[0], AO[1]): None for AO in AOs} - self._DOs = {(DO[0], DO[1]): None for DO in DOs} + self._DOs = {(DO[0], DO[1], DO[2]): None for DO in DOs} self._DDSs = {(DDS[0], DDS[1]): None for DDS in DDSs} for AO in self._AOs.keys(): @@ -21,7 +21,7 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): for DO in self._DOs.keys(): if self._DOs[DO] is None: - self._DOs[DO] = self._blacs_tablist[DO[0]]._DO[DO[1]].create_widget() + self._DOs[DO] = self._blacs_tablist[DO[0]]._DO[DO[1]].create_widget(inverted=DO[2]) self._DOs[DO].last_DO = None dds_widgets = [] From fecf70edaf59c7b636831075833517887f120f6b Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Fri, 2 Aug 2024 20:30:31 -0400 Subject: [PATCH 03/18] Add empty menu --- blacs/plugins/virtual_device/__init__.py | 47 +++++++++++++++++-- .../virtual_device/virtual_device_menu.ui | 33 +++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 blacs/plugins/virtual_device/virtual_device_menu.ui diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 6f9b800d..844f8db9 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -17,6 +17,11 @@ import labscript_utils.h5_lock import h5py +from qtutils.qt.QtCore import * +from qtutils.qt.QtGui import * +from qtutils.qt.QtWidgets import * +from qtutils import * + import labscript_utils.properties as properties from labscript_utils.connections import ConnectionTable from zprocess import TimeoutError @@ -25,7 +30,7 @@ from .virtual_device_tab import VirtualDeviceTab -name = "Virtual Device" +name = "Virtual Devices" module = "virtual_device" # should be folder name logger = logging.getLogger('BLACS.plugin.%s'%module) @@ -104,13 +109,13 @@ def get_callbacks(self): return {} def get_menu_class(self): - return None + return Menu def get_notification_classes(self): return [] def get_setting_classes(self): - return [] + return [Setting] def set_menu_instance(self, menu): self.menu = menu @@ -125,3 +130,39 @@ def close(self): except RuntimeError: # reconnect_thread did not start, fail gracefully pass + +class Menu(object): + def __init__(self, BLACS): + self.BLACS = BLACS + + def get_menu_items(self): + return {'name': name, + 'menu_items': [{'name': 'Edit', + 'action': self.on_edit_virtual_devices, + 'icon': ':/qtutils/fugue/document--pencil' + } + ] + } + + def on_edit_virtual_devices(self, *args, **kwargs): + self.BLACS['settings'].create_dialog(goto_page=Setting) + + def close(self): + pass + +class Setting(object): + name = name + + def __init__(self, data): + self.data = data + + def create_dialog(self, notebook): + ui = UiLoader().load(os.path.join(PLUGINS_DIR, module, 'virtual_device_menu.ui')) + + return ui, None + + def save(self): + return self.data + + def close(self): + pass diff --git a/blacs/plugins/virtual_device/virtual_device_menu.ui b/blacs/plugins/virtual_device/virtual_device_menu.ui new file mode 100644 index 00000000..ae5f4acd --- /dev/null +++ b/blacs/plugins/virtual_device/virtual_device_menu.ui @@ -0,0 +1,33 @@ + + + Form + + + + 0 + 0 + 844 + 528 + + + + Form + + + + + + + 12 + + + + Virtual Devices + + + + + + + + From a9922d6639d1bb3250616c08139eb572fa74c796 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 4 Aug 2024 22:56:43 -0400 Subject: [PATCH 04/18] Add virtual device building menu and saving process --- blacs/plugins/virtual_device/__init__.py | 277 ++++++++++++++++-- .../virtual_device/virtual_device_menu.ui | 28 +- 2 files changed, 281 insertions(+), 24 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 844f8db9..9dc1fb2f 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -27,6 +27,7 @@ from zprocess import TimeoutError from labscript_utils.ls_zprocess import Event from blacs.plugins import PLUGINS_DIR, callback +from blacs.device_base_class import DeviceTab from .virtual_device_tab import VirtualDeviceTab @@ -46,6 +47,7 @@ def __init__(self, initial_settings): self.disconnected_last = False self.virtual_devices = initial_settings.get('virtual_devices', {}) + self.save_virtual_devices = self.virtual_devices self.tab_dict = {} @@ -93,11 +95,17 @@ def plugin_setup_complete(self, BLACS): self.setup_complete = True self.reconnect_thread.start() + def get_virtual_devices(self): + return self.virtual_devices + + def get_save_virtual_devices(self): + return self.save_virtual_devices + + def set_save_virtual_devices(self, save_virtual_devices): + self.save_virtual_devices = save_virtual_devices + def get_save_data(self): - return {'virtual_devices': { - 'v0': {'AO': [], 'DO': [('christopher', 'GPIO 09', False), ('pdo_0', '1', False)], 'DDS': []}, - 'v1': {'AO': [], 'DO': [('pdo_0', '0', False), ('pdo_0', '2', True)], 'DDS': []}, - }} + return {'virtual_devices': self.save_virtual_devices} def get_tab_classes(self): return {k: VirtualDeviceTab for k in self.virtual_devices.keys()} @@ -115,7 +123,7 @@ def get_notification_classes(self): return [] def get_setting_classes(self): - return [Setting] + return [] def set_menu_instance(self, menu): self.menu = menu @@ -132,9 +140,77 @@ def close(self): pass class Menu(object): + VD_TREE_DUMMY_ROW_TEXT = '' + + CT_TREE_COL_NAME = 0 + CT_TREE_COL_ADD = 1 + CT_TREE_ROLE_NAME = Qt.UserRole + 1 + CT_TREE_ROLE_DO_INVERTED = Qt.UserRole + 2 + + VD_TREE_COL_NAME = 0 + VD_TREE_COL_UP = 1 + VD_TREE_COL_DN = 2 + VD_TREE_COL_DELETE = 3 + VD_TREE_ROLE_IS_DUMMY_ROW = Qt.UserRole + 1 + VD_TREE_ROLE_DO_INVERTED = Qt.UserRole + 2 + + def _get_root_parent(item): + while item.parent() is not None: + item = item.parent() + return item + def __init__(self, BLACS): self.BLACS = BLACS + self.connection_table_model = QStandardItemModel() + self.connection_table_model.setHorizontalHeaderLabels(['Connection Table Devices', 'Add']) + self.connection_table_view = None + + # Construct tree from tablist and connection table + connection_table = ConnectionTable(self.BLACS['connection_table_h5file']) + for tab_name, tab in self.BLACS['ui'].blacs.tablist.items(): + if isinstance(tab, DeviceTab): + device_item = QStandardItem(tab_name) + self.connection_table_model.appendRow([device_item]) + + analog_outputs = QStandardItem('Analog Outputs') + device_item.appendRow(analog_outputs) + for AO_name, AO_dev in tab._AO.items(): + conn_table_dev = connection_table.find_by_name(AO_dev.name.split(' - ').pop(1)) + if conn_table_dev is None: + # Don't list devices not in the connection table to reduce clutter + continue + AO_item = QStandardItem(AO_dev.name) + add_to_vd_item = QStandardItem() + add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) + add_to_vd_item.setEditable(False) + add_to_vd_item.setToolTip('Add this output to selected virtual device') + add_to_vd_item.setData(AO_name, self.CT_TREE_ROLE_NAME) + analog_outputs.appendRow([AO_item, add_to_vd_item]) + + digital_outputs = QStandardItem('Digital Outputs') + device_item.appendRow(digital_outputs) + for DO_name, DO_dev in tab._DO.items(): + conn_table_dev = connection_table.find_by_name(DO_dev.name.split(' - ').pop(1)) + if conn_table_dev is None: + # Don't list devices not in the connection table to reduce clutter + continue + print(conn_table_dev.properties) + DO_item = QStandardItem(DO_dev.name) + add_to_vd_item = QStandardItem() + add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) + add_to_vd_item.setEditable(False) + add_to_vd_item.setToolTip('Add this output to selected virtual device') + add_to_vd_item.setData(DO_name, self.CT_TREE_ROLE_NAME) + inverted = conn_table_dev.properties['inverted'] if 'inverted' in conn_table_dev.properties else False + add_to_vd_item.setData(inverted, self.CT_TREE_ROLE_DO_INVERTED) + digital_outputs.appendRow([DO_item, add_to_vd_item]) + + self.virtual_device_model = QStandardItemModel() + self.virtual_device_model.setHorizontalHeaderLabels(['Virtual Devices', 'Up', 'Down', 'Remove']) + self.virtual_device_model.itemChanged.connect(self.on_virtual_devices_item_changed) + self.virtual_device_view = None + def get_menu_items(self): return {'name': name, 'menu_items': [{'name': 'Edit', @@ -144,25 +220,188 @@ def get_menu_items(self): ] } - def on_edit_virtual_devices(self, *args, **kwargs): - self.BLACS['settings'].create_dialog(goto_page=Setting) - - def close(self): - pass - -class Setting(object): - name = name + def make_virtual_device_output_row(self, name): + name_item = QStandardItem(name) + up_item = QStandardItem() + up_item.setIcon(QIcon(':qtutils/fugue/arrow-090')) + up_item.setEditable(False) + up_item.setToolTip('Move this output up in the virtual device output list') + dn_item = QStandardItem() + dn_item.setIcon(QIcon(':qtutils/fugue/arrow-270')) + dn_item.setEditable(False) + dn_item.setToolTip('Move this output down in the virtual device output list') + remove_item = QStandardItem() + remove_item.setIcon(QIcon(':qtutils/fugue/minus')) + remove_item.setEditable(False) + remove_item.setToolTip('Remove this output from the virtual device') + + return [name_item, up_item, dn_item, remove_item] + + def on_treeView_connection_table_clicked(self, index): + item = self.connection_table_model.itemFromIndex(index) + if item.column() == self.CT_TREE_COL_ADD: + # Add this output to the currently selected virtual devices + new_vd_output = QStandardItem('{}.{}'.format(item.parent().parent().text(), + item.data(self.CT_TREE_ROLE_NAME))) + if item.data(self.CT_TREE_ROLE_DO_INVERTED) is not None: + new_vd_output.setData(item.data(self.CT_TREE_ROLE_DO_INVERTED), self.VD_TREE_ROLE_DO_INVERTED) + + complete_vds = [] + for i in self.virtual_device_view.selectedIndexes(): + vd = Menu._get_root_parent(self.virtual_device_model.itemFromIndex(i)) + if vd.text() in complete_vds: + continue + complete_vds.append(vd.text()) + + for r in range(0, vd.rowCount()): + if vd.child(r, self.VD_TREE_COL_NAME).text() != item.parent().text(): + continue + + vd.child(r, self.VD_TREE_COL_NAME).appendRow(self.make_virtual_device_output_row(new_vd_output)) + + def on_virtual_devices_item_changed(self, item): + if item.column() != self.VD_TREE_COL_NAME or not item.data(self.VD_TREE_ROLE_IS_DUMMY_ROW): + # Item rearrangement, nothing we need to do. + return - def __init__(self, data): - self.data = data + if item.text() != self.VD_TREE_DUMMY_ROW_TEXT: + # If dummy row text has changed, use this as name of new virtual device and add it + new_vd_name = item.text() + if len(self.virtual_device_model.findItems(new_vd_name)) > 1: + QMessageBox.warning(self.BLACS['ui'], 'Unable to add virtual device', + 'Unable to add virtual device, name {} already in use'.format(new_vd_name)) + item.setText(self.VD_TREE_DUMMY_ROW_TEXT) + return + + new_device_item = QStandardItem(new_vd_name) + remove_item = QStandardItem() + remove_item.setIcon(QIcon(':qtutils/fugue/minus')) + remove_item.setEditable(False) + remove_item.setToolTip('Remove this virtual device') + self.virtual_device_model.insertRow(self.virtual_device_model.rowCount() - 1, + [new_device_item, None, None, remove_item]) + new_device_item.appendRow(QStandardItem('Analog Outputs')) + new_device_item.appendRow(QStandardItem('Digital Outputs')) + + item.setText(self.VD_TREE_DUMMY_ROW_TEXT) + + def on_treeView_virtual_devices_clicked(self, index): + item = self.virtual_device_model.itemFromIndex(index) + if item.data(self.VD_TREE_ROLE_IS_DUMMY_ROW): + name_index = index.sibling(index.row(), self.VD_TREE_COL_NAME) + name_item = self.virtual_device_model.itemFromIndex(name_index) + self.virtual_device_view.setCurrentIndex(name_index) + self.virtual_device_view.edit(name_index) + return + elif item.column() == self.VD_TREE_COL_UP: + if index.row() > 0: + item.parent().insertRow(index.row()-1, item.parent().takeRow(index.row())) + elif item.column() == self.VD_TREE_COL_DN: + if index.row() < item.parent().rowCount()-1: + item.parent().insertRow(index.row()+1, item.parent().takeRow(index.row())) + elif item.column() == self.VD_TREE_COL_DELETE: + item.parent().removeRow(index.row()) - def create_dialog(self, notebook): + def on_edit_virtual_devices(self, *args, **kwargs): + # Construct tree of virtual devices + # This happens here so that the tree is up to date + for vd_name, vd in self.BLACS['plugins'][module].get_save_virtual_devices().items(): + device_item = QStandardItem(vd_name) + self.virtual_device_model.appendRow([device_item]) + + analog_outputs = QStandardItem('Analog Outputs') + device_item.appendRow(analog_outputs) + for AO in vd['AO']: + analog_outputs.appendRow(self.make_virtual_device_output_row(AO[0] + '.' + AO[1])) + + digital_outputs = QStandardItem('Digital Outputs') + device_item.appendRow(digital_outputs) + for DO in vd['DO']: + digital_outputs.appendRow(self.make_virtual_device_output_row(DO[0] + '.' + DO[1])) + + add_vd_item = QStandardItem(self.VD_TREE_DUMMY_ROW_TEXT) + add_vd_item.setData(True, self.VD_TREE_ROLE_IS_DUMMY_ROW) + add_vd_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.virtual_device_model.appendRow([add_vd_item]) + + edit_dialog = QDialog(self.BLACS['ui']) + edit_dialog.setModal(True) + edit_dialog.accepted.connect(self.on_save) + edit_dialog.rejected.connect(self.on_cancel) + edit_dialog.setWindowTitle('Virtual Device Builder') + # Remove the help flag next to the [X] close button + edit_dialog.setWindowFlags(edit_dialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) + + layout = QVBoxLayout(edit_dialog) ui = UiLoader().load(os.path.join(PLUGINS_DIR, module, 'virtual_device_menu.ui')) + layout.addWidget(ui) + + ui.treeView_connection_table.setModel(self.connection_table_model) + ui.treeView_connection_table.setAnimated(True) + ui.treeView_connection_table.setSelectionMode(QTreeView.ExtendedSelection) + ui.treeView_connection_table.setSortingEnabled(False) + ui.treeView_connection_table.setColumnWidth(self.CT_TREE_COL_NAME, 200) + ui.treeView_connection_table.clicked.connect(self.on_treeView_connection_table_clicked) + for column in range(1, self.connection_table_model.columnCount()): + ui.treeView_connection_table.resizeColumnToContents(column) + self.connection_table_view = ui.treeView_connection_table + + ui.treeView_virtual_devices.setModel(self.virtual_device_model) + ui.treeView_virtual_devices.setAnimated(True) + ui.treeView_virtual_devices.setSelectionMode(QTreeView.ExtendedSelection) + ui.treeView_virtual_devices.setSortingEnabled(False) + ui.treeView_virtual_devices.setColumnWidth(self.VD_TREE_COL_NAME, 200) + ui.treeView_virtual_devices.clicked.connect(self.on_treeView_virtual_devices_clicked) + for column in range(1, self.virtual_device_model.columnCount()): + ui.treeView_virtual_devices.resizeColumnToContents(column) + self.virtual_device_view = ui.treeView_virtual_devices + + # Add OK/cancel buttons + widget = QWidget() + hlayout = QHBoxLayout(widget) + button_box = QDialogButtonBox() + button_box.setStandardButtons(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(edit_dialog.accept) + button_box.rejected.connect(edit_dialog.reject) + hlayout.addItem(QSpacerItem(0,0,QSizePolicy.MinimumExpanding,QSizePolicy.Minimum)) + hlayout.addWidget(button_box) + layout.addWidget(widget) + + edit_dialog.show() - return ui, None + return - def save(self): - return self.data + def _encode_virtual_devices(self): + virtual_device_data = {} + root = self.virtual_device_model.invisibleRootItem() + for i in range(root.rowCount()): + vd = root.child(i) + if vd.text() == self.VD_TREE_DUMMY_ROW_TEXT: + continue + + virtual_device_data[vd.text()] = {'AO': [], 'DO': [], 'DDS': []} + for j in range(vd.rowCount()): + output_group = vd.child(j) + if output_group.text() == 'Analog Outputs': + for k in range(output_group.rowCount()): + AO_name = output_group.child(k).text().split('.') + virtual_device_data[vd.text()]['AO'].append((AO_name[0], AO_name[1])) + elif output_group.text() == 'Digital Outputs': + for k in range(output_group.rowCount()): + DO_name = output_group.child(k).text().split('.') + inverted = output_group.child(k).data(self.VD_TREE_ROLE_DO_INVERTED) + virtual_device_data[vd.text()]['DO'].append((DO_name[0], DO_name[1], inverted)) + + return virtual_device_data + + def on_save(self): + self.BLACS['plugins'][module].set_save_virtual_devices(self._encode_virtual_devices()) + QMessageBox.information(self.BLACS['ui'], 'Virtual Devices Saved', + 'New virtual devices saved. Please restart BLACS to load new devices.') + + def on_cancel(self): + QMessageBox.information(self.BLACS['ui'], 'Virtual Devices Not Saved', + 'Editing of virtual devices canceled.') def close(self): pass diff --git a/blacs/plugins/virtual_device/virtual_device_menu.ui b/blacs/plugins/virtual_device/virtual_device_menu.ui index ae5f4acd..b58287de 100644 --- a/blacs/plugins/virtual_device/virtual_device_menu.ui +++ b/blacs/plugins/virtual_device/virtual_device_menu.ui @@ -6,26 +6,44 @@ 0 0 - 844 - 528 + 800 + 900 Form - + - + 12 - Virtual Devices + Edit Virtual Devices + + + + + + true + + + + + + + true + + + + + From 9b0a785742d21a34144b9ef6d04632ccd202ee3c Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 5 Aug 2024 12:12:55 -0400 Subject: [PATCH 05/18] Stop using private variables --- blacs/plugins/virtual_device/__init__.py | 78 ++++++++++++++----- .../virtual_device/virtual_device_tab.py | 8 +- 2 files changed, 61 insertions(+), 25 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 9dc1fb2f..4dcab529 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -28,6 +28,9 @@ from labscript_utils.ls_zprocess import Event from blacs.plugins import PLUGINS_DIR, callback from blacs.device_base_class import DeviceTab +from blacs.output_classes import AO as AO_output_class +from blacs.output_classes import DO as DO_output_class +from blacs.output_classes import DDS as DDS_output_class from .virtual_device_tab import VirtualDeviceTab @@ -159,6 +162,35 @@ def _get_root_parent(item): item = item.parent() return item + def _get_child_outputs(self, conn_table, root_devs, dev_name, tab): + AOs = [] + DOs = [] + DDSs = [] + + device_conn = conn_table.find_by_name(dev_name) + for child in device_conn.child_list.keys(): + if child in root_devs: + # Don't cross between tabs here + continue + + child_dev = device_conn.find_by_name(child) + channel = tab.get_channel(child_dev.parent_port) + if channel is None: + AOs_, DOs_, DDSs_ = self._get_child_outputs(conn_table, root_devs, child, tab) + + AOs += AOs_ + DOs += DOs_ + DDSs += DDSs_ + elif isinstance(channel, DO_output_class): + inv = child_dev.properties['inverted'] if 'inverted' in child_dev.properties else False + DOs.append((child, child_dev.parent_port, inv)) + elif isinstance(channel, AO_output_class): + AOs.append((child, child_dev.parent_port)) + elif isinstance(channel, DDS_output_class): + DDSS.append((child, child_dev.parent_port)) + + return AOs, DOs, DDSs + def __init__(self, BLACS): self.BLACS = BLACS @@ -174,37 +206,41 @@ def __init__(self, BLACS): self.connection_table_model.appendRow([device_item]) analog_outputs = QStandardItem('Analog Outputs') + digital_outputs = QStandardItem('Digital Outputs') + dds_outputs = QStandardItem('DDS Outputs') + device_item.appendRow(analog_outputs) - for AO_name, AO_dev in tab._AO.items(): - conn_table_dev = connection_table.find_by_name(AO_dev.name.split(' - ').pop(1)) - if conn_table_dev is None: - # Don't list devices not in the connection table to reduce clutter - continue - AO_item = QStandardItem(AO_dev.name) + device_item.appendRow(digital_outputs) + device_item.appendRow(dds_outputs) + + root_devs = self.BLACS['ui'].blacs.tablist.keys() + AOs, DOs, DDSs = self._get_child_outputs(connection_table, root_devs, tab_name, tab) + + for DO in DOs: + DO_item = QStandardItem(DO[1] + ' - ' + DO[0]) + add_to_vd_item = QStandardItem() + add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) + add_to_vd_item.setEditable(False) + add_to_vd_item.setToolTip('Add this output to selected virtual device') + add_to_vd_item.setData(DO[1], self.CT_TREE_ROLE_NAME) + add_to_vd_item.setData(DO[2], self.CT_TREE_ROLE_DO_INVERTED) + digital_outputs.appendRow([DO_item, add_to_vd_item]) + for AO in AOs: + AO_item = QStandardItem(AO[1] + ' - ' + AO[0]) add_to_vd_item = QStandardItem() add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) add_to_vd_item.setEditable(False) add_to_vd_item.setToolTip('Add this output to selected virtual device') - add_to_vd_item.setData(AO_name, self.CT_TREE_ROLE_NAME) + add_to_vd_item.setData(AO[1], self.CT_TREE_ROLE_NAME) analog_outputs.appendRow([AO_item, add_to_vd_item]) - - digital_outputs = QStandardItem('Digital Outputs') - device_item.appendRow(digital_outputs) - for DO_name, DO_dev in tab._DO.items(): - conn_table_dev = connection_table.find_by_name(DO_dev.name.split(' - ').pop(1)) - if conn_table_dev is None: - # Don't list devices not in the connection table to reduce clutter - continue - print(conn_table_dev.properties) - DO_item = QStandardItem(DO_dev.name) + for DDS in DDSs: + DDS_item = QStandardItem(DDS[1] + ' - ' + DDS[0]) add_to_vd_item = QStandardItem() add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) add_to_vd_item.setEditable(False) add_to_vd_item.setToolTip('Add this output to selected virtual device') - add_to_vd_item.setData(DO_name, self.CT_TREE_ROLE_NAME) - inverted = conn_table_dev.properties['inverted'] if 'inverted' in conn_table_dev.properties else False - add_to_vd_item.setData(inverted, self.CT_TREE_ROLE_DO_INVERTED) - digital_outputs.appendRow([DO_item, add_to_vd_item]) + add_to_vd_item.setData(DDS[1], self.CT_TREE_ROLE_NAME) + dds_outputs.appendRow([DDS_item, add_to_vd_item]) self.virtual_device_model = QStandardItemModel() self.virtual_device_model.setHorizontalHeaderLabels(['Virtual Devices', 'Up', 'Down', 'Remove']) diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index e2b04656..bbb9bcbc 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -16,12 +16,12 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): for AO in self._AOs.keys(): if self._AOs[AO] is None: - self._AOs[AO] = self._blacs_tablist[AO[0]]._AO[AO[1]].create_widget(None, False, None) + self._AOs[AO] = self._blacs_tablist[AO[0]].get_channel(AO[1]).create_widget(None, False, None) self._AOs[AO].last_AO = None for DO in self._DOs.keys(): if self._DOs[DO] is None: - self._DOs[DO] = self._blacs_tablist[DO[0]]._DO[DO[1]].create_widget(inverted=DO[2]) + self._DOs[DO] = self._blacs_tablist[DO[0]].get_channel(DO[1]).create_widget(inverted=DO[2]) self._DOs[DO].last_DO = None dds_widgets = [] @@ -36,12 +36,12 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): def connect_widgets(self): for AO in self._AOs.keys(): if self._AOs[AO] is not None: - new_AO = self._blacs_tablist[AO[0]]._AO[AO[1]] + new_AO = self._blacs_tablist[AO[0]].get_channel(AO[1]) if self._AOs[AO].get_AO() is None and self._AOs[AO].last_AO != new_AO: self._AOs[AO].set_AO(new_AO) for DO in self._DOs.keys(): if self._DOs[DO] is not None: - new_DO = self._blacs_tablist[DO[0]]._DO[DO[1]] + new_DO = self._blacs_tablist[DO[0]].get_channel(DO[1]) if self._DOs[DO].get_DO() is None and self._DOs[DO].last_DO != new_DO: self._DOs[DO].set_DO(new_DO) From d80ae3d91431d52eb72b8aeb0bc1daa3be38b8a9 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 5 Aug 2024 12:20:51 -0400 Subject: [PATCH 06/18] Prevent duplicate outputs in virtual devices --- blacs/plugins/virtual_device/__init__.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 4dcab529..aa715cee 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -290,10 +290,20 @@ def on_treeView_connection_table_clicked(self, index): complete_vds.append(vd.text()) for r in range(0, vd.rowCount()): - if vd.child(r, self.VD_TREE_COL_NAME).text() != item.parent().text(): + output_group = vd.child(r, self.VD_TREE_COL_NAME) + if output_group.text() != item.parent().text(): continue - vd.child(r, self.VD_TREE_COL_NAME).appendRow(self.make_virtual_device_output_row(new_vd_output)) + # Avoid duplicating outputs in a virtual device + already_present = False + for j in range(0, output_group.rowCount()): + if output_group.child(j).text() == new_vd_output.text(): + already_present = True + break + if already_present: + continue + + output_group.appendRow(self.make_virtual_device_output_row(new_vd_output)) def on_virtual_devices_item_changed(self, item): if item.column() != self.VD_TREE_COL_NAME or not item.data(self.VD_TREE_ROLE_IS_DUMMY_ROW): From b780600bd6349d01a97604920f5b3a2104d2278d Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sat, 14 Sep 2024 15:56:16 -0400 Subject: [PATCH 07/18] Virtual Device DigitalOutputs contain full name of connection --- blacs/plugins/virtual_device/virtual_device_tab.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index bbb9bcbc..3cbb922b 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -22,6 +22,8 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): for DO in self._DOs.keys(): if self._DOs[DO] is None: self._DOs[DO] = self._blacs_tablist[DO[0]].get_channel(DO[1]).create_widget(inverted=DO[2]) + orig_label = self._DOs[DO].text().split('\n') + self._DOs[DO].setText('%s\n%s'%(DO[0]+'.'+orig_label[0], orig_label[1])) self._DOs[DO].last_DO = None dds_widgets = [] From 7abc4f33b43d95438e0d065ac650b98b00fa0768 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sat, 14 Sep 2024 16:06:04 -0400 Subject: [PATCH 08/18] Virtual Device AnalogOutputs contain full name of connection --- blacs/plugins/virtual_device/virtual_device_tab.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index 3cbb922b..f18e1cea 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -16,7 +16,9 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): for AO in self._AOs.keys(): if self._AOs[AO] is None: - self._AOs[AO] = self._blacs_tablist[AO[0]].get_channel(AO[1]).create_widget(None, False, None) + chan = self._blacs_tablist[AO[0]].get_channel(AO[1]) + orig_label = chan.name.split('-') + self._AOs[AO] = chan.create_widget('%s\n%s'%(AO[0]+'.'+orig_label[0], orig_label[1]), False, None) self._AOs[AO].last_AO = None for DO in self._DOs.keys(): From cf7c86f10812b3c9b442a2f7417f8b9f09f4a4bf Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sat, 14 Sep 2024 16:13:29 -0400 Subject: [PATCH 09/18] Virtual Device builder now has device names in virtual devices side. --- blacs/plugins/virtual_device/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index aa715cee..a47a8467 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -222,7 +222,7 @@ def __init__(self, BLACS): add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) add_to_vd_item.setEditable(False) add_to_vd_item.setToolTip('Add this output to selected virtual device') - add_to_vd_item.setData(DO[1], self.CT_TREE_ROLE_NAME) + add_to_vd_item.setData(DO[1] + ' - ' + DO[0], self.CT_TREE_ROLE_NAME) add_to_vd_item.setData(DO[2], self.CT_TREE_ROLE_DO_INVERTED) digital_outputs.appendRow([DO_item, add_to_vd_item]) for AO in AOs: @@ -231,7 +231,7 @@ def __init__(self, BLACS): add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) add_to_vd_item.setEditable(False) add_to_vd_item.setToolTip('Add this output to selected virtual device') - add_to_vd_item.setData(AO[1], self.CT_TREE_ROLE_NAME) + add_to_vd_item.setData(AO[1] + ' - ' + AO[0], self.CT_TREE_ROLE_NAME) analog_outputs.appendRow([AO_item, add_to_vd_item]) for DDS in DDSs: DDS_item = QStandardItem(DDS[1] + ' - ' + DDS[0]) @@ -239,7 +239,7 @@ def __init__(self, BLACS): add_to_vd_item.setIcon(QIcon(':qtutils/fugue/arrow')) add_to_vd_item.setEditable(False) add_to_vd_item.setToolTip('Add this output to selected virtual device') - add_to_vd_item.setData(DDS[1], self.CT_TREE_ROLE_NAME) + add_to_vd_item.setData(DDS[1] + ' - ' + DDS[0], self.CT_TREE_ROLE_NAME) dds_outputs.appendRow([DDS_item, add_to_vd_item]) self.virtual_device_model = QStandardItemModel() @@ -430,11 +430,11 @@ def _encode_virtual_devices(self): output_group = vd.child(j) if output_group.text() == 'Analog Outputs': for k in range(output_group.rowCount()): - AO_name = output_group.child(k).text().split('.') + AO_name = output_group.child(k).text().split(' - ')[0].split('.') virtual_device_data[vd.text()]['AO'].append((AO_name[0], AO_name[1])) elif output_group.text() == 'Digital Outputs': for k in range(output_group.rowCount()): - DO_name = output_group.child(k).text().split('.') + DO_name = output_group.child(k).text().split(' - ')[0].split('.') inverted = output_group.child(k).data(self.VD_TREE_ROLE_DO_INVERTED) virtual_device_data[vd.text()]['DO'].append((DO_name[0], DO_name[1], inverted)) From 6ab8fd4a981a51799869962aa599c804999a0acb Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 30 Sep 2024 17:44:58 -0400 Subject: [PATCH 10/18] Display complete name in virtual device editorfor existing virtual devices. --- blacs/plugins/virtual_device/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index a47a8467..d24a96e4 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -358,12 +358,14 @@ def on_edit_virtual_devices(self, *args, **kwargs): analog_outputs = QStandardItem('Analog Outputs') device_item.appendRow(analog_outputs) for AO in vd['AO']: - analog_outputs.appendRow(self.make_virtual_device_output_row(AO[0] + '.' + AO[1])) + chan = self.BLACS['ui'].blacs.tablist[AO[0]].get_channel(AO[1]) + analog_outputs.appendRow(self.make_virtual_device_output_row(AO[0] + '.' + chan.name)) digital_outputs = QStandardItem('Digital Outputs') device_item.appendRow(digital_outputs) for DO in vd['DO']: - digital_outputs.appendRow(self.make_virtual_device_output_row(DO[0] + '.' + DO[1])) + chan = self.BLACS['ui'].blacs.tablist[DO[0]].get_channel(DO[1]) + digital_outputs.appendRow(self.make_virtual_device_output_row(DO[0] + '.' + chan.name)) add_vd_item = QStandardItem(self.VD_TREE_DUMMY_ROW_TEXT) add_vd_item.setData(True, self.VD_TREE_ROLE_IS_DUMMY_ROW) From 0ae5f7f3ebdee6cd79a1047d5edd931681f496ef Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Wed, 9 Oct 2024 17:16:44 -0400 Subject: [PATCH 11/18] More comments --- blacs/plugins/virtual_device/__init__.py | 21 +++++++++++++++++++ .../virtual_device/virtual_device_tab.py | 14 ++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index d24a96e4..119abafc 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -56,13 +56,22 @@ def __init__(self, initial_settings): self.setup_complete = False self.close_event = threading.Event() + # If the virtual device starts after a device starts, it needs to connect its widgets. + # So we start a background thread to periodically check if we have any non-connected widgets and connect them. self.reconnect_thread = threading.Thread(target=self.reconnect, args=(self.close_event,)) self.reconnect_thread.daemon = True + # If a tab (device) is restarted, it recreates its outputs. + # If the virtual device still has widgets referencing those outputs, they will fail. + # So, we need to disconnect them. self.tab_restart_receiver = lambda dn, s=self: self.disconnect_widgets(dn) @inmain_decorator(True) def connect_widgets(self): + ''' + For each of our tabs, tell connect its widgets to outputs. + Also connect restart receivers so we can detect if new tabs start to close. + ''' if not self.setup_complete: return for name, vd_tab in self.tab_dict.items(): @@ -73,6 +82,9 @@ def connect_widgets(self): @inmain_decorator(True) def disconnect_widgets(self, closing_device_name): + ''' + For each of our tabs, disconnect it from closing_device_name + ''' if not self.setup_complete: return self.BLACS['ui'].blacs.tablist[closing_device_name].disconnect_restart_receiver(self.tab_restart_receiver) @@ -163,6 +175,12 @@ def _get_root_parent(item): return item def _get_child_outputs(self, conn_table, root_devs, dev_name, tab): + ''' + Get all child outputs of a device. + + This is more complex than simply accessing `child_list` for the device + as some devices have structures between themselves and their ultimate outputs. + ''' AOs = [] DOs = [] DDSs = [] @@ -420,6 +438,9 @@ def on_edit_virtual_devices(self, *args, **kwargs): return def _encode_virtual_devices(self): + ''' + Convert data in model to a simple dictionary that can be saved. + ''' virtual_device_data = {} root = self.virtual_device_model.invisibleRootItem() for i in range(root.rowCount()): diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index f18e1cea..3032f173 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -9,6 +9,10 @@ class VirtualDeviceTab(PluginTab): def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): + ''' + This function sets up the tab, and should be called as soon as the plugin is otherwise ready. + Here, we create dictionaries of widgets (initially connecting them to outputs). + ''' self._blacs_tablist = blacs_tablist self._AOs = {(AO[0], AO[1]): None for AO in AOs} self._DOs = {(DO[0], DO[1], DO[2]): None for DO in DOs} @@ -28,7 +32,7 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): self._DOs[DO].setText('%s\n%s'%(DO[0]+'.'+orig_label[0], orig_label[1])) self._DOs[DO].last_DO = None - dds_widgets = [] + dds_widgets = [] # TODO if len(self._AOs) > 0: self.place_widget_group('Analog Outputs', [v for k, v in self._AOs.items()]) @@ -38,6 +42,10 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): return def connect_widgets(self): + ''' + For each of our widgets, check if it is connected to an output. + If not, connect it. + ''' for AO in self._AOs.keys(): if self._AOs[AO] is not None: new_AO = self._blacs_tablist[AO[0]].get_channel(AO[1]) @@ -50,6 +58,10 @@ def connect_widgets(self): self._DOs[DO].set_DO(new_DO) def disconnect_widgets(self, closing_device_name): + ''' + For each of our widgets, check if it connects to an output in 'closing_device_name'. + If it is, disconnect it so that 'closing_device_name' can be safely closed. + ''' for AO in self._AOs.keys(): if AO[0] == closing_device_name: self._AOs[AO].last_AO = self._AOs[AO].get_AO() From 5d692bf0b43394f7e06b2ed5f57f3d05630e7eb3 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 13 Apr 2025 12:16:00 -0400 Subject: [PATCH 12/18] Now handling DDS devices --- blacs/plugins/virtual_device/__init__.py | 13 ++++++++- .../virtual_device/virtual_device_tab.py | 27 ++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 119abafc..d972f8f8 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -205,7 +205,7 @@ def _get_child_outputs(self, conn_table, root_devs, dev_name, tab): elif isinstance(channel, AO_output_class): AOs.append((child, child_dev.parent_port)) elif isinstance(channel, DDS_output_class): - DDSS.append((child, child_dev.parent_port)) + DDSs.append((child, child_dev.parent_port)) return AOs, DOs, DDSs @@ -346,6 +346,7 @@ def on_virtual_devices_item_changed(self, item): [new_device_item, None, None, remove_item]) new_device_item.appendRow(QStandardItem('Analog Outputs')) new_device_item.appendRow(QStandardItem('Digital Outputs')) + new_device_item.appendRow(QStandardItem('DDS Outputs')) item.setText(self.VD_TREE_DUMMY_ROW_TEXT) @@ -385,6 +386,12 @@ def on_edit_virtual_devices(self, *args, **kwargs): chan = self.BLACS['ui'].blacs.tablist[DO[0]].get_channel(DO[1]) digital_outputs.appendRow(self.make_virtual_device_output_row(DO[0] + '.' + chan.name)) + dds_outputs = QStandardItem('DDS Outputs') + device_item.appendRow(dds_outputs) + for DDS in vd['DDS']: + chan = self.BLACS['ui'].blacs.tablist[DDS[0]].get_channel(DDS[1]) + dds_outputs.appendRow(self.make_virtual_device_output_row(DDS[0] + '.' + chan.name)) + add_vd_item = QStandardItem(self.VD_TREE_DUMMY_ROW_TEXT) add_vd_item.setData(True, self.VD_TREE_ROLE_IS_DUMMY_ROW) add_vd_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) @@ -460,6 +467,10 @@ def _encode_virtual_devices(self): DO_name = output_group.child(k).text().split(' - ')[0].split('.') inverted = output_group.child(k).data(self.VD_TREE_ROLE_DO_INVERTED) virtual_device_data[vd.text()]['DO'].append((DO_name[0], DO_name[1], inverted)) + elif output_group.text() == 'DDS Outputs': + for k in range(output_group.rowCount()): + DDS_name = output_group.child(k).text().split(' - ')[0].split('.') + virtual_device_data[vd.text()]['DDS'].append((DDS_name[0], DDS_name[1])) return virtual_device_data diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index 3032f173..87dcc3d3 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -3,6 +3,7 @@ from qtutils.qt.QtWidgets import * from labscript_utils.qtwidgets.toolpalette import ToolPaletteGroup +from labscript_utils.qtwidgets.ddsoutput import DDSOutput from blacs.tab_base_classes import PluginTab @@ -22,22 +23,32 @@ def create_widgets(self, blacs_tablist, AOs, DOs, DDSs): if self._AOs[AO] is None: chan = self._blacs_tablist[AO[0]].get_channel(AO[1]) orig_label = chan.name.split('-') - self._AOs[AO] = chan.create_widget('%s\n%s'%(AO[0]+'.'+orig_label[0], orig_label[1]), False, None) + virtual_label = '%s\n%s' % (AO[0]+'.'+orig_label[0], orig_label[1]) + self._AOs[AO] = chan.create_widget(virtual_label, False, None) self._AOs[AO].last_AO = None for DO in self._DOs.keys(): if self._DOs[DO] is None: self._DOs[DO] = self._blacs_tablist[DO[0]].get_channel(DO[1]).create_widget(inverted=DO[2]) orig_label = self._DOs[DO].text().split('\n') - self._DOs[DO].setText('%s\n%s'%(DO[0]+'.'+orig_label[0], orig_label[1])) + virtual_label = '%s\n%s' % (DO[0]+'.'+orig_label[0], orig_label[1]) + self._DOs[DO].setText(virtual_label) self._DOs[DO].last_DO = None - dds_widgets = [] # TODO + for DDS in self._DDSs.keys(): + if self._DDSs[DDS] is None: + chan = self._blacs_tablist[DDS[0]].get_channel(DDS[1]) + orig_label = chan.name.split(' - ') + self._DDSs[DDS] = DDSOutput(DDS[0]+'.'+orig_label[0], orig_label[1]) + chan.add_widget(self._DDSs[DDS]) + self._DDSs[DDS].last_DDS = None if len(self._AOs) > 0: self.place_widget_group('Analog Outputs', [v for k, v in self._AOs.items()]) if len(self._DOs) > 0: self.place_widget_group('Digital Outputs', [v for k, v in self._DOs.items()]) + if len(self._DDSs) > 0: + self.place_widget_group('DDS Outputs', [v for k, v in self._DDSs.items()]) return @@ -56,6 +67,11 @@ def connect_widgets(self): new_DO = self._blacs_tablist[DO[0]].get_channel(DO[1]) if self._DOs[DO].get_DO() is None and self._DOs[DO].last_DO != new_DO: self._DOs[DO].set_DO(new_DO) + for DDS in self._DDSs.keys(): + if self._DDSs[DDS] is not None: + new_DDS = self._blacs_tablist[DDS[0]].get_channel(DDS[1]) + if self._DDSs[DDS].last_DDS != new_DDS: + new_DDS.add_widget(self._DDSs[DDS]) def disconnect_widgets(self, closing_device_name): ''' @@ -70,6 +86,11 @@ def disconnect_widgets(self, closing_device_name): if DO[0] == closing_device_name: self._DOs[DO].last_DO = self._DOs[DO].get_DO() self._DOs[DO].set_DO(None) + for DDDS in self._DDSs.keys(): + if DDS[0] == closing_device_name: + old_DDS = self._blacs_tablist[DDS[0]].get_channel(DDS[1]) + self._DDSs[DDS].last_DDS = old_DDS + old_DDS.remove_widget(self._DDSs[DDS]) def place_widget_group(self, name, widgets): widget = QWidget() From 3127f517f11fef9102e47f44de68f2269c43953d Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 13 Apr 2025 12:41:10 -0400 Subject: [PATCH 13/18] Fix virtual device removal --- blacs/plugins/virtual_device/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index d972f8f8..f1a97672 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -168,6 +168,7 @@ class Menu(object): VD_TREE_COL_DELETE = 3 VD_TREE_ROLE_IS_DUMMY_ROW = Qt.UserRole + 1 VD_TREE_ROLE_DO_INVERTED = Qt.UserRole + 2 + VD_TREE_ROLE_IS_VIRTUAL_DEVICE = Qt.UserRole + 3 def _get_root_parent(item): while item.parent() is not None: @@ -339,6 +340,7 @@ def on_virtual_devices_item_changed(self, item): new_device_item = QStandardItem(new_vd_name) remove_item = QStandardItem() + remove_item.setData(True, self.VD_TREE_ROLE_IS_VIRTUAL_DEVICE) remove_item.setIcon(QIcon(':qtutils/fugue/minus')) remove_item.setEditable(False) remove_item.setToolTip('Remove this virtual device') @@ -364,6 +366,8 @@ def on_treeView_virtual_devices_clicked(self, index): elif item.column() == self.VD_TREE_COL_DN: if index.row() < item.parent().rowCount()-1: item.parent().insertRow(index.row()+1, item.parent().takeRow(index.row())) + elif item.data(self.VD_TREE_ROLE_IS_VIRTUAL_DEVICE) and item.column() == self.VD_TREE_COL_DELETE: + self.virtual_device_model.removeRows(index.row(), 1) elif item.column() == self.VD_TREE_COL_DELETE: item.parent().removeRow(index.row()) @@ -372,7 +376,12 @@ def on_edit_virtual_devices(self, *args, **kwargs): # This happens here so that the tree is up to date for vd_name, vd in self.BLACS['plugins'][module].get_save_virtual_devices().items(): device_item = QStandardItem(vd_name) - self.virtual_device_model.appendRow([device_item]) + remove_item = QStandardItem() + remove_item.setData(True, self.VD_TREE_ROLE_IS_VIRTUAL_DEVICE) + remove_item.setIcon(QIcon(':qtutils/fugue/minus')) + remove_item.setEditable(False) + remove_item.setToolTip('Remove this virtual device') + self.virtual_device_model.appendRow([device_item, None, None, remove_item]) analog_outputs = QStandardItem('Analog Outputs') device_item.appendRow(analog_outputs) From 539c8a1994121140b23eec41cc489e1be288b8c9 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 13 Apr 2025 13:27:09 -0400 Subject: [PATCH 14/18] Fix bugs in DDS device restart handling --- blacs/plugins/virtual_device/virtual_device_tab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blacs/plugins/virtual_device/virtual_device_tab.py b/blacs/plugins/virtual_device/virtual_device_tab.py index 87dcc3d3..801330ac 100644 --- a/blacs/plugins/virtual_device/virtual_device_tab.py +++ b/blacs/plugins/virtual_device/virtual_device_tab.py @@ -3,8 +3,9 @@ from qtutils.qt.QtWidgets import * from labscript_utils.qtwidgets.toolpalette import ToolPaletteGroup -from labscript_utils.qtwidgets.ddsoutput import DDSOutput +from labscript_utils.qtwidgets.ddsoutput import AnalogOutput, DigitalOutput, DDSOutput +from blacs.tab_base_classes import PluginTab from blacs.tab_base_classes import PluginTab class VirtualDeviceTab(PluginTab): @@ -86,11 +87,10 @@ def disconnect_widgets(self, closing_device_name): if DO[0] == closing_device_name: self._DOs[DO].last_DO = self._DOs[DO].get_DO() self._DOs[DO].set_DO(None) - for DDDS in self._DDSs.keys(): + for DDS in self._DDSs.keys(): if DDS[0] == closing_device_name: old_DDS = self._blacs_tablist[DDS[0]].get_channel(DDS[1]) self._DDSs[DDS].last_DDS = old_DDS - old_DDS.remove_widget(self._DDSs[DDS]) def place_widget_group(self, name, widgets): widget = QWidget() From 74bc687c677b236214698da2182c3c18b53c295b Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 13 Apr 2025 13:27:21 -0400 Subject: [PATCH 15/18] Setup Virtual Device editing dialog better --- blacs/plugins/virtual_device/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index f1a97672..598446a2 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -407,6 +407,7 @@ def on_edit_virtual_devices(self, *args, **kwargs): self.virtual_device_model.appendRow([add_vd_item]) edit_dialog = QDialog(self.BLACS['ui']) + edit_dialog.setMinimumSize(1024, 768) edit_dialog.setModal(True) edit_dialog.accepted.connect(self.on_save) edit_dialog.rejected.connect(self.on_cancel) @@ -485,10 +486,13 @@ def _encode_virtual_devices(self): def on_save(self): self.BLACS['plugins'][module].set_save_virtual_devices(self._encode_virtual_devices()) + # Cleanup model in case editing window is reopened. + self.virtual_device_model.removeRows(0, self.virtual_device_model.rowCount()) QMessageBox.information(self.BLACS['ui'], 'Virtual Devices Saved', 'New virtual devices saved. Please restart BLACS to load new devices.') def on_cancel(self): + self.virtual_device_model.removeRows(0, self.virtual_device_model.rowCount()) QMessageBox.information(self.BLACS['ui'], 'Virtual Devices Not Saved', 'Editing of virtual devices canceled.') From da050463d6514a40e16b65688fc45a0124d9077e Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Sun, 4 May 2025 16:52:25 -0400 Subject: [PATCH 16/18] Fix virtual device editing bugs: 1. Make sure DO inverted data is reloaded from save 2. Check if BLACS tabs are still available when loading editor --- blacs/plugins/virtual_device/__init__.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index 598446a2..f283082e 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -275,8 +275,7 @@ def get_menu_items(self): ] } - def make_virtual_device_output_row(self, name): - name_item = QStandardItem(name) + def make_virtual_device_output_row(self, name_item): up_item = QStandardItem() up_item.setIcon(QIcon(':qtutils/fugue/arrow-090')) up_item.setEditable(False) @@ -386,20 +385,33 @@ def on_edit_virtual_devices(self, *args, **kwargs): analog_outputs = QStandardItem('Analog Outputs') device_item.appendRow(analog_outputs) for AO in vd['AO']: + if AO[0] not in self.BLACS['ui'].blacs.tablist: + # BLACS tab removed, remove virtual device + continue chan = self.BLACS['ui'].blacs.tablist[AO[0]].get_channel(AO[1]) - analog_outputs.appendRow(self.make_virtual_device_output_row(AO[0] + '.' + chan.name)) + ao_item = QStandardItem(AO[0] + '.' + chan.name) + analog_outputs.appendRow(self.make_virtual_device_output_row(ao_item)) digital_outputs = QStandardItem('Digital Outputs') device_item.appendRow(digital_outputs) for DO in vd['DO']: + if DO[0] not in self.BLACS['ui'].blacs.tablist: + # BLACS tab removed, remove virtual device + continue chan = self.BLACS['ui'].blacs.tablist[DO[0]].get_channel(DO[1]) - digital_outputs.appendRow(self.make_virtual_device_output_row(DO[0] + '.' + chan.name)) + do_item = QStandardItem(DO[0] + '.' + chan.name) + do_item.setData(DO[2], self.VD_TREE_ROLE_DO_INVERTED) + digital_outputs.appendRow(self.make_virtual_device_output_row(do_item)) dds_outputs = QStandardItem('DDS Outputs') device_item.appendRow(dds_outputs) for DDS in vd['DDS']: + if DDS[0] not in self.BLACS['ui'].blacs.tablist: + # BLACS tab removed, remove virtual device + continue chan = self.BLACS['ui'].blacs.tablist[DDS[0]].get_channel(DDS[1]) - dds_outputs.appendRow(self.make_virtual_device_output_row(DDS[0] + '.' + chan.name)) + dds_item = QStandardItem(DDS[0] + '.' + chan.name) + dds_outputs.appendRow(self.make_virtual_device_output_row(dds_item)) add_vd_item = QStandardItem(self.VD_TREE_DUMMY_ROW_TEXT) add_vd_item.setData(True, self.VD_TREE_ROLE_IS_DUMMY_ROW) From 789c3973f34ec4f984e0f4162b6a40a52da401c4 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 29 Sep 2025 17:26:18 -0400 Subject: [PATCH 17/18] More function documentation of virtual device plugin --- blacs/plugins/virtual_device/__init__.py | 48 +++++++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index f283082e..ac68f737 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -92,6 +92,10 @@ def disconnect_widgets(self, closing_device_name): vd_tab.disconnect_widgets(closing_device_name) def reconnect(self, stop_event): + ''' + Runs constantly in a second thread to reconnect widgets in virtual devices + to hardware devices after the hardware device tabs restart. + ''' while not stop_event.wait(CONNECT_CHECK_INTERVAL): self.connect_widgets() @@ -155,6 +159,15 @@ def close(self): pass class Menu(object): + ''' + The virtual device adding/editing menu. + + Reads the connection table to determine what hardware is available, + then allows adding or removing hardware from virtual devices. + + Stores new virtual devices in the Plugin object, + which does not reload them until a BLACS restart. + ''' VD_TREE_DUMMY_ROW_TEXT = '' CT_TREE_COL_NAME = 0 @@ -211,6 +224,12 @@ def _get_child_outputs(self, conn_table, root_devs, dev_name, tab): return AOs, DOs, DDSs def __init__(self, BLACS): + ''' + Some small preparation for the menu. + + Parses the connection table immediately, + defers parsing of current virtual devices so that they can be edited multiple times. + ''' self.BLACS = BLACS self.connection_table_model = QStandardItemModel() @@ -292,6 +311,12 @@ def make_virtual_device_output_row(self, name_item): return [name_item, up_item, dn_item, remove_item] def on_treeView_connection_table_clicked(self, index): + ''' + Processes user clicking on an item in the connection table tree. + + The only column we respond to is the "add" column. + This adds a hardware output to the currently selected virtual device + ''' item = self.connection_table_model.itemFromIndex(index) if item.column() == self.CT_TREE_COL_ADD: # Add this output to the currently selected virtual devices @@ -352,6 +377,15 @@ def on_virtual_devices_item_changed(self, item): item.setText(self.VD_TREE_DUMMY_ROW_TEXT) def on_treeView_virtual_devices_clicked(self, index): + ''' + Processes user clicking on an item in the virtual device tree. + + Options are: + -Dummy row clicked: add new device + -Up or down arrow clicked: reorder hardware output + -Remove virtual device clicked: remove virtual device + -Remove hardware output clicked: remove hardware output from virtual device + ''' item = self.virtual_device_model.itemFromIndex(index) if item.data(self.VD_TREE_ROLE_IS_DUMMY_ROW): name_index = index.sibling(index.row(), self.VD_TREE_COL_NAME) @@ -371,8 +405,11 @@ def on_treeView_virtual_devices_clicked(self, index): item.parent().removeRow(index.row()) def on_edit_virtual_devices(self, *args, **kwargs): - # Construct tree of virtual devices - # This happens here so that the tree is up to date + ''' + Open the editing menu. + + At this point, virtual devices are parsed and GUI objects are instantiated. + ''' for vd_name, vd in self.BLACS['plugins'][module].get_save_virtual_devices().items(): device_item = QStandardItem(vd_name) remove_item = QStandardItem() @@ -497,6 +534,10 @@ def _encode_virtual_devices(self): return virtual_device_data def on_save(self): + ''' + Pass new virtual devices back to the plugin. + Instructs the user to restart BLACS to reload virtual devices. + ''' self.BLACS['plugins'][module].set_save_virtual_devices(self._encode_virtual_devices()) # Cleanup model in case editing window is reopened. self.virtual_device_model.removeRows(0, self.virtual_device_model.rowCount()) @@ -504,6 +545,9 @@ def on_save(self): 'New virtual devices saved. Please restart BLACS to load new devices.') def on_cancel(self): + ''' + Inform the user that nothing has happened. + ''' self.virtual_device_model.removeRows(0, self.virtual_device_model.rowCount()) QMessageBox.information(self.BLACS['ui'], 'Virtual Devices Not Saved', 'Editing of virtual devices canceled.') From a0a5de2d209b959bed3c66c082a821c07b966677 Mon Sep 17 00:00:00 2001 From: Carter Turn Date: Mon, 29 Sep 2025 17:33:11 -0400 Subject: [PATCH 18/18] Add lengthier overview of virtual devices --- blacs/plugins/virtual_device/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/blacs/plugins/virtual_device/__init__.py b/blacs/plugins/virtual_device/__init__.py index ac68f737..3b286f89 100644 --- a/blacs/plugins/virtual_device/__init__.py +++ b/blacs/plugins/virtual_device/__init__.py @@ -42,6 +42,19 @@ CONNECT_CHECK_INTERVAL = 0.1 class Plugin(object): + ''' + The `virtual_device` plugin provides a way to group analog, digital, and DDS outputs + that are related in purpose but attached to different hardware devices into a single BLACS tab. + + A typical use case might be creating a "MOT" panel from analog and digital channels on a dedicated analog card and digital card. + + To enable the `virtual_device` plugin, add the line `virtual_device = True` + to your `.ini` config file in the `labconfig` folder. + After restarting BLACS, click the `Edit` option in the new `Virtual Devices` menu + to begin constructing virtual devices. + Modifying virtual devices requires restarting BLACS. + ''' + def __init__(self, initial_settings): self.menu = None self.notifications = {}