From 3f23502b80d29b3b91fb49be9654cfda1eeebe2b Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 22 Apr 2025 13:46:45 -0400 Subject: [PATCH 1/4] Initial pass at adding `EnumOutput` BLACSTab support via the `EO` class Does not attempt to add `EO` handling to the `auto_create_widgets` and `auto_place_widgets` functions to better maintain backwards compatibility. --- blacs/device_base_class.py | 50 +++++++- blacs/output_classes.py | 239 +++++++++++++++++++++++++++++++++++-- 2 files changed, 273 insertions(+), 16 deletions(-) diff --git a/blacs/device_base_class.py b/blacs/device_base_class.py index fcf5ab06..c99325d1 100644 --- a/blacs/device_base_class.py +++ b/blacs/device_base_class.py @@ -26,7 +26,7 @@ from blacs import BLACS_DIR from blacs.tab_base_classes import Tab, Worker, define_state from blacs.tab_base_classes import MODE_MANUAL, MODE_TRANSITION_TO_BUFFERED, MODE_TRANSITION_TO_MANUAL, MODE_BUFFERED -from blacs.output_classes import AO, DO, DDS, Image +from blacs.output_classes import AO, DO, DDS, Image, EO from labscript_utils.qtwidgets.toolpalette import ToolPaletteGroup from labscript_utils.shared_drive import path_to_agnostic @@ -41,6 +41,7 @@ def __init__(self,notebook,settings,restart=False): self._DO = {} self._DDS = {} self._image = {} + self._EO = {} self._final_values = {} self._last_programmed_values = {} @@ -151,6 +152,13 @@ def supports_remote_value_check(self,support): # }, # } # + # + # eo_properties = {'hardware_channel_reference': {'options':['option 1', 'option 2']}, + # 'eo1': {'options': {'option 1': {'index': 0, 'tooltip': 'description 1'}, + # 'option 2': {'index': 3, 'tooltip': 'description 2'}, + # 'option 3': 1}, + # 'return_index': True}} + def create_digital_outputs(self,digital_properties): for hardware_name,properties in digital_properties.items(): # Save the DO object @@ -222,7 +230,26 @@ def create_dds_outputs(self,dds_properties): sub_chnls['gate'] = self._create_DO_object(connection_name,hardware_name+'_gate','gate',properties) self._DDS[hardware_name] = DDS(hardware_name,connection_name,sub_chnls) + + def create_eo_outputs(self, eo_properties): + for output_name, properties in eo_properties.items(): + self._EO[output_name] = self._create_eo_object(self.device_name, output_name, properties) + + def _create_eo_object(self, parent_device, device_property, properties): + properties.setdefault('return_index', False) + return EO(device_property, parent_device, self.device_name, self.program_device, self.settings, + properties['options'], properties['return_index']) + def create_eo_widgets(self,device_properties): + widgets = {} + for output_name, properties in device_properties.items(): + properties.setdefault('display_name',output_name) + properties.setdefault('horizontal_alignment',False) + properties.setdefault('parent',None) + widgets[output_name] = self._EO[output_name].create_widget(properties['display_name'], properties['horizontal_alignment'], properties['parent']) + + return widgets + def get_child_from_connection_table(self, parent_device_name, port): return self.connection_table.find_child(parent_device_name, port) @@ -361,7 +388,7 @@ def update_from_settings(self,settings): self.restore_save_data(settings['saved_data']) self.settings = settings - for output in [self._AO, self._DO, self._image]: + for output in [self._AO, self._DO, self._image, self._EO]: for name,channel in output.items(): if not channel._locked: channel._update_from_settings(settings) @@ -374,8 +401,8 @@ def update_from_settings(self,settings): subchnl._update_from_settings(settings) def get_front_panel_values(self): - return {channel:item.value for output in [self._AO,self._DO,self._image,self._DDS] for channel,item in output.items()} - + return {channel:item.value for output in [self._AO,self._DO,self._image,self._DDS,self._EO] for channel,item in output.items()} + def get_channel(self,channel): if channel in self._AO: return self._AO[channel] @@ -385,6 +412,8 @@ def get_channel(self,channel): return self._image[channel] elif channel in self._DDS: return self._DDS[channel] + elif channel in self._EO: + return self._EO[channel] else: return None @@ -519,6 +548,16 @@ def check_remote_values(self): ui.channel_label.setText(self._AO[channel].name) ui.front_value.setText(front_value) ui.remote_value.setText(remote_value) + elif channel in self._EO: + # very easy case, values are strings + front_value = str(self._last_programmed_values[channel]) + remote_value = str(remote_value) + if front_value != remote_value: + changed = True + ui = UiLoader().load(os.path.join(BLACS_DIR, 'tab_value_changed.ui')) + ui.channel_label.setText(self._EO[channel].name) + ui.front_value.setText(front_value) + ui.remote_value.setText(remote_value) else: raise RuntimeError('device_base_class.py is not programmed to handle channel types other than DDS, AO and DO in check_remote_values') @@ -666,7 +705,8 @@ def transition_to_manual(self,notify_queue,program=False): self._image[channel].set_value(value,program=False) elif channel in self._DDS: self._DDS[channel].set_value(value,program=False) - + elif channel in self._EO: + self._EO[channel].set_value(value,program=False) if success: diff --git a/blacs/output_classes.py b/blacs/output_classes.py index 1bf5febd..a45cba29 100644 --- a/blacs/output_classes.py +++ b/blacs/output_classes.py @@ -23,11 +23,13 @@ from labscript_utils.qtwidgets.digitaloutput import DigitalOutput, InvertedDigitalOutput from labscript_utils.qtwidgets.ddsoutput import DDSOutput from labscript_utils.qtwidgets.imageoutput import ImageOutput +from labscript_utils.qtwidgets.enumoutput import EnumOutput from labscript_utils.unitconversions import get_unit_conversion_class class AO(object): - def __init__(self, hardware_name, connection_name, device_name, program_function, settings, calib_class, calib_params, default_units, min, max, step, decimals): + def __init__(self, hardware_name, connection_name, device_name, program_function, settings, + calib_class, calib_params, default_units, min, max, step, decimals): self._connection_name = connection_name self._hardware_name = hardware_name self._device_name = device_name @@ -238,8 +240,8 @@ def convert_range_from_base(self,value,range,unit): return abs(bound1-bound2) - def create_widget(self,display_name=None, horizontal_alignment=False, parent=None): - widget = AnalogOutput(self._hardware_name,self._connection_name,display_name, horizontal_alignment, parent) + def create_widget(self, display_name=None, horizontal_alignment=False, parent=None): + widget = AnalogOutput(self._hardware_name, self._connection_name, display_name, horizontal_alignment, parent) self.add_widget(widget) return widget @@ -482,10 +484,12 @@ def _update_from_settings(self,settings): def create_widget(self, *args, **kwargs): inverted = kwargs.pop("inverted", False) + display_name = kwargs.pop("display_name",None) + label_text = (self._hardware_name + '\n' + self._connection_name) if display_name is None else display_name if not inverted: - widget = DigitalOutput('%s\n%s'%(self._hardware_name,self._connection_name),*args,**kwargs) + widget = DigitalOutput(label_text,*args,**kwargs) else: - widget = InvertedDigitalOutput('%s\n%s'%(self._hardware_name,self._connection_name),*args,**kwargs) + widget = InvertedDigitalOutput(label_text,*args,**kwargs) self.add_widget(widget, inverted=inverted) return widget @@ -750,6 +754,189 @@ def set_value(self,value,program=True): @property def name(self): return self._hardware_name + ' - ' + self._connection_name + +class EO(object): + """Enumeration Output object for control of outputs that only allow discrete values.""" + + def __init__(self, hardware_name, connection_name, device_name, program_function, + settings, options, return_index=False): + self._hardware_name = hardware_name + self._connection_name = connection_name + self._widget_list = [] + self._return_index = return_index + + self._device_name = device_name + self._logger = logging.getLogger('BLACS.%s.%s'%(self._device_name, hardware_name)) + + # populate combobox model from options + self._comboboxmodel = QStandardItemModel() + if isinstance(options, list) or isinstance(options, tuple): + # use input order to populate model for list or tuple + for i,key in enumerate(options): + item = QStandardItem(str(key)) + item.setData(i, Qt.UserRole) + self._comboboxmodel.appendRow(item) + elif isinstance(options, dict): + # python >3.7 preserves input dictionary ordering + # for general compatibility, index must be an integer + for key,val in options.items(): + item = QStandardItem(str(key)) + if isinstance(val, dict): + # allow for tooltip definition of options + item.setData(int(val['index']), Qt.UserRole) + item.setData(val['tooltip'], Qt.ToolTipRole) + else: + item.setData(int(val), Qt.UserRole) + self._comboboxmodel.appendRow(item) + + else: + msg = """'options' is not a list, tuple, or dictionary""" + self._logger.error(msg) + + # Note that while we could store self._current_value and self._locked in the + # settings dictionary, this dictionary is available to other parts of BLACS + # and using separate variables avoids those parts from being able to directly + # influence behaviour (the worst they can do is change the value used on initialisation) + self._locked = False + # populate self._current_value and self._current_index from default value + default_item = self._comboboxmodel.item(0,0) + if default_item: + self._current_value = default_item.text() + self._current_index = default_item.data(Qt.UserRole) + else: + msg = "default_value not in options" + self._logger.error(msg) + + self._options = options + self._program_device = program_function + self._update_from_settings(settings) + + def _update_from_settings(self,settings, program=True): + # Build up settings dictionary if it doesn't exist + if not isinstance(settings,dict): + settings = {} + if 'front_panel_settings' not in settings or not isinstance(settings['front_panel_settings'],dict): + settings['front_panel_settings'] = {} + if self._hardware_name not in settings['front_panel_settings'] or not isinstance(settings['front_panel_settings'][self._hardware_name],dict): + settings['front_panel_settings'][self._hardware_name] = {} + # Set default values if they are not already saved in the settings dictionary + if 'base_value' not in settings['front_panel_settings'][self._hardware_name]: + settings['front_panel_settings'][self._hardware_name]['base_value'] = self._current_index + if 'locked' not in settings['front_panel_settings'][self._hardware_name]: + settings['front_panel_settings'][self._hardware_name]['locked'] = False + if 'name' not in settings['front_panel_settings'][self._hardware_name]: + settings['front_panel_settings'][self._hardware_name]['name'] = self._connection_name + + # only keep a reference to the part of the settings dictionary relevant to this EO + self._settings = settings['front_panel_settings'][self._hardware_name] + + # Update the state + self.set_value(int(self._settings['base_value']), program=program) + + # Update the lock state + self._update_lock(self._settings['locked']) + + def create_widget(self, display_name=None, horizontal_alignment=False, parent=None): + widget = EnumOutput(self._hardware_name, self._connection_name, + display_name, horizontal_alignment, parent) + self.add_widget(widget) + return widget + + def add_widget(self, widget): + if widget in self._widget_list: + return False + + self._widget_list.append(widget) + + # make sure the widget knows about this EO + widget.set_EO(self, True, False) + # use options from the EO to set the combobox + widget.set_combobox_model(self._comboboxmodel) + + # Connect widget signal to EO slot + widget.connect_value_change(self.set_value) + self.set_value(self._current_value, False) + # Update lock state of widgets + self._update_lock(self._locked) + + return True + + def remove_widget(self, widget): + if widget not in self._widget_list: + msg = '''Widget cannot be removed because it is not registered + with the EO object %s''' + raise RuntimeError(msg % (self._hardware_name)) + + widget.disconnect_value_change() + widget.set_combobox_model(QStandardItemModel()) + self._widget_list.remove(widget) + + @property + def value(self): + if self._return_index: + return self._current_index + else: + return self._current_value + + def lock(self): + self._update_lock(True) + + def unlock(self): + self._update_lock(False) + + def _update_lock(self,locked): + self._locked = locked + for widget in self._widget_list: + if locked: + widget.lock(False) + else: + widget.unlock(False) + + self._settings['locked'] = locked + + def set_value(self, value, program=True): + '''Update EO & EO widgets to value/index''' + if type(value) is int: + self.update_by_index(value) + elif isinstance(value,str): + self.update_by_value(value) + + if program: + self._logger.debug('program device called') + self._program_device() + + for widget in self._widget_list: + if self._current_value != widget.selected_option: + widget.block_combobox_signals() + widget.selected_option = self._current_value + widget.unblock_combobox_signals() + + def update_by_value(self,value): + self._current_value = value + items = self._comboboxmodel.findItems(value) + if items: + self._current_index = items[0].data(Qt.UserRole) + self._settings['base_value'] = self._current_index + else: + msg = f"""Value {value} not found in model""" + raise RuntimeError(msg) + + def update_by_index(self,index): + self._current_index = index + self._settings['base_value'] = index + # find option corresponding to index + indices = self._comboboxmodel.match(self._comboboxmodel.index(0,0), Qt.UserRole, index, 1, + Qt.MatchExactly|Qt.MatchWrap) + if indices: + self._current_value = self._comboboxmodel.itemFromIndex(indices[0]).text() + else: + msg = f"""Index {index} not found in model""" + raise RuntimeError(msg) + + @property + def name(self): + return self._hardware_name + ' - ' + self._connection_name + if __name__ == '__main__': from labscript_utils.qtwidgets.toolpalette import ToolPaletteGroup @@ -762,8 +949,9 @@ def name(self): widget = QWidget() layout.addWidget(widget) tpg = ToolPaletteGroup(widget) - toolpalette = tpg.append_new_palette('Digital Outputs') - toolpalette2 = tpg.append_new_palette('Analog Outputs') + toolpalette = tpg.append_new_palette('Digital Outputs') + toolpalette2 = tpg.append_new_palette('Analog Outputs') + toolpalette3 = tpg.append_new_palette('Enum Outputs') layout.addItem(QSpacerItem(0,0,QSizePolicy.Minimum,QSizePolicy.MinimumExpanding)) # create settings dictionary @@ -777,7 +965,11 @@ def name(self): 'locked':False, 'base_step_size':0.1, 'current_units':'V', - } + }, + 'eo0':{ + 'base_value':0, #must be a number to save in settings, this is index of comboBox + 'locked':False + } } } @@ -785,7 +977,8 @@ def print_something(): print('program_function called') # Create a DO object - my_DO = DO(hardware_name='do0', connection_name='my first digital output', program_function=print_something, settings=settings) + my_DO = DO(hardware_name='do0', connection_name='my first digital output', device_name='device', + program_function=print_something, settings=settings) # Link in two DO widgets button1 = DigitalOutput('do0\nmy first digital output') @@ -803,15 +996,39 @@ def print_something(): # link in two AO widgets analog1 = AnalogOutput('AO1') - analog2 = AnalogOutput('AO1 copy') + analog2 = AnalogOutput('AO1 copy',display_name='Linked AO1 Widget') my_AO.add_widget(analog1) my_AO.add_widget(analog2) toolpalette2.addWidget(analog1) toolpalette2.addWidget(analog2) # TODO: Add in test case for DDS + + # Create EO object + test_options = ['option 1','option 2'] + my_EO = EO(hardware_name='eo0', connection_name='my eo', device_name='device', + program_function=print_something, settings=settings, options=test_options) + + enum1 = EnumOutput('Enumerate',display_name='Test Enumerable', + horizontal_alignment=True) + # linked enum with different layout + enum2 = EnumOutput('Linked Enumerate') + my_EO.add_widget(enum1) + my_EO.add_widget(enum2) + toolpalette3.addWidget(enum1) + toolpalette3.addWidget(enum2) + + # Create second EO object with fancier options + test_options2 = {'option 1':{'index':0,'tooltip':'Option 1 Description'}, + 'option 2':{'index':3,'tooltip':'Option 2 Description'}, + 'option 3':1} #tooltips are optional, only pass index if not using them + my_EO2 = EO(hardware_name='eo1', connection_name='my fancier eo', device_name='device', + program_function=print_something, settings=settings, options=test_options2, return_index=True) + enum3 = EnumOutput('Enumerate3',display_name='Detailed options enumerable', + horizontal_alignment=True) + my_EO2.add_widget(enum3) + toolpalette3.addWidget(enum3) window.show() sys.exit(qapplication.exec_()) - From 7144f6d54a1023bcc8b28931a43e2ac90ed8d12f Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 22 Apr 2025 13:56:38 -0400 Subject: [PATCH 2/4] Rename functions to be consistent with other tab widget functions. Also adds a `auto_create_enum_widgets` that mirrors `auto_create_widgets` functionality, but just for enums (ie preserves backwards compat, enforces opt-in). --- blacs/device_base_class.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/blacs/device_base_class.py b/blacs/device_base_class.py index c99325d1..1d9dc4e2 100644 --- a/blacs/device_base_class.py +++ b/blacs/device_base_class.py @@ -231,24 +231,14 @@ def create_dds_outputs(self,dds_properties): self._DDS[hardware_name] = DDS(hardware_name,connection_name,sub_chnls) - def create_eo_outputs(self, eo_properties): + def create_enum_outputs(self, eo_properties): for output_name, properties in eo_properties.items(): - self._EO[output_name] = self._create_eo_object(self.device_name, output_name, properties) + self._EO[output_name] = self._create_EO_object(self.device_name, output_name, properties) - def _create_eo_object(self, parent_device, device_property, properties): + def _create_EO_object(self, parent_device, device_property, properties): properties.setdefault('return_index', False) return EO(device_property, parent_device, self.device_name, self.program_device, self.settings, properties['options'], properties['return_index']) - - def create_eo_widgets(self,device_properties): - widgets = {} - for output_name, properties in device_properties.items(): - properties.setdefault('display_name',output_name) - properties.setdefault('horizontal_alignment',False) - properties.setdefault('parent',None) - widgets[output_name] = self._EO[output_name].create_widget(properties['display_name'], properties['horizontal_alignment'], properties['parent']) - - return widgets def get_child_from_connection_table(self, parent_device_name, port): return self.connection_table.find_child(parent_device_name, port) @@ -298,6 +288,23 @@ def create_dds_widgets(self,channel_properties): return widgets + def create_enum_widgets(self,device_properties): + + widgets = {} + for output_name, properties in device_properties.items(): + properties.setdefault('display_name',output_name) + properties.setdefault('horizontal_alignment',False) + properties.setdefault('parent',None) + widgets[output_name] = self._EO[output_name].create_widget(properties['display_name'], properties['horizontal_alignment'], properties['parent']) + + return widgets + + def auto_create_enum_widgets(self): + eo_properties = {} + for channel,_ in self._EO.items(): + eo_properties[channel] = {} + return self.create_enum_widgets(eo_properties) + def auto_create_widgets(self): dds_properties = {} for channel,output in self._DDS.items(): From 10fae2ba3d3d339d697d65b24ad6c4f6d0cc647a Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 22 Apr 2025 14:48:36 -0400 Subject: [PATCH 3/4] Update `device_base_class.py` test to run. Creates a top-level `tests` directory which houses the dummy connectiontable object necessary to run the test. --- blacs/device_base_class.py | 11 ++++------- tests/device_base_classes_connection_table.h5 | Bin 0 -> 35144 bytes 2 files changed, 4 insertions(+), 7 deletions(-) create mode 100644 tests/device_base_classes_connection_table.h5 diff --git a/blacs/device_base_class.py b/blacs/device_base_class.py index 1d9dc4e2..b303b180 100644 --- a/blacs/device_base_class.py +++ b/blacs/device_base_class.py @@ -813,7 +813,7 @@ def transition_to_manual(self): if __name__ == '__main__': # Test case! - from connections import ConnectionTable + from labscript_utils.connections import ConnectionTable from labscript_utils.qtwidgets.dragdroptab import DragDropTabWidget class MyTab(DeviceTab): @@ -869,7 +869,7 @@ def sort(channel): button2.clicked.connect(lambda: self.transition_to_manual(Queue())) self.get_tab_layout().addWidget(button2) - connection_table = ConnectionTable(r'example_connection_table.h5') + connection_table = ConnectionTable('../tests/device_base_classes_connection_table.h5') class MyWindow(QWidget): @@ -880,14 +880,11 @@ def __init__(self,*args,**kwargs): def closeEvent(self,event): if not self.are_we_closed: event.ignore() - self.my_tab.shutdown() + self.my_tab.close_tab() self.are_we_closed = True QTimer.singleShot(1000,self.close) else: - if not self.my_tab.shutdown_complete: - QTimer.singleShot(1000,self.close) - else: - event.accept() + event.accept() def add_my_tab(self,tab): self.my_tab = tab diff --git a/tests/device_base_classes_connection_table.h5 b/tests/device_base_classes_connection_table.h5 new file mode 100644 index 0000000000000000000000000000000000000000..24487a554dd4e383a39fc623a4d88ebaf3c6a65f GIT binary patch literal 35144 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 literal 0 HcmV?d00001 From d6b042a76a61f1a3a19e6ec2a85a4aef2a61ba16 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 22 Apr 2025 15:05:05 -0400 Subject: [PATCH 4/4] Extend `device_base_class.py` test script to include DummyIntermediateDevice, with Enum outputs. --- blacs/device_base_class.py | 86 +++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 6 deletions(-) diff --git a/blacs/device_base_class.py b/blacs/device_base_class.py index b303b180..6cc8ccb8 100644 --- a/blacs/device_base_class.py +++ b/blacs/device_base_class.py @@ -754,7 +754,7 @@ def shutdown(self): def program_manual(self,front_panel_values): for channel,value in front_panel_values.items(): - if type(value) != type(True): + if isinstance(value, float): front_panel_values[channel] += 0.001 self.fpv = front_panel_values return front_panel_values @@ -762,10 +762,12 @@ def program_manual(self,front_panel_values): def check_remote_values(self): front_panel_values = {} for channel,value in self.fpv.items(): - if type(value) != type(True): + if isinstance(value, float): front_panel_values[channel] = value + 1.1 - else: + elif isinstance(value, bool): front_panel_values[channel] = not value + else: + front_panel_values[channel] = value if not front_panel_values: front_panel_values['ao0'] = 0 @@ -775,7 +777,7 @@ def check_remote_values(self): def transition_to_buffered(self,device_name,h5file,front_panel_values,refresh): time.sleep(3) for channel,value in front_panel_values.items(): - if type(value) != type(True): + if isinstance(value, float): front_panel_values[channel] += 0.003 return front_panel_values @@ -816,7 +818,7 @@ def transition_to_manual(self): from labscript_utils.connections import ConnectionTable from labscript_utils.qtwidgets.dragdroptab import DragDropTabWidget - class MyTab(DeviceTab): + class MyDAQTab(DeviceTab): def initialise_GUI(self): # Create Digital Output Objects @@ -868,6 +870,77 @@ def sort(channel): button2 = QPushButton("Transition to Manual") button2.clicked.connect(lambda: self.transition_to_manual(Queue())) self.get_tab_layout().addWidget(button2) + + class MyDummyTab(DeviceTab): + + def initialise_GUI(self): + # Create Digital Output Objects + do_prop = {} + for i in range(1): + do_prop['port0/line%d'] = {} + self.create_digital_outputs(do_prop) + + # Create Analog Output objects + ao_prop = {} + for i in range(1): + ao_prop['ao%d'%i] = {'base_unit':'V', + 'min':-10.0, + 'max':10.0, + 'step':0.01, + 'decimals':3 + } + self.create_analog_outputs(ao_prop) + + eo_prop = { + 'Enum1':{ + 'options':['option 1', 'option 2'], + 'return_index':True, + }, + 'Enum2':{ + 'options':{ + 'option 1':{'index':2, 'tooltip':'description 1'}, + 'option 2':4, + } + } + } + self.create_enum_outputs(eo_prop) + + # Create widgets for output objects + dds_widgets,ao_widgets,do_widgets = self.auto_create_widgets() + eo_widgets = self.auto_create_enum_widgets() + + # This function allows you do sort the order of widgets by hardware name. + # it is pass to the Python 'sorted' function as key=sort when passed in as + # the 3rd item of a tuple p(the tuple being an argument of self.auto_place_widgets() + # + # This function takes the channel name (hardware name) and returns a string (or whatever) + # that when sorted alphabetically, returns the correct order + def sort(channel): + port,line = channel.replace('port','').replace('line','').split('/') + port,line = int(port),int(line) + return '%02d/%02d'%(port,line) + + # and auto place them in the UI + self.auto_place_widgets(("DDS Outputs",dds_widgets), + ("Analog Outputs",ao_widgets), + ("Digital Outputs - Port 0",do_widgets), + ('Enums', eo_widgets)) + + # Set the primary worker + self.create_worker("my_worker_name",DeviceWorker,{}) + self.primary_worker = "my_worker_name" + self.create_worker("my_secondary_worker_name",DeviceWorker,{}) + self.add_secondary_worker("my_secondary_worker_name") + + self.supports_remote_value_check(True) + + # Create buttons to test things! + button1 = QPushButton("Transition to Buffered") + button1.clicked.connect(lambda: self.transition_to_buffered('',Queue())) + self.get_tab_layout().addWidget(button1) + button2 = QPushButton("Transition to Manual") + button2.clicked.connect(lambda: self.transition_to_manual(Queue())) + self.get_tab_layout().addWidget(button2) connection_table = ConnectionTable('../tests/device_base_classes_connection_table.h5') @@ -895,7 +968,8 @@ def add_my_tab(self,tab): notebook = DragDropTabWidget() layout.addWidget(notebook) - tab1 = MyTab(notebook,settings = {'device_name': 'ni_pcie_6363_0', 'connection_table':connection_table}) + tab1 = MyDAQTab(notebook,settings = {'device_name': 'ni_pcie_6363_0', 'connection_table':connection_table}) + tab2 = MyDummyTab(notebook,settings = {'device_name': 'intermediate_device', 'connection_table':connection_table}) window.add_my_tab(tab1) window.show() def run():