def _populate_app_fields(self): with WindowServiceProxy(59000) as w: self.video_mode_map = w.get_video_mode_map() if self.video_mode_map: self._video_available = True else: self._video_available = False self.video_mode_keys = sorted(self.video_mode_map.keys()) if self._video_available: self.device_key, self.devices = w.get_video_source_configs() field_list = [ Integer.named('overlay_opacity').using(default=50, optional=True), Directory.named('device_directory').using(default='', optional=True), String.named('transform_matrix').using(default='', optional=True, properties={'show_in_gui': False}), ] if self._video_available: video_mode_enum = Enum.named('video_mode').valued( *self.video_mode_keys).using(default=self.video_mode_keys[0], optional=True) video_enabled_boolean = Boolean.named('video_enabled').using( default=False, optional=True, properties={'show_in_gui': True}) recording_enabled_boolean = Boolean.named('recording_enabled').using( default=False, optional=True, properties={'show_in_gui': False}) field_list.append(video_mode_enum) field_list.append(video_enabled_boolean) field_list.append(recording_enabled_boolean) return Form.of(*field_list)
def StepFields(self): """ Dynamically generate step fields to support dynamic default values. .. versionadded:: 2.25 .. versionchanged:: 2.28.2 Set explicit field titles to prevent case mangling for protocol grid column titles. """ app_values = self.get_app_values() if not app_values: app_values = self.get_default_app_options() self.set_app_values(app_values) fields = Form.of(Float.named('Duration (s)') .using(default=app_values['default_duration'], optional=True, validators=[ValueAtLeast(minimum=0)]), Float.named('Voltage (V)') .using(default=app_values['default_voltage'], optional=True, validators=[ValueAtLeast(minimum=0)]), Float.named('Frequency (Hz)') .using(default=app_values['default_frequency'], optional=True, validators=[ValueAtLeast(minimum=0)])) # Set explicit field title to prevent case mangling for protocol grid # column titles. for field in fields.field_schema: field.properties['title'] = field.name return fields
class ZmqHubPlugin(SingletonPlugin, AppDataController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) plugin_name = 'wheelerlab.zmq_hub_plugin' ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' AppFields = Form.of( String.named('hub_uri').using(optional=True, default='tcp://*:31000'), Enum.named('log_level').using(default='info', optional=True).valued( 'debug', 'info', 'warning', 'error', 'critical'), ) def __init__(self): self.name = self.plugin_name self.hub_process = None def on_plugin_enable(self): """ Handler called once the plugin instance is enabled. Note: if you inherit your plugin from AppDataController and don't implement this handler, by default, it will automatically load all app options from the config file. If you decide to overide the default handler, you should call: AppDataController.on_plugin_enable(self) to retain this functionality. """ super(ZmqHubPlugin, self).on_plugin_enable() app_values = self.get_app_values() self.hub_process = Process( target=run_hub, args=(MicroDropHub(app_values['hub_uri'], self.name), getattr(logging, app_values['log_level'].upper()))) self.hub_process.start() def on_plugin_disable(self): """ Handler called once the plugin instance is disabled. """ if self.hub_process is not None: self.hub_process.terminate() self.hub_process = None
def AppFields(self): ''' .. versionadded:: 2.25 ''' return Form.of( Float.named('default_duration').using(default=1., optional=True), Float.named('default_voltage').using(default=100, optional=True), Float.named('default_frequency').using(default=10e3, optional=True))
def on_set_dstat_params_file(self, widget, data=None): options = self.get_step_options() form = Form.of(Filepath.named('dstat_params_file') .using(default=options.get('dstat_params_file', ''), optional=True, properties={'patterns': [('Dstat parameters file (*.yml)', ('*.yml', ))]})) dialog = FormViewDialog(form, 'Set DStat parameters file') valid, response = dialog.run() if valid: options['dstat_params_file'] = response['dstat_params_file'] self.set_step_values(options)
def AppFields(self): serial_ports = list(get_serial_ports()) if len(serial_ports): default_port = serial_ports[0] else: default_port = None return Form.of( Enum.named('serial_port').using( default=default_port, optional=True).valued(*serial_ports), Float.named('default_duration').using(default=1000, optional=True), Float.named('default_voltage').using(default=80, optional=True), Float.named('default_frequency').using(default=10e3, optional=True), Boolean.named('Auto-run diagnostic tests').using(default=True, optional=True))
def get_step_form_class(self): """ Override to set default values based on their corresponding app options. """ app_values = self.get_app_values() return Form.of( Integer.named('duration').using( default=app_values['default_duration'], optional=True, validators=[ValueAtLeast(minimum=0)]), Float.named('voltage').using( default=app_values['default_voltage'], optional=True, validators=[ValueAtLeast(minimum=0), max_voltage]), Float.named('frequency').using( default=app_values['default_frequency'], optional=True, validators=[ValueAtLeast(minimum=0), check_frequency]))
def on_select_script(self, widget, data=None): """ Handler called when the user clicks on "PSTrace step config..." in the "Tools" menu. """ app = get_app() options = self.get_step_options() form = Form.of(Filepath.named('script').using(default=options.script, optional=True)) dialog = FormViewDialog() valid, response = dialog.run(form) step_options_changed = False if valid and (response['script'] and response['script'] != options.script): options.script = response['script'] step_options_changed = True if step_options_changed: emit_signal('on_step_options_changed', [self.name, app.protocol .current_step_number], interface=IPlugin)
def _set_rows_attr(self, row_ids, column_title, value, prompt=False): title_map = dict([(c.title, c.attr) for c in self.columns]) attr = title_map.get(column_title) if prompt: Fields = Form.of(self._full_field_to_field_def[attr]) local_field = Fields.field_schema_mapping.keys()[0] temp = FormViewDialog(Fields, title='Set %s' % local_field) response_ok, values = temp.run({local_field: value}) if not response_ok: return value = values.values()[0] else: title_map = dict([(c.title, c.attr) for c in self.columns]) attr = title_map.get(column_title) for i in row_ids: setattr(self[i], attr, value) logging.debug('Set rows attr: row_ids=%s column_title=%s value=%s'\ % (row_ids, column_title, value)) self._on_multiple_changed(attr) return True
def _set_rows_attr(self, row_ids, column_title, value, prompt=False): title_map = dict([(c.title, c.attr) for c in self.columns]) attr = title_map.get(column_title) if prompt: Fields = Form.of(self._full_field_to_field_def[attr]) local_field = Fields.field_schema_mapping.keys()[0] temp = FormViewDialog(Fields, title='Set %s' % local_field) response_ok, values = temp.run({local_field: value}) if not response_ok: return value = values.values()[0] else: title_map = dict([(c.title, c.attr) for c in self.columns]) attr = title_map.get(column_title) for i in row_ids: setattr(self[i], attr, value) logging.debug('Set rows attr: row_ids=%s column_title=%s value=%s', row_ids, column_title, value) self._on_multiple_changed(attr) return True
def dict_to_form(dict_): """ Generate a flatland form based on a pandas Series. """ from flatland import Boolean, Form, String, Integer, Float def is_float(v): try: return (float(str(v)), True)[1] except (ValueError, TypeError): return False def is_int(v): try: return (int(str(v)), True)[1] except (ValueError, TypeError): return False def is_bool(v): return v in (True, False) schema_entries = [] for k, v in dict_.iteritems(): if is_int(v): schema_entries.append( Integer.named(k).using(default=v, optional=True)) elif is_float(v): schema_entries.append( Float.named(k).using(default=v, optional=True)) elif is_bool(v): schema_entries.append( Boolean.named(k).using(default=v, optional=True)) elif type(v) == str: schema_entries.append( String.named(k).using(default=v, optional=True)) return Form.of(*schema_entries)
def dict_to_form(dict): ''' Generate a flatland form based on a pandas Series. ''' from flatland import Boolean, Form, String, Integer, Float def is_float(v): try: return (float(str(v)), True)[1] except (ValueError, TypeError): return False def is_int(v): try: return (int(str(v)), True)[1] except (ValueError, TypeError): return False def is_bool(v): return v in (True, False) schema_entries = [] for k, v in dict.iteritems(): if is_int(v): schema_entries.append(Integer.named(k).using(default=v, optional=True)) elif is_float(v): schema_entries.append(Float.named(k).using(default=v, optional=True)) elif is_bool(v): schema_entries.append(Boolean.named(k).using(default=v, optional=True)) elif type(v) == str: schema_entries.append(String.named(k).using(default=v, optional=True)) return Form.of(*schema_entries)
class ProtocolGridController(SingletonPlugin, AppDataController): implements(IPlugin) AppFields = Form.of( String.named('column_positions').using( default='{}', optional=True, properties=dict(show_in_gui=False))) def __init__(self): self.name = "microdrop.gui.protocol_grid_controller" self.builder = None self.widget = None self._enabled_fields = None @property def enabled_fields(self): return self._enabled_fields @enabled_fields.setter def enabled_fields(self, data): self._enabled_fields = deepcopy(data) self.update_grid() def on_plugin_enable(self): app = get_app() self.parent = app.builder.get_object("vbox2") self.window = gtk.ScrolledWindow() self.window.show_all() self.parent.add(self.window) super(ProtocolGridController, self).on_plugin_enable() def on_plugin_enabled(self, env, plugin): self.update_grid() def on_plugin_disabled(self, env, plugin): self.update_grid() def test(self, *args, **kwargs): print 'args=%s, kwargs=%s' % (args, kwargs) print 'attrs=%s' % args[1].attrs def on_step_options_changed(self, plugin, step_number): if self.widget is None: return self.widget._on_step_options_changed(plugin, step_number) def on_protocol_run(self): self.widget.set_sensitive(False) def on_protocol_pause(self): self.widget.set_sensitive(True) def on_protocol_swapped(self, old_protocol, protocol): self.update_grid(protocol) def set_fields_filter(self, combined_fields, enabled_fields_by_plugin): app = get_app() self.enabled_fields = enabled_fields_by_plugin self.widget.select_row(app.protocol.current_step_number) _L().debug('%s', self.enabled_fields) def update_grid(self, protocol=None): app = get_app() if protocol is None: protocol = app.protocol if protocol is None: return _L().debug('plugin_fields=%s', protocol.plugin_fields) forms = dict([ (k, f) for k, f in emit_signal('get_step_form_class').iteritems() if f is not None ]) steps = protocol.steps _L().debug('forms=%s steps=%s', forms, steps) if self.enabled_fields is None: # Assign directly to _enabled_fields to avoid recursive call into # update_grid() self._enabled_fields = dict([ (form_name, set(form.field_schema_mapping.keys())) for form_name, form in forms.items() ]) # The step ID column can be hidden by changing show_ids to False combined_fields = ProtocolGridView(forms, self.enabled_fields, show_ids=True) combined_fields.connect('fields-filter-request', self.set_fields_filter) for i, step in enumerate(steps): values = emit_signal('get_step_values', [i]) attributes = dict() for form_name, form in combined_fields.forms.iteritems(): attr_values = values[form_name] attributes[form_name] = RowFields(**attr_values) combined_row = CombinedRow(combined_fields, attributes=attributes) combined_fields.append(combined_row) if self.widget: # Replacing a previously rendered widget. Maintain original column # order. # Store the position of each column, keyed by column title. column_positions = dict([ (_get_title(c), i) for i, c in enumerate(self.widget.get_columns()) ]) # Remove existing widget to replace with new widget. self.window.remove(self.widget) del self.widget else: # No previously rendered widget. Used saved column positions (if # available). app_values = self.get_app_values() column_positions_json = app_values.get('column_positions', '{}') column_positions = json.loads(column_positions_json) if column_positions: # Explicit column positions are available, so reorder columns # accordingly. # Remove columns so we can reinsert them in an explicit order. columns = combined_fields.get_columns() for c in columns: combined_fields.remove_column(c) # Sort columns according to original order. ordered_column_info = sorted([ (column_positions.get(_get_title(c), len(columns)), _get_title(c), c) for c in columns ]) # Re-add columns in order (sorted according to existing column # order). for i, title_i, column_i in ordered_column_info: combined_fields.append_column(column_i) self.widget = combined_fields app = get_app() if self.widget: self.widget.show_all() self.widget.select_row(app.protocol.current_step_number) self.window.add(self.widget) self.accel_group = self._create_accel_group( app.main_window_controller.view) app.main_window_controller.view.add_accel_group(self.accel_group) else: self.accel_group = None # Disable keyboard shortcuts when a cell edit has started. Without # doing so, certain keys may not behave as expected in edit mode. For # example, see [`step_label_plugin`][1]. # # [1]: https://github.com/wheeler-microfluidics/step_label_plugin/issues/1 self.widget.connect( 'editing-started', lambda *args: app.main_window_controller. disable_keyboard_shortcuts()) # Re-enable keyboard shortcuts when a cell edit has completed. self.widget.connect( 'editing-done', lambda *args: app.main_window_controller.enable_keyboard_shortcuts( )) def _create_accel_group(self, widget): class FocusWrapper(object): ''' This class allows for a function to be executed, restoring the focused state of the protocol grid view if necessary. ''' def __init__(self, controller, func): self.controller = controller self.func = func def __call__(self): focused = self.controller.widget.has_focus() self.func() if focused: self.controller.widget.grab_focus() app = get_app() shortcuts = { '<Control>c': self.widget.copy_rows, '<Control>x': FocusWrapper(self, self.widget.cut_rows), 'Delete': FocusWrapper(self, self.widget.delete_rows), '<Control>v': FocusWrapper(self, self.widget.paste_rows_after), '<Control><Shift>v': FocusWrapper(self, self.widget.paste_rows_before), '<Control><Shift>i': FocusWrapper(self, lambda: app.protocol.insert_step()) } return get_accel_group(widget, shortcuts, enabled_widgets=[self.widget]) def get_schedule_requests(self, function_name): """ Returns a list of scheduling requests (i.e., ScheduleRequest instances) for the function specified by function_name. """ if function_name == 'on_plugin_enable': return [ ScheduleRequest('microdrop.gui.main_window_controller', self.name) ] elif function_name == 'on_protocol_swapped': # Ensure that the app's reference to the new protocol gets set return [ScheduleRequest('microdrop.app', self.name)] return [] def on_step_default_created(self, step_number): self.update_grid() def on_step_created(self, step_number): self.update_grid() def on_step_swapped(self, original_step_number, step_number): _L().debug('%d -> %d', original_step_number, step_number) if self.widget: self.widget.select_row(get_app().protocol.current_step_number) def on_step_removed(self, step_number, step): _L().debug('%d', step_number) self.update_grid() def on_app_exit(self): if self.widget: # Save column positions on exit. column_positions = dict([ (_get_title(c), i) for i, c in enumerate(self.widget.get_columns()) ]) self.set_app_values( {'column_positions': json.dumps(column_positions)})
class App(SingletonPlugin, AppDataController): implements(IPlugin) ''' INFO: <Plugin App 'microdrop.app'> INFO: <Plugin ConfigController 'microdrop.gui.config_controller'> INFO: <Plugin DmfDeviceController 'microdrop.gui.dmf_device_controller'> INFO: <Plugin ExperimentLogController 'microdrop.gui.experiment_log_controller'> INFO: <Plugin MainWindowController 'microdrop.gui.main_window_controller'> INFO: <Plugin ProtocolController 'microdrop.gui.protocol_controller'> INFO: <Plugin ProtocolGridController 'microdrop.gui.protocol_grid_controller'> ''' core_plugins = [ 'microdrop.app', 'microdrop.gui.config_controller', 'microdrop.gui.dmf_device_controller', 'microdrop.gui.experiment_log_controller', 'microdrop.gui.main_window_controller', 'microdrop.gui.protocol_controller', 'microdrop.gui.protocol_grid_controller', 'wheelerlab.zmq_hub_plugin', 'wheelerlab.electrode_controller_plugin', 'wheelerlab.device_info_plugin' ] AppFields = Form.of( Integer.named('x').using(default=None, optional=True, properties={'show_in_gui': False}), Integer.named('y').using(default=None, optional=True, properties={'show_in_gui': False}), Integer.named('width').using(default=400, optional=True, properties={'show_in_gui': False}), Integer.named('height').using(default=500, optional=True, properties={'show_in_gui': False}), Enum.named('update_automatically' #pylint: disable-msg=E1101,E1120 ).using(default=1, optional=True).valued( 'auto-update', 'check for updates, but ask before installing', '''don't check for updates'''), String.named('server_url').using( #pylint: disable-msg=E1120 default='http://microfluidics.utoronto.ca/update', optional=True, properties=dict(show_in_gui=False)), Boolean.named('realtime_mode').using( #pylint: disable-msg=E1120 default=False, optional=True, properties=dict(show_in_gui=False)), Filepath.named('log_file').using( #pylint: disable-msg=E1120 default='', optional=True, properties={'action': gtk.FILE_CHOOSER_ACTION_SAVE}), Boolean.named('log_enabled').using( #pylint: disable-msg=E1120 default=False, optional=True), Enum.named('log_level').using( #pylint: disable-msg=E1101, E1120 default='info', optional=True).valued('debug', 'info', 'warning', 'error', 'critical'), ) def __init__(self): args = parse_args() print 'Arguments: %s' % args self.name = "microdrop.app" # get the version number self.version = "" try: raise Exception version = subprocess.Popen( ['git', 'describe'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE).communicate()[0].rstrip() m = re.match('v(\d+)\.(\d+)-(\d+)', version) self.version = "%s.%s.%s" % (m.group(1), m.group(2), m.group(3)) branch = subprocess.Popen( ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE).communicate()[0].rstrip() if branch.strip() != 'master': self.version += "-%s" % branch except: import pkg_resources version = pkg_resources.get_distribution('microdrop').version dev = ('dev' in version) self.version = re.sub('\.dev.*', '', re.sub('post', '', version)) if dev: self.version += "-dev" self.realtime_mode = False self.running = False self.builder = gtk.Builder() self.signals = {} self.plugin_data = {} # these members are initialized by plugins self.experiment_log_controller = None self.config_controller = None self.dmf_device_controller = None self.protocol_controller = None self.main_window_controller = None # Enable custom logging handler logging.getLogger().addHandler(CustomHandler()) self.log_file_handler = None # config model try: self.config = Config(args.config) except IOError: logging.error( 'Could not read configuration file, `%s`. Make sure' ' it exists and is readable.', args.config) raise SystemExit(-1) # set the log level if self.name in self.config.data and ('log_level' in self.config.data[self.name]): self._set_log_level(self.config.data[self.name]['log_level']) logger.info('MicroDrop version: %s', self.version) logger.info('Running in working directory: %s', os.getcwd()) # Run post install hooks for freshly installed plugins. # It is necessary to delay the execution of these hooks here due to # Windows file locking preventing the deletion of files that are in use. post_install_queue_path = \ path(self.config.data['plugins']['directory']) \ .joinpath('post_install_queue.yml') if post_install_queue_path.isfile(): post_install_queue = yaml.load(post_install_queue_path.bytes()) post_install_queue = map(path, post_install_queue) logger.info('[App] processing post install hooks.') for p in post_install_queue[:]: try: info = get_plugin_info(p) logger.info(" running post install hook for %s" % info.plugin_name) plugin_manager.post_install(p) except Exception: logging.info(''.join(traceback.format_exc())) logging.error('Error running post-install hook for %s.', p.name, exc_info=True) finally: post_install_queue.remove(p) post_install_queue_path.write_bytes(yaml.dump(post_install_queue)) # Delete paths that were marked during the uninstallation of a plugin. # It is necessary to delay the deletion until here due to Windows file # locking preventing the deletion of files that are in use. deletions_path = path(self.config.data['plugins']['directory'])\ .joinpath('requested_deletions.yml') if deletions_path.isfile(): requested_deletions = yaml.load(deletions_path.bytes()) requested_deletions = map(path, requested_deletions) logger.info('[App] processing requested deletions.') for p in requested_deletions[:]: try: if p != p.abspath(): logger.info( ' (warning) ignoring path %s since it ' 'is not absolute', p) continue if p.isdir(): info = get_plugin_info(p) if info: logger.info(' deleting %s' % p) cwd = os.getcwd() os.chdir(p.parent) try: path(p.name).rmtree() #ignore_errors=True) except Exception, why: logger.warning('Error deleting path %s (%s)', p, why) raise os.chdir(cwd) requested_deletions.remove(p) else: # if the directory doesn't exist, remove it from the # list requested_deletions.remove(p) except (AssertionError, ): logger.info(' NOT deleting %s' % (p)) continue except (Exception, ): logger.info(' NOT deleting %s' % (p)) continue
def on_edit_calibration(self, widget=None, data=None): if not self.control_board.connected(): logging.error("A control board must be connected in order to " "edit calibration settings.") return hardware_version = utility.Version.fromstring( self.control_board.hardware_version()) schema_entries = [] settings = {} settings['amplifier_gain'] = self.control_board.amplifier_gain() schema_entries.append( Float.named('amplifier_gain').using( default=settings['amplifier_gain'], optional=True, validators=[ValueAtLeast(minimum=0.01), ]), ) settings['auto_adjust_amplifier_gain'] = self.control_board \ .auto_adjust_amplifier_gain() schema_entries.append( Boolean.named('auto_adjust_amplifier_gain').using( default=settings['auto_adjust_amplifier_gain'], optional=True), ) settings['voltage_tolerance'] = \ self.control_board.voltage_tolerance(); schema_entries.append( Float.named('voltage_tolerance').using( default=settings['voltage_tolerance'], optional=True, validators=[ValueAtLeast(minimum=0),]), ) if hardware_version.major == 1: settings['WAVEOUT_GAIN_1'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_WAVEOUT_GAIN_1_ADDRESS) schema_entries.append( Integer.named('WAVEOUT_GAIN_1').using( default=settings['WAVEOUT_GAIN_1'], optional=True, validators=[ValueAtLeast(minimum=0), ValueAtMost(maximum=255),]), ) settings['VGND'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_VGND_ADDRESS) schema_entries.append( Integer.named('VGND').using( default=settings['VGND'], optional=True, validators=[ValueAtLeast(minimum=0), ValueAtMost(maximum=255),]), ) else: settings['SWITCHING_BOARD_I2C_ADDRESS'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_SWITCHING_BOARD_I2C_ADDRESS) schema_entries.append( Integer.named('SWITCHING_BOARD_I2C_ADDRESS').using( default=settings['SWITCHING_BOARD_I2C_ADDRESS'], optional=True, validators=[ValueAtLeast(minimum=0), ValueAtMost(maximum=255),]), ) settings['SIGNAL_GENERATOR_BOARD_I2C_ADDRESS'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_SIGNAL_GENERATOR_BOARD_I2C_ADDRESS) schema_entries.append( Integer.named('SIGNAL_GENERATOR_BOARD_I2C_ADDRESS').using( default=settings['SIGNAL_GENERATOR_BOARD_I2C_ADDRESS'], optional=True, validators=[ValueAtLeast(minimum=0), ValueAtMost(maximum=255),]), ) for i in range(len(self.control_board.calibration.R_hv)): settings['R_hv_%d' % i] = self.control_board.calibration.R_hv[i] schema_entries.append( Float.named('R_hv_%d' % i).using( default=settings['R_hv_%d' % i], optional=True, validators=[ValueAtLeast(minimum=0),])) settings['C_hv_%d' % i] =\ self.control_board.calibration.C_hv[i]*1e12 schema_entries.append( Float.named('C_hv_%d' % i).using( default=settings['C_hv_%d' % i], optional=True, validators=[ValueAtLeast(minimum=0),])) for i in range(len(self.control_board.calibration.R_fb)): settings['R_fb_%d' % i] = self.control_board.calibration.R_fb[i] schema_entries.append( Float.named('R_fb_%d' % i).using( default=settings['R_fb_%d' % i], optional=True, validators=[ValueAtLeast(minimum=0),])) settings['C_fb_%d' % i] = \ self.control_board.calibration.C_fb[i]*1e12 schema_entries.append( Float.named('C_fb_%d' % i).using( default=settings['C_fb_%d' % i], optional=True, validators=[ValueAtLeast(minimum=0),])) form = Form.of(*schema_entries) dialog = FormViewDialog('Edit calibration settings') valid, response = dialog.run(form) if valid: for k, v in response.items(): if settings[k] != v: m = re.match('(R|C)_(hv|fb)_(\d)', k) if k=='amplifier_gain': self.control_board.set_amplifier_gain(v) elif k=='auto_adjust_amplifier_gain': self.control_board.set_auto_adjust_amplifier_gain(v) elif k=='WAVEOUT_GAIN_1': self.control_board.eeprom_write( self.control_board.EEPROM_WAVEOUT_GAIN_1_ADDRESS, v) elif k=='VGND': self.control_board.eeprom_write( self.control_board.EEPROM_VGND_ADDRESS, v) elif k=='SWITCHING_BOARD_I2C_ADDRESS': self.control_board.eeprom_write( self.control_board.EEPROM_SWITCHING_BOARD_I2C_ADDRESS, v) elif k=='SIGNAL_GENERATOR_BOARD_I2C_ADDRESS': self.control_board.eeprom_write( self.control_board.EEPROM_SIGNAL_GENERATOR_BOARD_I2C_ADDRESS, v) elif k=='voltage_tolerance': self.control_board.set_voltage_tolerance(v) elif m: series_resistor = int(m.group(3)) if m.group(2)=='hv': channel = 0 else: channel = 1 self.control_board.set_series_resistor_index(channel, series_resistor) if m.group(1)=='R': self.control_board.set_series_resistance(channel, v) else: if v is None: v=0 self.control_board.set_series_capacitance(channel, v/1e12) # reconnect to update settings self.connect() if get_app().protocol: self.on_step_run()
def run(self, forms, initial_values=None): # Empty plugin form vbox # Get list of app option forms self.forms = forms self.form_views = {} self.clear_form() app = get_app() core_plugins_count = 0 for name, form in self.forms.iteritems(): # For each form, generate a pygtkhelpers formview and append the view # onto the end of the plugin vbox if len(form.field_schema) == 0: continue # Only include fields that do not have show_in_gui set to False in # 'properties' dictionary schema_entries = [f for f in form.field_schema\ if f.properties.get('show_in_gui', True)] gui_form = Form.of(*[Boolean.named(s.name).using(default=True, optional=True) for s in schema_entries]) FormView.schema_type = gui_form if not schema_entries: continue self.form_views[name] = FormView() if name in app.core_plugins: self.core_plugins_vbox.pack_start(self.form_views[name].widget) core_plugins_count += 1 else: expander = gtk.Expander() expander.set_label(name) expander.set_expanded(True) expander.add(self.form_views[name].widget) self.plugin_form_vbox.pack_start(expander) if core_plugins_count == 0: self.frame_core_plugins.hide() self.plugin_form_vbox.remove(self.frame_core_plugins) else: if not self.frame_core_plugins in self.plugin_form_vbox.children(): self.plugin_form_vbox.pack_start(self.frame_core_plugins) self.frame_core_plugins.show() if not initial_values: initial_values = {} for form_name, form in self.forms.iteritems(): if not form.field_schema: continue form_view = self.form_views[form_name] values = initial_values.get(form_name, {}) for name, field in form_view.form.fields.items(): if name in values or not initial_values: value = True else: value = False logger.debug('set %s to %s' % (name, value)) proxy = proxy_for(getattr(form_view, name)) proxy.set_widget_value(value) field.label_widget.set_text( re.sub(r'_', ' ', name).title()) self.dialog.show_all() response = self.dialog.run() if response == gtk.RESPONSE_OK: self.apply() elif response == gtk.RESPONSE_CANCEL: pass self.dialog.hide() return response
def AppFields(self): return Form.of( Directory.named('notebook_directory').using(default='', optional=True), )
def run(self, forms, initial_values=None): # Empty plugin form vbox # Get list of app option forms self.forms = forms self.form_views = {} self.clear_form() app = get_app() core_plugins_count = 0 for name, form in self.forms.iteritems(): # For each form, generate a pygtkhelpers formview and append the view # onto the end of the plugin vbox if len(form.field_schema) == 0: continue # Only include fields that do not have show_in_gui set to False in # 'properties' dictionary schema_entries = [f for f in form.field_schema\ if f.properties.get('show_in_gui', True)] gui_form = Form.of(*[ Boolean.named(s.name).using(default=True, optional=True) for s in schema_entries ]) FormView.schema_type = gui_form if not schema_entries: continue self.form_views[name] = FormView() if name in app.core_plugins: self.core_plugins_vbox.pack_start(self.form_views[name].widget) core_plugins_count += 1 else: expander = gtk.Expander() expander.set_label(name) expander.set_expanded(True) expander.add(self.form_views[name].widget) self.plugin_form_vbox.pack_start(expander) if core_plugins_count == 0: self.frame_core_plugins.hide() self.plugin_form_vbox.remove(self.frame_core_plugins) else: if not self.frame_core_plugins in self.plugin_form_vbox.children(): self.plugin_form_vbox.pack_start(self.frame_core_plugins) self.frame_core_plugins.show() if not initial_values: initial_values = {} for form_name, form in self.forms.iteritems(): if not form.field_schema: continue form_view = self.form_views[form_name] values = initial_values.get(form_name, {}) for name, field in form_view.form.fields.items(): if name in values or not initial_values: value = True else: value = False logger.debug('set %s to %s' % (name, value)) proxy = proxy_for(getattr(form_view, name)) proxy.set_widget_value(value) field.label_widget.set_text(re.sub(r'_', ' ', name).title()) self.dialog.show_all() response = self.dialog.run() if response == gtk.RESPONSE_OK: self.apply() elif response == gtk.RESPONSE_CANCEL: pass self.dialog.hide() return response
class DropletPlanningPlugin(Plugin, StepOptionsController): """ This class is automatically registered with the PluginManager. .. versionchanged:: 2.4 Refactor to implement the `IElectrodeMutator` interface, which delegates route execution to the ``microdrop.electrode_controller_plugin``. .. versionchanged:: 2.5.2 Explicitly set human-readable title for ``repeat_duration_s`` step field to ``"Repeat duration (s)"``. """ implements(IPlugin) implements(IElectrodeMutator) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Integer.named('trail_length').using( default=1, optional=True, validators=[ValueAtLeast(minimum=1)]), Integer.named('route_repeats').using( default=1, optional=True, validators=[ValueAtLeast(minimum=1)]), Integer.named('repeat_duration_s').using( default=0, optional=True, properties={'title': 'Repeat ' 'duration (s)'})) def __init__(self): self.name = self.plugin_name self._electrode_states = iter([]) self.plugin = None self.executor = ThreadPoolExecutor(max_workers=1) self._plugin_monitor_task = None def get_schedule_requests(self, function_name): """ .. versionchanged:: 2.5 Enable _after_ command plugin and zmq hub to ensure command can be registered. .. versionchanged:: 2.5.3 Remove scheduling requests for deprecated `on_step_run()` method. """ if function_name == 'on_plugin_enable': return [ ScheduleRequest('microdrop.zmq_hub_plugin', self.name), ScheduleRequest('microdrop.command_plugin', self.name) ] return [] def on_plugin_enable(self): ''' .. versionchanged:: 2.5 - Use `zmq_plugin.plugin.watch_plugin()` to monitor ZeroMQ interface in background thread. - Register `clear_routes` commands with ``microdrop.command_plugin``. ''' self.cleanup() self.plugin = RouteControllerZmqPlugin(self, self.name, get_hub_uri()) self._plugin_monitor_task = watch_plugin(self.executor, self.plugin) hub_execute_async('microdrop.command_plugin', 'register_command', command_name='clear_routes', namespace='global', plugin_name=self.name, title='Clear all r_outes') hub_execute_async('microdrop.command_plugin', 'register_command', command_name='clear_routes', namespace='electrode', plugin_name=self.name, title='Clear electrode ' '_routes') def on_plugin_disable(self): """ Handler called once the plugin instance is disabled. """ self.cleanup() def on_app_exit(self): """ Handler called just before the Microdrop application exits. """ self.cleanup() def cleanup(self): if self.plugin is not None: self.plugin = None if self._plugin_monitor_task is not None: self._plugin_monitor_task.cancel() ########################################################################### # Step event handler methods def get_electrode_states_request(self): try: return self._electrode_states.next() except StopIteration: return None def on_step_options_swapped(self, plugin, old_step_number, step_number): """ Handler called when the step options are changed for a particular plugin. This will, for example, allow for GUI elements to be updated based on step specified. Parameters ---------- plugin : plugin instance for which the step options changed old_step_number : int Previous step number. step_number : int Current step number that the options changed for. """ self.reset_electrode_states_generator() def on_step_swapped(self, old_step_number, step_number): """ Handler called when the current step is swapped. """ self.reset_electrode_states_generator() if self.plugin is not None: self.plugin.execute_async(self.name, 'get_routes') def on_step_inserted(self, step_number, *args): self.clear_routes(step_number=step_number) self._electrode_states = iter([]) ########################################################################### # Step options dependent methods def add_route(self, electrode_ids): ''' Add droplet route. Args: electrode_ids (list) : Ordered list of identifiers of electrodes on route. ''' drop_routes = self.get_routes() route_i = (drop_routes.route_i.max() + 1 if drop_routes.shape[0] > 0 else 0) drop_route = (pd.DataFrame(electrode_ids, columns=[ 'electrode_i' ]).reset_index().rename(columns={'index': 'transition_i'})) drop_route.insert(0, 'route_i', route_i) drop_routes = drop_routes.append(drop_route, ignore_index=True) self.set_routes(drop_routes) return {'route_i': route_i, 'drop_routes': drop_routes} def clear_routes(self, electrode_id=None, step_number=None): ''' Clear all drop routes for protocol step that include the specified electrode (identified by string identifier). ''' step_options = self.get_step_options(step_number) if electrode_id is None: # No electrode identifier specified. Clear all step routes. df_routes = RouteController.default_routes() else: df_routes = step_options['drop_routes'] # Find indexes of all routes that include electrode. routes_to_clear = df_routes.loc[df_routes.electrode_i == electrode_id, 'route_i'] # Remove all routes that include electrode. df_routes = df_routes.loc[~df_routes.route_i. isin(routes_to_clear.tolist())].copy() step_options['drop_routes'] = df_routes self.set_step_values(step_options, step_number=step_number) def get_routes(self, step_number=None): step_options = self.get_step_options(step_number=step_number) return step_options.get('drop_routes', RouteController.default_routes()) def set_routes(self, df_routes, step_number=None): step_options = self.get_step_options(step_number=step_number) step_options['drop_routes'] = df_routes self.set_step_values(step_options, step_number=step_number) def reset_electrode_states_generator(self): ''' Reset iterator over actuation states of electrodes in routes table. ''' df_routes = self.get_routes() step_options = self.get_step_options() _L().debug('df_routes=%s\nstep_options=%s', df_routes, step_options) self._electrode_states = \ electrode_states(df_routes, trail_length=step_options['trail_length'], repeats=step_options['route_repeats'], repeat_duration_s=step_options ['repeat_duration_s'])
class DropletPlanningPlugin(Plugin, StepOptionsController, pmh.BaseMqttReactor): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Integer.named('trail_length').using(default=1, optional=True, validators= [ValueAtLeast(minimum=1)]), Integer.named('route_repeats').using(default=1, optional=True, validators= [ValueAtLeast(minimum=1)]), Integer.named('repeat_duration_s').using(default=0, optional=True), Integer.named('transition_duration_ms') .using(optional=True, default=750, validators=[ValueAtLeast(minimum=0)])) def __init__(self,*args, **kwargs): self.name = self.plugin_name self.step_start_time = None self.route_controller = None pmh.BaseMqttReactor.__init__(self) self.start() def get_schedule_requests(self, function_name): """ Returns a list of scheduling requests (i.e., ScheduleRequest instances) for the function specified by function_name. """ if function_name in ['on_step_run']: # Execute `on_step_run` before control board. return [ScheduleRequest(self.name, 'dmf_control_board_plugin')] return [] def on_connect(self, client, userdata, flags, rc): self.mqtt_client.subscribe("microdrop/dmf-device-ui/add-route") self.mqtt_client.subscribe("microdrop/dmf-device-ui/get-routes") self.mqtt_client.subscribe("microdrop/dmf-device-ui/clear-routes") self.mqtt_client.subscribe('microdrop/dmf-device-ui/execute-routes') self.mqtt_client.subscribe('microdrop/dmf-device-ui/update-protocol') self.mqtt_client.subscribe("microdrop/mqtt-plugin/step-inserted") def on_message(self, client, userdata, msg): ''' Callback for when a ``PUBLISH`` message is received from the broker. ''' logger.info('[on_message] %s: "%s"', msg.topic, msg.payload) if msg.topic == 'microdrop/dmf-device-ui/add-route': self.add_route(json.loads(msg.payload)) self.get_routes() if msg.topic == 'microdrop/dmf-device-ui/get-routes': self.get_routes() if msg.topic == 'microdrop/dmf-device-ui/clear-routes': data = json.loads(msg.payload) if data: self.clear_routes(electrode_id=data['electrode_id']) else: self.clear_routes() if msg.topic == 'microdrop/dmf-device-ui/execute-routes': self.execute_routes(json.loads(msg.payload)) if msg.topic == 'microdrop/dmf-device-ui/update-protocol': self.update_protocol(json.loads(msg.payload)) if msg.topic == "microdrop/mqtt-plugin/step-inserted": self.step_inserted(json.loads(msg.payload)) def on_plugin_enable(self): self.route_controller = RouteController(self) form = flatlandToDict(self.StepFields) self.mqtt_client.publish('microdrop/droplet-planning-plugin/schema', json.dumps(form), retain=True) defaults = {} for k,v in form.iteritems(): defaults[k] = v['default'] self.mqtt_client.publish('microdrop/droplet-planning-plugin/step-options', json.dumps([defaults], cls=PandasJsonEncoder), retain=True) def on_plugin_disable(self): """ Handler called once the plugin instance is disabled. """ pass def on_app_exit(self): """ Handler called just before the Microdrop application exits. """ pass ########################################################################### # Step event handler methods def on_error(self, *args): logger.error('Error executing routes.', exc_info=True) # An error occurred while initializing Analyst remote control. emit_signal('on_step_complete', [self.name, 'Fail']) def on_protocol_pause(self): self.kill_running_step() def kill_running_step(self): # Stop execution of any routes that are currently running. if self.route_controller is not None: self.route_controller.reset() def on_step_run(self): """ Handler called whenever a step is executed. Note that this signal is only emitted in realtime mode or if a protocol is running. Plugins that handle this signal must emit the on_step_complete signal once they have completed the step. The protocol controller will wait until all plugins have completed the current step before proceeding. return_value can be one of: None 'Repeat' - repeat the step or 'Fail' - unrecoverable error (stop the protocol) """ app = get_app() if not app.running: return self.kill_running_step() step_options = self.get_step_options() try: self.repeat_i = 0 self.step_start_time = datetime.now() df_routes = self.get_routes() self.route_controller.execute_routes( df_routes, step_options['transition_duration_ms'], trail_length=step_options['trail_length'], on_complete=self.on_step_routes_complete, on_error=self.on_error) except: self.on_error() def on_step_routes_complete(self, start_time, electrode_ids): ''' Callback function executed when all concurrent routes for a step have completed a single run. If repeats are requested, either through repeat counts or a repeat duration, *cycle* routes (i.e., routes that terminate at the start electrode) will repeat as necessary. ''' step_options = self.get_step_options() step_duration_s = (datetime.now() - self.step_start_time).total_seconds() if ((step_options['repeat_duration_s'] > 0 and step_duration_s < step_options['repeat_duration_s']) or (self.repeat_i + 1 < step_options['route_repeats'])): # Either repeat duration has not been met, or the specified number # of repetitions has not been met. Execute another iteration of # the routes. self.repeat_i += 1 df_routes = self.get_routes() self.route_controller.execute_routes( df_routes, step_options['transition_duration_ms'], trail_length=step_options['trail_length'], cyclic=True, acyclic=False, on_complete=self.on_step_routes_complete, on_error=self.on_error) else: logger.info('Completed routes (%s repeats in %ss)', self.repeat_i + 1, si_format(step_duration_s)) # Transitions along all droplet routes have been processed. # Signal step has completed and reset plugin step state. emit_signal('on_step_complete', [self.name, None]) def on_step_options_swapped(self, plugin, old_step_number, step_number): """ Handler called when the step options are changed for a particular plugin. This will, for example, allow for GUI elements to be updated based on step specified. Parameters: plugin : plugin instance for which the step options changed step_number : step number that the options changed for """ logger.info('[on_step_swapped] old step=%s, step=%s', old_step_number, step_number) self.kill_running_step() def on_step_removed(self, step_number, step): self.update_steps() def on_step_options_changed(self, plugin, step_number): self.update_steps() def on_step_swapped(self, old_step_number, step_number): """ Handler called when the current step is swapped. """ logger.info('[on_step_swapped] old step=%s, step=%s', old_step_number, step_number) self.kill_running_step() self.get_routes() def on_step_inserted(self, step_number, *args): self.step_inserted(step_number) def step_inserted(self, step_number): app = get_app() logger.info('[on_step_inserted] current step=%s, created step=%s', app.protocol.current_step_number, step_number) self.clear_routes(step_number=step_number) ########################################################################### # Step options dependent methods def update_protocol(self, protocol): app = get_app() for i, s in enumerate(protocol): step = app.protocol.steps[i] prevData = step.get_data(self.plugin_name) values = {} for k,v in prevData.iteritems(): if k in s: values[k] = s[k] step.set_data(self.plugin_name, values) emit_signal('on_step_options_changed', [self.plugin_name, i], interface=IPlugin) def add_route(self, electrode_ids): ''' Add droplet route. Args: electrode_ids (list) : Ordered list of identifiers of electrodes on route. ''' drop_routes = self.get_routes() route_i = (drop_routes.route_i.max() + 1 if drop_routes.shape[0] > 0 else 0) drop_route = (pd.DataFrame(electrode_ids, columns=['electrode_i']) .reset_index().rename(columns={'index': 'transition_i'})) drop_route.insert(0, 'route_i', route_i) drop_routes = drop_routes.append(drop_route, ignore_index=True) self.set_routes(drop_routes) return {'route_i': route_i, 'drop_routes': drop_routes} def clear_routes(self, electrode_id=None, step_number=None): ''' Clear all drop routes for protocol step that include the specified electrode (identified by string identifier). ''' step_options = self.get_step_options(step_number) if electrode_id is None: # No electrode identifier specified. Clear all step routes. df_routes = RouteController.default_routes() else: df_routes = step_options['drop_routes'] # Find indexes of all routes that include electrode. routes_to_clear = df_routes.loc[df_routes.electrode_i == electrode_id, 'route_i'] # Remove all routes that include electrode. df_routes = df_routes.loc[~df_routes.route_i .isin(routes_to_clear.tolist())].copy() step_options['drop_routes'] = df_routes self.set_step_values(step_options, step_number=step_number) self.get_routes() def get_routes(self, step_number=None): step_options = self.get_step_options(step_number=step_number) x = step_options.get('drop_routes', RouteController.default_routes()) msg = json.dumps(x, cls=PandasJsonEncoder) self.mqtt_client.publish('microdrop/droplet-planning-plugin/routes-set', msg, retain=True) return x def execute_routes(self, data): # TODO allow for passing of both electrode_id and route_i # Currently electrode_id only try: df_routes = self.get_routes() step_options = self.get_step_options() if 'transition_duration_ms' in data: transition_duration_ms = data['transition_duration_ms'] else: transition_duration_ms = step_options['transition_duration_ms'] if 'trail_length' in data: trail_length = data['trail_length'] else: trail_length = step_options['trail_length'] if 'route_i' in data: df_routes = df_routes.loc[df_routes.route_i == data['route_i']] elif 'electrode_i' in data: if data['electrode_i'] is not None: routes_to_execute = df_routes.loc[df_routes.electrode_i == data['electrode_i'], 'route_i'] df_routes = df_routes.loc[df_routes.route_i .isin(routes_to_execute .tolist())].copy() route_controller = RouteController(self) route_controller.execute_routes(df_routes, transition_duration_ms, trail_length=trail_length) except: logger.error(str(data), exc_info=True) def update_steps(self): app = get_app() num_steps = len(app.protocol.steps) protocol = [] for i in range(num_steps): protocol.append(self.get_step_options(i)) self.mqtt_client.publish('microdrop/droplet-planning-plugin/step-options', json.dumps(protocol, cls=PandasJsonEncoder), retain=True) def set_routes(self, df_routes, step_number=None): step_options = self.get_step_options(step_number=step_number) step_options['drop_routes'] = df_routes self.set_step_values(step_options, step_number=step_number)
class TestPlugin(Plugin, AppDataController, StepOptionsController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugins_name = get_plugin_info(path(__file__).parent).plugin_name ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' serial_ports_ = [port for port in serial_device.get_serial_ports()] if len(serial_ports_): default_port_ = serial_ports_[0] else: default_port_ = None AppFields = Form.of( Enum.named('serial_port').using(default=default_port_, optional=True)\ .valued(*serial_ports_), ) ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Boolean.named('led_on').using(default=False, optional=True), ) def __init__(self): self.name = self.plugins_name self.proxy = None def on_plugin_enable(self): # We need to call AppDataController's on_plugin_enable() to update the # application options data. AppDataController.on_plugin_enable(self) self.on_app_init() app_values = self.get_app_values() try: self.proxy = SerialProxy(port=app_values['serial_port']) self.proxy.pin_mode(pin=13, mode=1) logger.info('Connected to %s on port %s', self.proxy.properties.display_name, app_values['serial_port']) except Exception, e: logger.error('Could not connect to base-node-rpc on port %s: %s.', app_values['serial_port'], e) if get_app().protocol: pgc = get_service_instance(ProtocolGridController, env='microdrop') pgc.update_grid()
class DmfDeviceController(SingletonPlugin, AppDataController): implements(IPlugin) AppFields = Form.of(Directory.named('device_directory') .using(default='', optional=True)) def __init__(self): self.name = "microdrop.gui.dmf_device_controller" self.previous_device_dir = None self._modified = False @property def modified(self): return self._modified @modified.setter def modified(self, value): self._modified = value if getattr(self, 'menu_rename_dmf_device', None): self.menu_rename_dmf_device.set_sensitive(not value) self.menu_save_dmf_device.set_sensitive(value) @gtk_threadsafe def on_app_options_changed(self, plugin_name): try: if plugin_name == self.name: values = self.get_app_values() if 'device_directory' in values: self.apply_device_dir(values['device_directory']) except (Exception,): _L().info(''.join(traceback.format_exc())) raise def apply_device_dir(self, device_directory): app = get_app() # if the device directory is empty or None, set a default if not device_directory: device_directory = (ph.path(app.config.data['data_dir']) .joinpath('devices')) self.set_app_values({'device_directory': device_directory}) if self.previous_device_dir and (device_directory == self.previous_device_dir): # If the data directory hasn't changed, we do nothing return False device_directory = ph.path(device_directory) if self.previous_device_dir: device_directory.makedirs_p() if device_directory.listdir(): result = yesno('Merge?', '''\ Target directory [%s] is not empty. Merge contents with current devices [%s] (overwriting common paths in the target directory)?''' % (device_directory, self.previous_device_dir)) if not result == gtk.RESPONSE_YES: return False original_directory = ph.path(self.previous_device_dir) for d in original_directory.dirs(): copytree(d, device_directory.joinpath(d.name)) for f in original_directory.files(): f.copyfile(device_directory.joinpath(f.name)) original_directory.rmtree() elif not device_directory.isdir(): # if the device directory doesn't exist, copy the skeleton dir if device_directory.parent: device_directory.parent.makedirs_p() base_path().joinpath('devices').copytree(device_directory) self.previous_device_dir = device_directory return True def on_plugin_enable(self): ''' .. versionchanged:: 2.11.2 Use :func:`gtk_threadsafe` decorator to wrap GTK code blocks, ensuring the code runs in the main GTK thread. ''' app = get_app() app.dmf_device_controller = self defaults = self.get_default_app_options() data = app.get_data(self.name) for k, v in defaults.items(): if k not in data: data[k] = v app.set_data(self.name, data) emit_signal('on_app_options_changed', [self.name]) self.menu_detect_connections = \ app.builder.get_object('menu_detect_connections') self.menu_import_dmf_device = \ app.builder.get_object('menu_import_dmf_device') self.menu_load_dmf_device = \ app.builder.get_object('menu_load_dmf_device') self.menu_rename_dmf_device = \ app.builder.get_object('menu_rename_dmf_device') self.menu_save_dmf_device = \ app.builder.get_object('menu_save_dmf_device') self.menu_save_dmf_device_as = \ app.builder.get_object('menu_save_dmf_device_as') app.signals["on_menu_detect_connections_activate"] = \ self.on_detect_connections app.signals["on_menu_import_dmf_device_activate"] = \ self.on_import_dmf_device app.signals["on_menu_load_dmf_device_activate"] = \ self.on_load_dmf_device app.signals["on_menu_rename_dmf_device_activate"] = \ self.on_rename_dmf_device app.signals["on_menu_save_dmf_device_activate"] = \ self.on_save_dmf_device app.signals["on_menu_save_dmf_device_as_activate"] = \ self.on_save_dmf_device_as @gtk_threadsafe def _init_ui(): # disable menu items until a device is loaded self.menu_detect_connections.set_sensitive(False) self.menu_rename_dmf_device.set_sensitive(False) self.menu_save_dmf_device.set_sensitive(False) self.menu_save_dmf_device_as.set_sensitive(False) _init_ui() def on_protocol_pause(self): pass def on_app_exit(self): self.save_check() def load_device(self, file_path, **kwargs): ''' Load device file. Parameters ---------- file_path : str A MicroDrop device `.svg` file or a (deprecated) MicroDrop 1.0 device. ''' logger = _L() # use logger with method context app = get_app() self.modified = False device = app.dmf_device file_path = ph.path(file_path) if not file_path.isfile(): old_version_file_path = (file_path.parent .joinpath(OLD_DEVICE_FILENAME)) if old_version_file_path: # SVG device file does not exist, but old-style (i.e., v0.3.0) # device file found. try: # Try to import old-style device to new SVG format. self.import_device(old_version_file_path) logger.warning('Auto-converted old-style device to new SVG' ' device format. Open in Inkscape to ' 'verify scale and adjacent electrode ' 'connections.') except Exception, e: logger.error('Error importing device. %s', e, exc_info=True) return else: logger.error('Error opening device. Please ensure file ' 'exists and is readable.', exc_info=True) return # SVG device file exists. Load the device. try: logger.info('[DmfDeviceController].load_device: %s' % file_path) if app.get_device_directory().realpath() == (file_path.realpath() .parent.parent): # Selected device file is in MicroDrop devices directory. new_device = False else: # Selected device file is not in MicroDrop devices directory. # Copy file to devices directory under subdirectory with same # name as file. new_device_directory = (app.get_device_directory().realpath() .joinpath(file_path.namebase) .noconflict()) new_device_directory.makedirs_p() new_file_path = new_device_directory.joinpath('device.svg') file_path.copy(new_file_path) file_path = new_file_path new_device = True # Load device from SVG file. device = DmfDevice.load(file_path, name=file_path.parent.name, **kwargs) if new_device: # Inform user that device was copied from original location # into MicroDrop devices directory. pgh.ui.dialogs.info('Device imported successfully', long='New device copied into MicroDrop ' 'devices directory:\n{}'.format(file_path), parent=app.main_window_controller.view) logger.info('[DmfDeviceController].load_device: Copied new ' 'device to: %s', file_path) emit_signal("on_dmf_device_swapped", [app.dmf_device, device]) except Exception: logger.error('Error loading device.', exc_info=True)
class ZmqHubPlugin(SingletonPlugin, AppDataController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) plugin_name = 'microdrop.zmq_hub_plugin' ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' AppFields = Form.of( String.named('hub_uri').using(optional=True, default='tcp://*:31000'), Enum.named('log_level').using(default='info', optional=True) .valued('debug', 'info', 'warning', 'error', 'critical')) def __init__(self): self.name = self.plugin_name self.hub_process = None #: ..versionadded:: 2.25 self.exec_thread = None def on_plugin_enable(self): ''' .. versionchanged:: 2.25 Start asyncio event loop in background thread to process ZeroMQ hub execution requests. ''' super(ZmqHubPlugin, self).on_plugin_enable() app_values = self.get_app_values() self.cleanup() self.hub_process = Process(target=_safe_run_hub, args=(MicroDropHub(app_values['hub_uri'], self.name), getattr(logging, app_values['log_level'] .upper()))) # Set process as daemonic so it terminate when main process terminates. self.hub_process.daemon = True self.hub_process.start() _L().info('ZeroMQ hub process (pid=%s, daemon=%s)', self.hub_process.pid, self.hub_process.daemon) zmq_ready = threading.Event() @asyncio.coroutine def _exec_task(): self.zmq_plugin = ZmqPlugin('microdrop', get_hub_uri()) self.zmq_plugin.reset() zmq_ready.set() event = asyncio.Event() try: yield asyncio.From(event.wait()) except asyncio.CancelledError: _L().info('closing ZeroMQ execution event loop') self.zmq_exec_task = cancellable(_exec_task) self.exec_thread = threading.Thread(target=self.zmq_exec_task) self.exec_thread.deamon = True self.exec_thread.start() zmq_ready.wait() def cleanup(self): ''' .. versionchanged:: 2.25 Stop asyncio event loop. ''' if self.hub_process is not None: self.hub_process.terminate() self.hub_process = None if self.exec_thread is not None: self.zmq_exec_task.cancel() self.exec_thread = None
def fields_frame_to_flatland_form_class(df_fields, sep='.'): # Create Flatland form class from jsonschema schema. return Form.of(*[get_flatland_field(row).named(sep.join(row.parents + (row.field, ))) for i, row in df_fields.iterrows()])
class DmfDeviceUiPlugin(AppDataController, StepOptionsController, Plugin): """ This class is automatically registered with the PluginManager. .. versionchanged:: 2.10 Set default window size and position according to **screen size** *and* **window titlebar size**. Also, force default window size if ``MICRODROP_FIRST_RUN`` environment variable is set to non-empty value. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name AppFields = Form.of( String.named('video_config').using(default='', optional=True, properties={'show_in_gui': False}), String.named('surface_alphas').using(default='', optional=True, properties={'show_in_gui': False}), String.named('canvas_corners').using(default='', optional=True, properties={'show_in_gui': False}), String.named('frame_corners').using(default='', optional=True, properties={'show_in_gui': False}), Integer.named('x').using(default=.5 * SCREEN_WIDTH, optional=True, properties={'show_in_gui': False}), Integer.named('y').using(default=SCREEN_TOP, optional=True, properties={'show_in_gui': False}), Integer.named('width').using(default=.5 * SCREEN_WIDTH, optional=True, properties={'show_in_gui': False}), Integer.named('height').using(default=SCREEN_HEIGHT - 1.5 * TITLEBAR_HEIGHT, optional=True, properties={'show_in_gui': False})) StepFields = Form.of( Boolean.named('video_enabled').using(default=True, optional=True, properties={'title': 'Video'})) def __init__(self): self.name = self.plugin_name self.gui_process = None self.gui_heartbeat_id = None self._gui_enabled = False self.alive_timestamp = None def reset_gui(self): ''' .. versionchanged:: 2.2.2 Use :func:`pygtkhelpers.gthreads.gtk_threadsafe` decorator around function to wait for GUI process, rather than using :func:`gobject.idle_add`, to make intention clear. .. versionchanged:: 2.9 Refresh list of registered commands once device UI process has started. The list of registered commands is used to dynamically generate items in the device UI context menu. ''' py_exe = sys.executable # Set allocation based on saved app values (i.e., remember window size # and position from last run). app_values = self.get_app_values() if os.environ.get('MICRODROP_FIRST_RUN'): # Use default options for window allocation. default_app_values = self.get_default_app_options() for k in ('x', 'y', 'width', 'height'): app_values[k] = default_app_values[k] allocation_args = ['-a', json.dumps(app_values)] app = get_app() if app.config.data.get('advanced_ui', False): debug_args = ['-d'] else: debug_args = [] self.gui_process = Popen( [py_exe, '-m', 'dmf_device_ui.bin.device_view', '-n', self.name] + allocation_args + debug_args + ['fixed', get_hub_uri()], creationflags=CREATE_NEW_PROCESS_GROUP) self._gui_enabled = True def keep_alive(): if not self._gui_enabled: self.alive_timestamp = None return False elif self.gui_process.poll() == 0: # GUI process has exited. Restart. self.cleanup() self.reset_gui() return False else: self.alive_timestamp = datetime.now() # Keep checking. return True self.step_video_settings = None @gtk_threadsafe def _wait_for_gui(): self.wait_for_gui_process() # Get current video settings from UI. app_values = self.get_app_values() # Convert JSON settings to 0MQ plugin API Python types. ui_settings = self.json_settings_as_python(app_values) self.set_ui_settings(ui_settings, default_corners=True) self.gui_heartbeat_id = gobject.timeout_add(1000, keep_alive) # Refresh list of electrode and route commands. hub_execute('microdrop.command_plugin', 'get_commands') # Call as thread-safe function, since function uses GTK. _wait_for_gui() def cleanup(self): ''' .. versionchanged:: 2.2.2 Catch any exception encountered during GUI process termination. .. versionchanged:: 2.3.1 Use :func:`kill_process_tree` to terminate DMF device UI process. This ensures any child processes of the UI process (e.g., video input process) are also killed. See also: https://stackoverflow.com/a/44648162/345236 .. versionchanged:: 2.7 Only try to terminate the GUI process if it is still running. ''' logger.info('Stop DMF device UI keep-alive timer') if self.gui_heartbeat_id is not None: # Stop keep-alive polling of device UI process. gobject.source_remove(self.gui_heartbeat_id) if self.gui_process is not None and self.gui_process.poll() is None: logger.info('Terminate DMF device UI process') try: kill_process_tree(self.gui_process.pid) logger.info('Close DMF device UI process `%s`', self.gui_process.pid) except Exception: logger.info( 'Unexpected error closing DMF device UI process ' '`%s`', self.gui_process.pid, exc_info=True) else: logger.info('No active DMF device UI process') self.alive_timestamp = None def wait_for_gui_process(self, retry_count=20, retry_duration_s=1): ''' .. versionchanged:: 2.7.2 Do not execute `refresh_gui()` while waiting for response from `hub_execute()`. ''' start = datetime.now() for i in xrange(retry_count): try: hub_execute(self.name, 'ping', timeout_s=5, silent=True) except Exception: logger.debug('[wait_for_gui_process] failed (%d of %d)', i + 1, retry_count, exc_info=True) else: logger.info('[wait_for_gui_process] success (%d of %d)', i + 1, retry_count) self.alive_timestamp = datetime.now() return for j in xrange(10): time.sleep(retry_duration_s / 10.) refresh_gui() raise IOError('Timed out after %ss waiting for GUI process to connect ' 'to hub.' % si_format( (datetime.now() - start).total_seconds())) def get_schedule_requests(self, function_name): """ Returns a list of scheduling requests (i.e., ScheduleRequest instances) for the function specified by function_name. .. versionchanged:: 2.3.3 Do not submit ``on_app_exit`` schedule request. This is no longer necessary since ``hub_execute`` listening socket is no longer closed by ``microdrop.device_info_plugin`` during ``on_app_exit`` callback. .. versionadded:: 2.9 Enable _after_ command plugin and zmq hub plugin. """ if function_name == 'on_plugin_enable': return [ ScheduleRequest(p, self.name) for p in ('microdrop.zmq_hub_plugin', 'microdrop.command_plugin', 'droplet_planning_plugin') ] return [] def on_app_exit(self): logger.info('Get current video settings from DMF device UI plugin.') json_settings = self.get_ui_json_settings() self.save_ui_settings(json_settings) self._gui_enabled = False self.cleanup() # ######################################################################### # # DMF device UI 0MQ plugin settings def get_ui_json_settings(self): ''' Get current video settings from DMF device UI plugin. Returns ------- (dict) : DMF device UI plugin settings in JSON-compatible format (i.e., only basic Python data types). .. versionchanged:: 2.7.2 Do not execute `refresh_gui()` while waiting for response from `hub_execute()`. ''' video_settings = {} # Try to request video configuration. try: video_config = hub_execute(self.name, 'get_video_config', timeout_s=2) except IOError: logger.warning('Timed out waiting for device window size and ' 'position request.') else: if video_config is not None: video_settings['video_config'] = video_config.to_json() else: video_settings['video_config'] = '' # Try to request allocation to save in app options. try: data = hub_execute(self.name, 'get_corners', timeout_s=2) except IOError: logger.warning('Timed out waiting for device window size and ' 'position request.') else: if data: # Get window allocation settings (i.e., width, height, x, y). # Replace `df_..._corners` with CSV string named `..._corners` # (no `df_` prefix). for k in ('df_canvas_corners', 'df_frame_corners'): if k in data: data['allocation'][k[3:]] = data.pop(k).to_csv() video_settings.update(data['allocation']) # Try to request surface alphas. try: surface_alphas = hub_execute(self.name, 'get_surface_alphas', timeout_s=2) except IOError: logger.warning('Timed out waiting for surface alphas.') else: if surface_alphas is not None: video_settings['surface_alphas'] = surface_alphas.to_json() else: video_settings['surface_alphas'] = '' return video_settings def get_ui_settings(self): ''' Get current video settings from DMF device UI plugin. Returns ------- (dict) : DMF device UI plugin settings in Python types expected by DMF device UI plugin 0MQ commands. ''' json_settings = self.get_ui_json_settings() return self.json_settings_as_python(json_settings) def json_settings_as_python(self, json_settings): ''' Convert DMF device UI plugin settings from json format to Python types. Python types are expected by DMF device UI plugin 0MQ command API. Args ---- json_settings (dict) : DMF device UI plugin settings in JSON-compatible format (i.e., only basic Python data types). Returns ------- (dict) : DMF device UI plugin settings in Python types expected by DMF device UI plugin 0MQ commands. ''' py_settings = {} corners = dict([(k, json_settings.get(k)) for k in ('canvas_corners', 'frame_corners')]) if all(corners.values()): # Convert CSV corners lists for canvas and frame to # `pandas.DataFrame` instances for k, v in corners.iteritems(): # Prepend `'df_'` to key to indicate the type as a data frame. py_settings['df_' + k] = pd.read_csv(io.BytesIO(bytes(v)), index_col=0) for k in ('video_config', 'surface_alphas'): if k in json_settings: if not json_settings[k]: py_settings[k] = pd.Series(None) else: py_settings[k] = pd.Series(json.loads(json_settings[k])) return py_settings def save_ui_settings(self, video_settings): ''' Save specified DMF device UI 0MQ plugin settings to persistent Microdrop configuration (i.e., settings to be applied when Microdrop is launched). Args ---- video_settings (dict) : DMF device UI plugin settings in JSON-compatible format returned by `get_ui_json_settings` method (i.e., only basic Python data types). ''' app_values = self.get_app_values() # Select subset of app values that are present in `video_settings`. app_video_values = dict([(k, v) for k, v in app_values.iteritems() if k in video_settings.keys()]) # If the specified video settings differ from app values, update # app values. if app_video_values != video_settings: app_values.update(video_settings) self.set_app_values(app_values) def set_ui_settings(self, ui_settings, default_corners=False): ''' Set DMF device UI settings from settings dictionary. Args ---- ui_settings (dict) : DMF device UI plugin settings in format returned by `json_settings_as_python` method. .. versionchanged:: 2.7.2 Do not execute `refresh_gui()` while waiting for response from `hub_execute()`. ''' if self.alive_timestamp is None or self.gui_process is None: # Repeat until GUI process has started. raise IOError('GUI process not ready.') if 'video_config' in ui_settings: hub_execute(self.name, 'set_video_config', video_config=ui_settings['video_config'], timeout_s=5) if 'surface_alphas' in ui_settings: hub_execute(self.name, 'set_surface_alphas', surface_alphas=ui_settings['surface_alphas'], timeout_s=5) if all((k in ui_settings) for k in ('df_canvas_corners', 'df_frame_corners')): if default_corners: hub_execute(self.name, 'set_default_corners', canvas=ui_settings['df_canvas_corners'], frame=ui_settings['df_frame_corners'], timeout_s=5) else: hub_execute(self.name, 'set_corners', df_canvas_corners=ui_settings['df_canvas_corners'], df_frame_corners=ui_settings['df_frame_corners'], timeout_s=5) # ######################################################################### # # Plugin signal handlers def on_plugin_disable(self): self._gui_enabled = False self.cleanup() def on_plugin_enable(self): super(DmfDeviceUiPlugin, self).on_plugin_enable() self.reset_gui() def on_step_run(self): ''' Handler called whenever a step is executed. Plugins that handle this signal must emit the on_step_complete signal once they have completed the step. The protocol controller will wait until all plugins have completed the current step before proceeding. .. versionchanged:: 2.2.2 Emit ``on_step_complete`` signal within thread-safe function, since signal callbacks may use GTK. ''' app = get_app() if (app.realtime_mode or app.running) and self.gui_process is not None: step_options = self.get_step_options() if not step_options['video_enabled']: command = 'disable_video' else: command = 'enable_video' hub_execute(self.name, command) # Call as thread-safe function, since signal callbacks may use GTK. gtk_threadsafe(emit_signal)('on_step_complete', [self.name, None])
class MrBoxPeripheralBoardPlugin(AppDataController, StepOptionsController, Plugin): ''' This class is automatically registered with the PluginManager. ''' implements(IPlugin) plugin_name = str(ph.path(__file__).realpath().parent.name) try: version = __version__ except NameError: version = 'v0.0.0+unknown' AppFields = Form.of(Boolean.named('Use PMT y-axis SI units') .using(default=True, optional=True)) StepFields = Form.of(# PMT Fields Boolean.named('Measure_PMT') .using(default=False, optional=True), # Only allow PMT Duration to be set if `Measure_PMT` # field is set to `True`. Integer.named('Measurement_duration_(s)') .using(default=10, optional=True, validators=[ValueAtLeast(minimum=0)], properties={'mappers': [PropertyMapper ('sensitive', attr='Measure_PMT'), PropertyMapper ('editable', attr='Measure_PMT')]})) # Only allow ADC Gain to be set if `Measure_PMT` field # is set to `True`. # TODO Convert ADC Gain to dropdown list with # valid_values = (1,2,4,8,16) # Integer.named('ADC_Gain') # .using(default=1, optional=True, # validators=[ValueAtLeast(minimum=1), # ValueAtMost(maximum=16)], # properties={'mappers': # [PropertyMapper # ('sensitive', attr='Measure_PMT'), # PropertyMapper # ('editable', # attr='Measure_PMT')]}), def __init__(self): super(MrBoxPeripheralBoardPlugin, self).__init__() self.board = None # XXX `name` attribute is required in addition to `plugin_name` # # The `name` attribute is required in addition to the `plugin_name` # attribute because MicroDrop uses it for plugin labels in, for # example, the plugin manager dialog. self.name = self.plugin_name # Flag to indicate whether user has already been warned about the board # not being connected when trying to set board state. self._user_warned = False # `dropbot.SerialProxy` instance self.dropbot_remote = None # Latch to, e.g., config menus, only once self.initialized = False self.adc_gain_calibration = None self.adc_offset_calibration = None self.off_cal_val = None def reset_board_state(self): ''' Reset MR-Box peripheral board to default state. ''' # Reset user warned state (i.e., warn user next time board settings # are applied when board is not connected). self._user_warned = False if self.board is None: return # TODO Add reset method for each component (PMT) # TODO to respective `mr-box-peripheral-board.py` C++ classes code. # Set PMT control voltage to zero. self.board.pmt_set_pot(0) # Start the ADC and Perform ADC Calibration MAX11210_begin(self.board) def apply_step_options(self, step_options): ''' Apply the specified step options. Parameters ---------- step_options : dict Dictionary containing the MR-Box peripheral board plugin options for a protocol step. ''' app = get_app() app_values = self.get_app_values() if self.board: step_log = {} services_by_name = {service_i.name: service_i for service_i in PluginGlobals .env('microdrop.managed').services} step_label = None if 'wheelerlab.step_label_plugin' in services_by_name: # Step label is set for current step step_label_plugin = (services_by_name .get('wheelerlab.step_label_plugin')) step_label = (step_label_plugin.get_step_options() or {}).get('label') # Apply board hardware options. try: # PMT/ADC # ------- if step_options.get('Measure_PMT'): # Start the ADC and Perform ADC Calibration MAX11210_begin(self.board) if step_label.lower() == 'background': ''' Set PMT control voltage via digipot.''' # Divide the control voltage by the maximum 1100 mV and # convert it to digipot steps ''' Perform certain calibration steps only for the background measurement. Read from the 24bit Registries (SCGC, SCOC) and store their values for the rest of the measurements. ''' logger.warning('Open PMT shutter and close box lid') self.adc_gain_calibration = self.board.MAX11210_getSelfCalGain() self.adc_offset_calibration = self.board.MAX11210_getSelfCalOffset() self.board.MAX11210_setSysOffsetCal(0x00) self.board.MAX11210_send_command(0b10001000) reading_i = [] for i in range(0,20): self.board.MAX11210_setRate(120) reading_i.append(self.board.MAX11210_getData()) reading_avg = (sum(reading_i)* 1.0) / (len(reading_i) * 1.0) self.off_cal_val = int(reading_avg) - 1677 else: if not self.adc_gain_calibration: logger.warning('Missing ADC Calibration Values!' 'Please perform a Background measurement') else: logger.warning('Open PMT shutter and close box lid') self.board.MAX11210_setSelfCalGain(self.adc_gain_calibration) self.board.MAX11210_setSelfCalOffset(self.adc_offset_calibration) '''if (self.board.config.pmt_sys_offset_cal != 0): self.board.MAX11210_setSysOffsetCal(self.board.config.pmt_sys_offset_cal) else: self.board.MAX11210_setSysOffsetCal(self.off_cal_val) self.board.MAX11210_setSysGainCal(self.board.config.pmt_sys_gain_cal) self.board.MAX11210_send_command(0b10001000)''' adc_calibration = self.board.get_adc_calibration().to_dict() logger.info('ADC calibration:\n%s' % adc_calibration) step_log['ADC calibration'] = adc_calibration # Launch PMT measure dialog. delta_t = dt.timedelta(seconds=1) # Set sampling reset_board_state adc_rate = self.board.config.pmt_sampling_rate # Construct a function compatible with `measure_dialog` to # read from MAX11210 ADC. data_func = (mrbox.ui.gtk.measure_dialog .adc_data_func_factory(proxy=self.board, delta_t=delta_t, adc_rate=adc_rate)) # Use constructed function to launch measurement dialog for # the duration specified by the step options. duration_s = (step_options.get('Measurement_duration_(s)') + 1) use_si_prefixes = app_values.get('Use PMT y-axis SI ' 'prefixes') data = (mrbox.ui.gtk.measure_dialog .measure_dialog(data_func, duration_s=duration_s, auto_start=True, auto_close=False, si_units=use_si_prefixes)) if data is not None: # Append measured data as JSON line to [new-line # delimited JSON][1] file for step. # # Each line of results can be loaded using # `pandas.read_json(..., orient='split')`. # # [1]: http://ndjson.org/ filename = ph.path('PMT_readings-step%04d.ndjson' % app.protocol.current_step_number) log_dir = app.experiment_log.get_log_path() log_dir.makedirs_p() data.name = filename.namebase if step_label: # Set name of data series based on step label. data.name = step_label with log_dir.joinpath(filename).open('a') as output: # Write JSON data with `split` orientation, which # preserves the name of the Pandas series. data.to_json(output, orient='split') output.write('\n') step_log['data'] = data.to_dict() self.update_excel_results() logger.warning('Close PMT Shutter') except Exception: logger.error('[%s] Error applying step options.', __name__, exc_info=True) finally: app.experiment_log.add_data(step_log, self.name) elif not self._user_warned: logger.warning('[%s] Cannot apply board settings since board is ' 'not connected.', __name__, exc_info=True) # Do not warn user again until after the next connection attempt. self._user_warned = True def update_excel_results(self, launch=False): ''' Update output Excel results file. .. versionadded:: 0.19 Parameters ---------- launch : bool, optional If ``True``, launch Excel spreadsheet after writing. ''' app = get_app() log_dir = app.experiment_log.get_log_path() # Update Excel file with latest PMT results. output_path = log_dir.joinpath('PMT_readings.xlsx') data_files = list(log_dir.files('PMT_readings-*.ndjson')) if not data_files: logger.debug('No PMT readings files found.') return logger.info(TEMPLATE_PATH) def _threadsafe_write_results(): logger.info(launch) while True: try: _write_results(TEMPLATE_PATH, output_path, data_files) if launch: try: output_path.launch() except Exception: pass break except IOError as e: logger.info("I/O error({0}): {1}".format(e.errno, e.strerror)) response = yesno('Error writing PMT summary to Excel ' 'spreadsheet output path: `%s`.\n\nTry ' 'again?' %output_path) if response == gtk.RESPONSE_NO: break # Schedule writing of results to occur in main GTK # thread in case confirmation dialog needs to be # displayed. gobject.idle_add(_threadsafe_write_results) def open_board_connection(self): ''' Establish serial connection to MR-Box peripheral board. ''' # Try to connect to peripheral board through serial connection. # XXX Try to connect multiple times. # See [issue 1][1] on the [MR-Box peripheral board firmware # project][2]. # # [1]: https://github.com/wheeler-microfluidics/mr-box-peripheral-board.py/issues/1 # [2]: https://github.com/wheeler-microfluidics/mr-box-peripheral-board.py retry_count = 2 for i in xrange(retry_count): try: self.board.close() self.board = None except Exception: pass try: self.board = mrbox.SerialProxy() host_software_version = utility.Version.fromstring( str(self.board.host_software_version)) remote_software_version = utility.Version.fromstring( str(self.board.remote_software_version)) # Offer to reflash the firmware if the major and minor versions # are not not identical. If micro versions are different, # the firmware is assumed to be compatible. See [1] # # [1]: https://github.com/wheeler-microfluidics/base-node-rpc/ # issues/8 if any([host_software_version.major != remote_software_version.major, host_software_version.minor != remote_software_version.minor]): response = yesno("The MR-box peripheral board firmware " "version (%s) does not match the driver " "version (%s). Update firmware?" % (remote_software_version, host_software_version)) if response == gtk.RESPONSE_YES: self.on_flash_firmware() # Serial connection to peripheral **successfully established**. logger.info('Serial connection to peripheral board ' '**successfully established**') logger.info('Peripheral board properties:\n%s', self.board.properties) logger.info('Reset board state to defaults.') break except (serial.SerialException, IOError): time.sleep(1) else: # Serial connection to peripheral **could not be established**. logger.warning('Serial connection to peripheral board could not ' 'be established.') def on_edit_configuration(self, widget=None, data=None): ''' Display a dialog to manually edit the configuration settings. ''' config = self.board.config form = dict_to_form(config) dialog = FormViewDialog(form, 'Edit configuration settings') valid, response = dialog.run() if valid: self.board.update_config(**response) def on_flash_firmware(self, widget=None, data=None): app = get_app() try: self.board.flash_firmware() app.main_window_controller.info("Firmware updated successfully.", "Firmware update") except Exception, why: logger.error("Problem flashing firmware. ""%s" % why)
class SyringePumpPlugin(Plugin, AppDataController, StepOptionsController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name serial_ports_ = [port for port in get_serial_ports()] if len(serial_ports_): default_port_ = serial_ports_[0] else: default_port_ = None ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' AppFields = Form.of( Enum.named('serial_port').using(default=default_port_, optional=True).valued(*serial_ports_), Float.named('steps_per_microliter').using(optional=True, default=1.0), ) ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Float.named('microliters_per_min').using( optional=True, default=60, #validators= #[ValueAtLeast(minimum=0), # ValueAtMost(maximum=100000)] ), Float.named('microliters').using( optional=True, default=10, #validators= #[ValueAtLeast(minimum=0), # ValueAtMost(maximum=100000)] ), ) def __init__(self): self.name = self.plugin_name self.proxy = None self.initialized = False # Latch to, e.g., config menus, only once def connect(self): """ Try to connect to the syring pump at the default serial port selected in the Microdrop application options. If unsuccessful, try to connect to the proxy on any available serial port, one-by-one. """ from stepper_motor_controller import SerialProxy if len(SyringePumpPlugin.serial_ports_): app_values = self.get_app_values() # try to connect to the last successful port try: self.proxy = SerialProxy(port=str(app_values['serial_port'])) except: logger.warning( 'Could not connect to the syringe pump on port %s. ' 'Checking other ports...', app_values['serial_port'], exc_info=True) self.proxy = SerialProxy() app_values['serial_port'] = self.proxy.port self.set_app_values(app_values) else: raise Exception("No serial ports available.") def check_device_name_and_version(self): """ Check to see if: a) The connected device is a what we are expecting b) The device firmware matches the host driver API version In the case where the device firmware version does not match, display a dialog offering to flash the device with the firmware version that matches the host driver API version. """ try: self.connect() properties = self.proxy.properties package_name = properties['package_name'] display_name = properties['display_name'] if package_name != self.proxy.host_package_name: raise Exception("Device is not a %s" % properties['display_name']) host_software_version = self.proxy.host_software_version remote_software_version = self.proxy.remote_software_version # Reflash the firmware if it is not the right version. if host_software_version != remote_software_version: response = yesno("The %s firmware version (%s) " "does not match the driver version (%s). " "Update firmware?" % (display_name, remote_software_version, host_software_version)) if response == gtk.RESPONSE_YES: self.on_flash_firmware() except Exception, why: logger.warning("%s" % why)
class DropBotDxPlugin(Plugin, StepOptionsController, AppDataController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) implements(IWaveformGenerator) serial_ports_ = [port for port in get_serial_ports()] if len(serial_ports_): default_port_ = serial_ports_[0] else: default_port_ = None AppFields = Form.of( Enum.named('serial_port').using(default=default_port_, optional=True).valued(*serial_ports_), Float.named('default_duration').using(default=1000, optional=True), Float.named('default_voltage').using(default=80, optional=True), Float.named('default_frequency').using(default=10e3, optional=True), ) version = get_plugin_info(path(__file__).parent).version @property def StepFields(self): """ Expose StepFields as a property to avoid breaking code that accesses the StepFields member (vs through the get_step_form_class method). """ return self.get_step_form_class() def __init__(self): self.control_board = None self.name = get_plugin_info(path(__file__).parent).plugin_name self.connection_status = "Not connected" self.current_frequency = None self.timeout_id = None self.channel_states = pd.Series() self.plugin = None self.plugin_timeout_id = None def get_step_form_class(self): """ Override to set default values based on their corresponding app options. """ app = get_app() app_values = self.get_app_values() return Form.of( Integer.named('duration').using( default=app_values['default_duration'], optional=True, validators=[ ValueAtLeast(minimum=0), ]), Float.named('voltage').using( default=app_values['default_voltage'], optional=True, validators=[ValueAtLeast(minimum=0), max_voltage]), Float.named('frequency').using( default=app_values['default_frequency'], optional=True, validators=[ValueAtLeast(minimum=0), check_frequency]), ) def update_channel_states(self, channel_states): # Update locally cached channel states with new modified states. try: self.channel_states = channel_states.combine_first( self.channel_states) except ValueError: logging.info('channel_states: %s', channel_states) logging.info('self.channel_states: %s', self.channel_states) logging.info('', exc_info=True) else: app = get_app() connected = self.control_board != None if connected and (app.realtime_mode or app.running): self.on_step_run() def cleanup_plugin(self): if self.plugin_timeout_id is not None: gobject.source_remove(self.plugin_timeout_id) if self.plugin is not None: self.plugin = None def on_plugin_enable(self): super(DropBotDxPlugin, self).on_plugin_enable() self.cleanup_plugin() # Initialize 0MQ hub plugin and subscribe to hub messages. self.plugin = DmfZmqPlugin(self, self.name, get_hub_uri(), subscribe_options={zmq.SUBSCRIBE: ''}) # Initialize sockets. self.plugin.reset() # Periodically process outstanding message received on plugin sockets. self.plugin_timeout_id = gtk.timeout_add(10, self.plugin.check_sockets) self.check_device_name_and_version() if get_app().protocol: self.on_step_run() self._update_protocol_grid() def on_plugin_disable(self): self.cleanup_plugin() if get_app().protocol: self.on_step_run() self._update_protocol_grid() def on_app_exit(self): """ Handler called just before the Microdrop application exits. """ self.cleanup_plugin() try: self.control_board.hv_output_enabled = False except: # ignore any exceptions (e.g., if the board is not connected) pass def on_protocol_swapped(self, old_protocol, protocol): self._update_protocol_grid() def _update_protocol_grid(self): pgc = get_service_instance(ProtocolGridController, env='microdrop') if pgc.enabled_fields: pgc.update_grid() def on_app_options_changed(self, plugin_name): app = get_app() if plugin_name == self.name: app_values = self.get_app_values() reconnect = False if self.control_board: for k, v in app_values.items(): if k == 'serial_port' and self.control_board.port != v: reconnect = True if reconnect: self.connect() self._update_protocol_grid() elif plugin_name == app.name: # Turn off all electrodes if we're not in realtime mode and not # running a protocol. if (self.control_board and not app.realtime_mode and not app.running): logger.info('Turning off all electrodes.') self.control_board.hv_output_enabled = False def connect(self): """ Try to connect to the control board at the default serial port selected in the Microdrop application options. If unsuccessful, try to connect to the control board on any available serial port, one-by-one. """ self.current_frequency = None if len(DropBotDxPlugin.serial_ports_): app_values = self.get_app_values() # try to connect to the last successful port try: self.control_board = SerialProxy( port=str(app_values['serial_port'])) except: logger.warning( 'Could not connect to control board on port %s.' ' Checking other ports...', app_values['serial_port'], exc_info=True) self.control_board = SerialProxy() self.control_board.initialize_switching_boards() app_values['serial_port'] = self.control_board.port self.set_app_values(app_values) else: raise Exception("No serial ports available.") def check_device_name_and_version(self): """ Check to see if: a) The connected device is a OpenDrop b) The device firmware matches the host driver API version In the case where the device firmware version does not match, display a dialog offering to flash the device with the firmware version that matches the host driver API version. """ try: self.connect() name = self.control_board.properties['package_name'] if name != self.control_board.host_package_name: raise Exception("Device is not a DropBot DX") host_software_version = self.control_board.host_software_version remote_software_version = self.control_board.remote_software_version # Reflash the firmware if it is not the right version. if host_software_version != remote_software_version: response = yesno( "The DropBot DX firmware version (%s) " "does not match the driver version (%s). " "Update firmware?" % (remote_software_version, host_software_version)) if response == gtk.RESPONSE_YES: self.on_flash_firmware() except Exception, why: logger.warning("%s" % why) self.update_connection_status()
class ZeroMQServicePlugin(Plugin, AppDataController, StepOptionsController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent.parent).version plugins_name = get_plugin_info(path(__file__).parent.parent).plugin_name ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' AppFields = Form.of( String.named('service_address').using(default='', optional=True), ) ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Boolean.named('service_enabled').using(default=False, optional=True), Float.named('timeout_sec').using(default=5., optional=True), ) def __init__(self): self.name = self.plugins_name self.context = zmq.Context.instance() self.socks = OrderedDict() self.timeout_id = None self._start_time = None def on_plugin_enable(self): # We need to call AppDataController's on_plugin_enable() to update the # application options data. AppDataController.on_plugin_enable(self) self.context = zmq.Context() self.reset_socks() if get_app().protocol: pgc = get_service_instance(ProtocolGridController, env='microdrop') pgc.update_grid() def close_socks(self): # Close any currently open sockets. for name, sock in self.socks.iteritems(): sock.close() self.socks = OrderedDict() def reset_socks(self): self.close_socks() app_values = self.get_app_values() if self.timeout_id is not None: gtk.timeout_remove(self.timeout_id) self.timeout_id = None if app_values['service_address']: # Service address is available self.socks['req'] = zmq.Socket(self.context, zmq.REQ) self.socks['req'].connect(app_values['service_address']) def on_app_options_changed(self, plugin_name): if plugin_name == self.name: self.reset_socks() def on_plugin_disable(self): self.close_socks() if get_app().protocol: pgc = get_service_instance(ProtocolGridController, env='microdrop') pgc.update_grid() def _on_check_service_response(self, timeout_sec): if not self.socks['req'].poll(timeout=11): # No response is ready yet. if timeout_sec < (datetime.now() - self._start_time).total_seconds(): # Timed out waiting for response. self.reset_socks() self.step_complete(return_value='Fail') self.timeout_id = None return False return True else: # Response is ready. response = self.socks['req'].recv() logger.info('[ZeroMQServicePlugin] Service response: %s', response) if response == 'completed': logger.info('[ZeroMQServicePlugin] Service completed task ' 'successfully.') self.step_complete() else: logger.error('[ZeroMQServicePlugin] Unexpected response: %s' % response) self.step_complete(return_value='Fail') self.timeout_id = None return False def step_complete(self, return_value=None): app = get_app() if app.running or app.realtime_mode: emit_signal('on_step_complete', [self.name, return_value]) def on_step_run(self): options = self.get_step_options() self.reset_socks() if options['service_enabled'] and self.socks['req'] is None: # Service is supposed to be called for this step, but the socket is # not ready. self.step_complete(return_value='Fail') elif options['service_enabled'] and self.socks['req'] is not None: logger.info('[ZeroMQServicePlugin] Send signal to service to ' 'start.') # Request start of service. self.socks['req'].send('start') if not self.socks['req'].poll(timeout=4000): self.reset_socks() logger.error('[ZeroMQServicePlugin] Timed-out waiting for ' 'a response.') else: # Response is ready. response = self.socks['req'].recv() if response == 'started': logger.info('[ZeroMQServicePlugin] Service started ' 'successfully.') self.socks['req'].send('notify_completion') self._start_time = datetime.now() self.timeout_id = gtk.timeout_add( 100, self._on_check_service_response, options['timeout_sec']) else: self.step_complete() def enable_service(self): pass
class App(SingletonPlugin, AppDataController): implements(IPlugin) ''' INFO: <Plugin App 'microdrop.app'> INFO: <Plugin ConfigController 'microdrop.gui.config_controller'> INFO: <Plugin DmfDeviceController 'microdrop.gui.dmf_device_controller'> INFO: <Plugin ExperimentLogController 'microdrop.gui.experiment_log_controller'> INFO: <Plugin MainWindowController 'microdrop.gui.main_window_controller'> INFO: <Plugin ProtocolController 'microdrop.gui.protocol_controller'> INFO: <Plugin ProtocolGridController 'microdrop.gui.protocol_grid_controller'> ''' core_plugins = [ 'microdrop.app', 'microdrop.gui.config_controller', 'microdrop.gui.dmf_device_controller', 'microdrop.gui.experiment_log_controller', 'microdrop.gui.main_window_controller', 'microdrop.gui.protocol_controller', 'microdrop.gui.protocol_grid_controller', 'microdrop.zmq_hub_plugin', 'microdrop.electrode_controller_plugin', 'microdrop.device_info_plugin' ] AppFields = Form.of( Integer.named('x').using(default=None, optional=True, properties={'show_in_gui': False}), Integer.named('y').using(default=None, optional=True, properties={'show_in_gui': False}), Integer.named('width').using(default=400, optional=True, properties={'show_in_gui': False}), Integer.named('height').using(default=500, optional=True, properties={'show_in_gui': False}), String.named('server_url').using( default='http://microfluidics.utoronto.ca/update', optional=True, properties=dict(show_in_gui=False)), Boolean.named('realtime_mode').using( default=False, optional=True, properties=dict(show_in_gui=False)), Filepath.named('log_file').using( default='', optional=True, properties={'action': gtk.FILE_CHOOSER_ACTION_SAVE}), Boolean.named('log_enabled').using(default=False, optional=True), Enum.named('log_level').using(default='info', optional=True).valued( 'debug', 'info', 'warning', 'error', 'critical')) def __init__(self): ''' .. versionchanged:: 2.11.2 Add :attr:`gtk_thread` attribute, holding a reference to the thread that the GTK main loop is executing in. .. versionchanged:: 2.17 Remove :attr:`version` attribute. Use :attr:`microdrop.__version__` instead. ''' args = parse_args() print 'Arguments: %s' % args self.name = "microdrop.app" #: .. versionadded:: 2.11.2 self.gtk_thread = None self.realtime_mode = False self.running = False self.builder = gtk.Builder() self.signals = {} self.plugin_data = {} # these members are initialized by plugins self.experiment_log_controller = None self.config_controller = None self.dmf_device_controller = None self.protocol_controller = None self.main_window_controller = None # Enable custom logging handler logging.getLogger().addHandler(CustomHandler()) self.log_file_handler = None # config model try: self.config = Config(args.config) except IOError: logging.error( 'Could not read configuration file, `%s`. Make sure' ' it exists and is readable.', args.config) raise SystemExit(-1) # set the log level if self.name in self.config.data and ('log_level' in self.config.data[self.name]): self._set_log_level(self.config.data[self.name]['log_level']) _L().info('MicroDrop version: %s', __version__) _L().info('Running in working directory: %s', os.getcwd()) # dmf device self.dmf_device = None # protocol self.protocol = None def get_data(self, plugin_name): data = self.plugin_data.get(plugin_name) if data: return data else: return {} def set_data(self, plugin_name, data): ''' .. versionchanged:: 2.20 Log data and plugin name to debug level. ''' logger = _L() # use logger with method context if logger.getEffectiveLevel() >= logging.DEBUG: caller = caller_name(skip=2) logger.debug('%s -> plugin_data:', caller) map(logger.debug, pprint.pformat(data).splitlines()) self.plugin_data[plugin_name] = data def on_app_options_changed(self, plugin_name): if plugin_name == self.name: data = self.get_data(self.name) if 'realtime_mode' in data: if self.realtime_mode != data['realtime_mode']: self.realtime_mode = data['realtime_mode'] if self.protocol_controller: self.protocol_controller.run_step() if 'log_file' in data and 'log_enabled' in data: self.apply_log_file_config(data['log_file'], data['log_enabled']) if 'log_level' in data: self._set_log_level(data['log_level']) if 'width' in data and 'height' in data: self.main_window_controller.view.resize( data['width'], data['height']) # allow window to resize before other signals are processed while gtk.events_pending(): gtk.main_iteration() if data.get('x') is not None and data.get('y') is not None: self.main_window_controller.view.move(data['x'], data['y']) # allow window to resize before other signals are processed while gtk.events_pending(): gtk.main_iteration() def apply_log_file_config(self, log_file, enabled): if enabled and not log_file: _L().error('Log file can only be enabled if a path is selected.') return False self.update_log_file() return True @property def plugins(self): return set(self.plugin_data.keys()) def plugin_name_lookup(self, name, re_pattern=False): if not re_pattern: return name for plugin_name in self.plugins: if re.search(name, plugin_name): return plugin_name return None def update_plugins(self): ''' .. versionchanged:: 2.16.2 Method was deprecated. ''' raise DeprecationWarning('The `update_plugins` method was deprecated ' 'in version 2.16.2.') def gtk_thread_active(self): ''' Returns ------- bool ``True`` if the currently active thread is the GTK thread. .. versionadded:: 2.11.2 ''' if self.gtk_thread is not None and (threading.current_thread().ident == self.gtk_thread.ident): return True else: return False def run(self): ''' .. versionchanged:: 2.11.2 Set :attr:`gtk_thread` attribute, holding a reference to the thread that the GTK main loop is executing in. .. versionchanged:: 2.16.2 Do not attempt to update plugins. ''' logger = _L() # use logger with method context self.gtk_thread = threading.current_thread() # set realtime mode to false on startup if self.name in self.config.data and \ 'realtime_mode' in self.config.data[self.name]: self.config.data[self.name]['realtime_mode'] = False plugin_manager.emit_signal('on_plugin_enable') log_file = self.get_app_values()['log_file'] if not log_file: self.set_app_values({ 'log_file': ph.path(self.config['data_dir']).joinpath('microdrop.log') }) pwd = ph.path(os.getcwd()).realpath() if '' in sys.path and pwd.joinpath('plugins').isdir(): logger.info( '[warning] Removing working directory `%s` from Python' ' import path.', pwd) sys.path.remove('') # Import enabled plugins from Conda environment. conda_plugins_dir = mpm.api.MICRODROP_CONDA_ETC.joinpath( 'plugins', 'enabled') if conda_plugins_dir.isdir(): plugin_manager.load_plugins(conda_plugins_dir, import_from_parent=False) self.update_log_file() logger.info('User data directory: %s', self.config['data_dir']) logger.info('Plugins directory: %s', conda_plugins_dir) logger.info('Devices directory: %s', self.get_device_directory()) FormViewDialog.default_parent = self.main_window_controller.view self.builder.connect_signals(self.signals) observers = {} plugins_to_disable_by_default = [] # Enable plugins according to schedule requests for package_name in self.config['plugins']['enabled']: try: service = plugin_manager. \ get_service_instance_by_package_name(package_name) observers[service.name] = service except KeyError: logger.warning('No plugin found registered with name `%s`', package_name) # Mark plugin to be removed from "enabled" list to prevent # trying to enable it on future launches. plugins_to_disable_by_default.append(package_name) except Exception, exception: logger.error(exception, exc_info=True) # Remove marked plugins from "enabled" list to prevent trying to enable # it on future launches. for package_name_i in plugins_to_disable_by_default: self.config['plugins']['enabled'].remove(package_name_i) schedule = plugin_manager.get_schedule(observers, "on_plugin_enable") # Load optional plugins marked as enabled in config for p in schedule: try: plugin_manager.enable(p) except KeyError: logger.warning('Requested plugin (%s) is not available.\n\n' 'Please check that it exists in the plugins ' 'directory:\n\n %s' % (p, self.config['plugins']['directory']), exc_info=True) plugin_manager.log_summary() self.experiment_log = None # save the protocol name from the config file because it is # automatically overwritten when we load a new device protocol_name = self.config['protocol']['name'] # if there is no device specified in the config file, try choosing one # from the device directory by default device_directory = ph.path(self.get_device_directory()) if not self.config['dmf_device']['name']: try: self.config['dmf_device']['name'] = \ device_directory.dirs()[0].name except Exception: pass # load the device from the config file if self.config['dmf_device']['name']: if device_directory: device_path = os.path.join(device_directory, self.config['dmf_device']['name'], DEVICE_FILENAME) self.dmf_device_controller.load_device(device_path) # if we successfully loaded a device if self.dmf_device: # reapply the protocol name to the config file self.config['protocol']['name'] = protocol_name # load the protocol if self.config['protocol']['name']: directory = self.get_device_directory() if directory: filename = os.path.join(directory, self.config['dmf_device']['name'], "protocols", self.config['protocol']['name']) self.protocol_controller.load_protocol(filename) data = self.get_data("microdrop.app") x = data.get('x', None) y = data.get('y', None) width = data.get('width', 400) height = data.get('height', 600) self.main_window_controller.view.resize(width, height) if x is not None and y is not None: self.main_window_controller.view.move(x, y) plugin_manager.emit_signal('on_gui_ready') self.main_window_controller.main()
def __init__(self, forms, enabled_attrs, show_ids=True, **kwargs): self.first_selected = True self._forms = forms.copy() row_id_properties = dict(editable=False) if not show_ids: row_id_properties['show_in_gui'] = False self._forms['__DefaultFields'] = \ Form.of(Integer.named('id').using(default=0, properties=row_id_properties)) self.uuid_mapping = dict([(name, uuid4().get_hex()[:10]) for name in self._forms]) self.uuid_reverse_mapping = dict([ (v, k) for k, v in self.uuid_mapping.items() ]) self._columns = [] self._full_field_to_field_def = {} if not enabled_attrs: def enabled(form_name, field): return True else: def enabled(form_name, field): return field.name in enabled_attrs.get(form_name, {}) # Make __DefaultFields.id the first column form_names = ['__DefaultFields'] + sorted(forms.keys()) for form_name in form_names: form = self._forms[form_name] for field_name in form.field_schema: if all([ not form_name == '__DefaultFields', not enabled(form_name, field_name) ]): continue default_title = re.sub(r'_', ' ', field_name.name).capitalize() # Use custom column heading/title, if available. title = field_name.properties.get('title', default_title) prefix = self.field_set_prefix % self.uuid_mapping[form_name] name = '%s%s' % (prefix, field_name.name) val_type = get_type_from_schema(field_name) d = dict(attr=name, type=val_type, title=title, resizable=True, editable=True, sorted=False) if field_name.properties.get('mappers', None): d['mappers'] = deepcopy(field_name.properties['mappers']) for m in d['mappers']: m.attr = '%s%s' % (prefix, m.attr) if 'editable' in field_name.properties: d['editable'] = field_name.properties['editable'] if 'show_in_gui' in field_name.properties: d['visible'] = field_name.properties['show_in_gui'] if val_type == bool: # Use checkbox for boolean cells d['use_checkbox'] = True elif val_type == int: # Use spinner for integer cells d['use_spin'] = True d['step'] = field_name.properties.get('step', 1) elif val_type == float: # Use spinner for integer cells d['use_spin'] = True d['digits'] = field_name.properties.get('digits', 2) d['step'] = field_name.properties.get('step', 0.1) self._columns.append(Column(**d)) self._full_field_to_field_def[name] = field_name super(CombinedFields, self).__init__(self._columns, **kwargs) s = self.get_selection() # Enable multiple row selection s.set_mode(Gtk.SelectionMode.MULTIPLE) self.connect('item-changed', self._on_item_changed) self.connect('item-right-clicked', self._on_right_clicked) self.enabled_fields_by_form_name = enabled_attrs self.connect('item-added', lambda x, y: self.reset_row_ids()) self.connect('item-inserted', lambda x, y, z: self.reset_row_ids()) self.connect('item-removed', lambda x, y: self.reset_row_ids())
def __init__(self, forms, enabled_attrs, show_ids=True, **kwargs): self.first_selected = True self._forms = forms.copy() row_id_properties = dict(editable=False) if not show_ids: row_id_properties['show_in_gui'] = False self._forms['__DefaultFields'] = Form.of(Integer.named('id')\ .using(default=0, properties=row_id_properties)) self.uuid_mapping = dict([(name, uuid4().get_hex()[:10]) for name in self._forms]) self.uuid_reverse_mapping = dict([(v, k) for k, v in self.uuid_mapping.items()]) self._columns = [] self._full_field_to_field_def = {} if not enabled_attrs: enabled = lambda form_name, field: True else: enabled = lambda form_name, field:\ field.name in enabled_attrs.get(form_name, {}) # Make __DefaultFields.id the first column form_names = ['__DefaultFields'] + sorted(forms.keys()) for form_name in form_names: form = self._forms[form_name] for field_name in form.field_schema: if not form_name == '__DefaultFields' and not enabled(form_name, field_name): continue default_title = re.sub(r'_', ' ', field_name.name).capitalize() # Use custom column heading/title, if available. title = field_name.properties.get('title', default_title) prefix = self.field_set_prefix % self.uuid_mapping[form_name] name = '%s%s' % (prefix, field_name.name) val_type = get_type_from_schema(field_name) d = dict(attr=name, type=val_type, title=title, resizable=True, editable=True, sorted=False) if field_name.properties.get('mappers', None): d['mappers'] = deepcopy(field_name.properties['mappers']) for m in d['mappers']: m.attr = '%s%s' % (prefix, m.attr) if 'editable' in field_name.properties: d['editable'] = field_name.properties['editable'] if 'show_in_gui' in field_name.properties: d['visible'] = field_name.properties['show_in_gui'] if val_type == bool: # Use checkbox for boolean cells d['use_checkbox'] = True elif val_type == int: # Use spinner for integer cells d['use_spin'] = True d['step'] = field_name.properties.get('step', 1) elif val_type == float: # Use spinner for integer cells d['use_spin'] = True d['digits'] = field_name.properties.get('digits', 2) d['step'] = field_name.properties.get('step', 0.1) self._columns.append(Column(**d)) self._full_field_to_field_def[name] = field_name super(CombinedFields, self).__init__(self._columns, **kwargs) s = self.get_selection() # Enable multiple row selection s.set_mode(gtk.SELECTION_MULTIPLE) self.connect('item-changed', self._on_item_changed) self.connect('item-right-clicked', self._on_right_clicked) self.enabled_fields_by_form_name = enabled_attrs self.connect('item-added', lambda x, y: self.reset_row_ids()) self.connect('item-inserted', lambda x, y, z: self.reset_row_ids()) self.connect('item-removed', lambda x, y: self.reset_row_ids())
def on_edit_calibration(self, widget=None, data=None): if not self.control_board.connected(): logging.error("A control board must be connected in order to " "edit calibration settings.") return hardware_version = utility.Version.fromstring( self.control_board.hardware_version()) schema_entries = [] settings = {} settings['amplifier_gain'] = self.control_board.amplifier_gain() schema_entries.append( Float.named('amplifier_gain').using( default=settings['amplifier_gain'], optional=True, validators=[ ValueAtLeast(minimum=0.01), ]), ) settings['auto_adjust_amplifier_gain'] = self.control_board \ .auto_adjust_amplifier_gain() schema_entries.append( Boolean.named('auto_adjust_amplifier_gain').using( default=settings['auto_adjust_amplifier_gain'], optional=True), ) settings['voltage_tolerance'] = \ self.control_board.voltage_tolerance() schema_entries.append( Float.named('voltage_tolerance').using( default=settings['voltage_tolerance'], optional=True, validators=[ ValueAtLeast(minimum=0), ]), ) if hardware_version.major == 1: settings['WAVEOUT_GAIN_1'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_WAVEOUT_GAIN_1_ADDRESS) schema_entries.append( Integer.named('WAVEOUT_GAIN_1').using( default=settings['WAVEOUT_GAIN_1'], optional=True, validators=[ ValueAtLeast(minimum=0), ValueAtMost(maximum=255), ]), ) settings['VGND'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_VGND_ADDRESS) schema_entries.append( Integer.named('VGND').using(default=settings['VGND'], optional=True, validators=[ ValueAtLeast(minimum=0), ValueAtMost(maximum=255), ]), ) else: settings['SWITCHING_BOARD_I2C_ADDRESS'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_SWITCHING_BOARD_I2C_ADDRESS) schema_entries.append( Integer.named('SWITCHING_BOARD_I2C_ADDRESS').using( default=settings['SWITCHING_BOARD_I2C_ADDRESS'], optional=True, validators=[ ValueAtLeast(minimum=0), ValueAtMost(maximum=255), ]), ) settings['SIGNAL_GENERATOR_BOARD_I2C_ADDRESS'] = self.control_board \ .eeprom_read(self.control_board.EEPROM_SIGNAL_GENERATOR_BOARD_I2C_ADDRESS) schema_entries.append( Integer.named('SIGNAL_GENERATOR_BOARD_I2C_ADDRESS').using( default=settings['SIGNAL_GENERATOR_BOARD_I2C_ADDRESS'], optional=True, validators=[ ValueAtLeast(minimum=0), ValueAtMost(maximum=255), ]), ) for i in range(len(self.control_board.calibration.R_hv)): settings['R_hv_%d' % i] = self.control_board.calibration.R_hv[i] schema_entries.append( Float.named('R_hv_%d' % i).using(default=settings['R_hv_%d' % i], optional=True, validators=[ ValueAtLeast(minimum=0), ])) settings['C_hv_%d' % i] =\ self.control_board.calibration.C_hv[i]*1e12 schema_entries.append( Float.named('C_hv_%d' % i).using(default=settings['C_hv_%d' % i], optional=True, validators=[ ValueAtLeast(minimum=0), ])) for i in range(len(self.control_board.calibration.R_fb)): settings['R_fb_%d' % i] = self.control_board.calibration.R_fb[i] schema_entries.append( Float.named('R_fb_%d' % i).using(default=settings['R_fb_%d' % i], optional=True, validators=[ ValueAtLeast(minimum=0), ])) settings['C_fb_%d' % i] = \ self.control_board.calibration.C_fb[i]*1e12 schema_entries.append( Float.named('C_fb_%d' % i).using(default=settings['C_fb_%d' % i], optional=True, validators=[ ValueAtLeast(minimum=0), ])) form = Form.of(*schema_entries) dialog = FormViewDialog('Edit calibration settings') valid, response = dialog.run(form) if valid: for k, v in response.items(): if settings[k] != v: m = re.match('(R|C)_(hv|fb)_(\d)', k) if k == 'amplifier_gain': self.control_board.set_amplifier_gain(v) elif k == 'auto_adjust_amplifier_gain': self.control_board.set_auto_adjust_amplifier_gain(v) elif k == 'WAVEOUT_GAIN_1': self.control_board.eeprom_write( self.control_board.EEPROM_WAVEOUT_GAIN_1_ADDRESS, v) elif k == 'VGND': self.control_board.eeprom_write( self.control_board.EEPROM_VGND_ADDRESS, v) elif k == 'SWITCHING_BOARD_I2C_ADDRESS': self.control_board.eeprom_write( self.control_board. EEPROM_SWITCHING_BOARD_I2C_ADDRESS, v) elif k == 'SIGNAL_GENERATOR_BOARD_I2C_ADDRESS': self.control_board.eeprom_write( self.control_board. EEPROM_SIGNAL_GENERATOR_BOARD_I2C_ADDRESS, v) elif k == 'voltage_tolerance': self.control_board.set_voltage_tolerance(v) elif m: series_resistor = int(m.group(3)) if m.group(2) == 'hv': channel = 0 else: channel = 1 self.control_board.set_series_resistor_index( channel, series_resistor) if m.group(1) == 'R': self.control_board.set_series_resistance( channel, v) else: if v is None: v = 0 self.control_board.set_series_capacitance( channel, v / 1e12) # reconnect to update settings self.connect() if get_app().protocol: self.on_step_run()
class DmfControlBoardPlugin(Plugin, StepOptionsController, AppDataController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) implements(IWaveformGenerator) serial_ports_ = [ port for port in serial_device.SerialDevice().get_serial_ports() ] if len(serial_ports_): default_port_ = serial_ports_[0] else: default_port_ = None AppFields = Form.of( Integer.named('sampling_time_ms').using(default=10, optional=True, validators=[ValueAtLeast(minimum=0), ],), Integer.named('delay_between_samples_ms').using(default=0, optional=True, validators=[ValueAtLeast(minimum=0), ],), Enum.named('serial_port').using(default=default_port_, optional=True)\ .valued(*serial_ports_), ) StepFields = Form.of( Integer.named('duration').using(default=100, optional=True, validators=[ ValueAtLeast(minimum=0), ]), Float.named('voltage').using(default=100, optional=True, validators=[ ValueAtLeast(minimum=0), ]), Float.named('frequency').using(default=1e3, optional=True, validators=[ ValueAtLeast(minimum=0), ]), Boolean.named('feedback_enabled').using(default=True, optional=True), ) _feedback_fields = set(['feedback_enabled']) version = get_plugin_info(path(__file__).parent.parent).version def __init__(self): self.control_board = DmfControlBoard() self.name = get_plugin_info(path(__file__).parent.parent).plugin_name self.url = self.control_board.host_url() self.steps = [] # list of steps in the protocol self.feedback_options_controller = None self.feedback_results_controller = None self.feedback_calibration_controller = None self.initialized = False self.connection_status = "Not connected" self.n_voltage_adjustments = None self.amplifier_gain_initialized = False self.current_frequency = None self.edit_log_calibration_menu_item = gtk.MenuItem("Edit calibration") self.save_log_calibration_menu_item = \ gtk.MenuItem("Save calibration to file") self.load_log_calibration_menu_item = \ gtk.MenuItem("Load calibration from file") self.timeout_id = None def on_plugin_enable(self): if not self.initialized: self.feedback_options_controller = FeedbackOptionsController(self) self.feedback_results_controller = FeedbackResultsController(self) self.feedback_calibration_controller = \ FeedbackCalibrationController(self) self.edit_log_calibration_menu_item.connect( "activate", self.feedback_calibration_controller.on_edit_log_calibration) self.save_log_calibration_menu_item.connect( "activate", self.feedback_calibration_controller.on_save_log_calibration) self.load_log_calibration_menu_item.connect( "activate", self.feedback_calibration_controller.on_load_log_calibration) experiment_log_controller = get_service_instance_by_name( "microdrop.gui.experiment_log_controller", "microdrop") if hasattr(experiment_log_controller, 'popup'): experiment_log_controller.popup.add_item( self.edit_log_calibration_menu_item) experiment_log_controller.popup.add_item( self.save_log_calibration_menu_item) experiment_log_controller.popup.add_item( self.load_log_calibration_menu_item) app = get_app() self.control_board_menu_item = gtk.MenuItem("DMF control board") app.main_window_controller.menu_tools.append( self.control_board_menu_item) self.control_board_menu = gtk.Menu() self.control_board_menu.show() self.control_board_menu_item.set_submenu(self.control_board_menu) self.feedback_options_controller.on_plugin_enable() menu_item = gtk.MenuItem("Perform calibration") menu_item.connect( "activate", self.feedback_calibration_controller.on_perform_calibration) self.control_board_menu.append(menu_item) self.perform_calibration_menu_item = menu_item menu_item.show() menu_item = gtk.MenuItem("Load calibration from file") menu_item.connect("activate", self.feedback_calibration_controller. \ on_load_calibration_from_file) self.control_board_menu.append(menu_item) self.load_calibration_from_file_menu_item = menu_item menu_item.show() menu_item = gtk.MenuItem("Edit calibration settings") menu_item.connect("activate", self.on_edit_calibration) self.control_board_menu.append(menu_item) self.edit_calibration_menu_item = menu_item menu_item.show() menu_item = gtk.MenuItem("Reset calibration to default values") menu_item.connect("activate", self.on_reset_calibration_to_default_values) self.control_board_menu.append(menu_item) self.reset_calibration_to_default_values_menu_item = menu_item menu_item.show() self.initialized = True super(DmfControlBoardPlugin, self).on_plugin_enable() self.check_device_name_and_version() self.control_board_menu_item.show() self.edit_log_calibration_menu_item.show() self.feedback_results_controller.feedback_results_menu_item.show() if get_app().protocol: self.on_step_run() pgc = get_service_instance(ProtocolGridController, env='microdrop') pgc.update_grid() def on_plugin_disable(self): self.feedback_options_controller.on_plugin_disable() self.control_board_menu_item.hide() self.edit_log_calibration_menu_item.hide() self.feedback_results_controller.window.hide() self.feedback_results_controller.feedback_results_menu_item.hide() if get_app().protocol: self.on_step_run() pgc = get_service_instance(ProtocolGridController, env='microdrop') pgc.update_grid() def on_app_options_changed(self, plugin_name): if plugin_name == self.name: app_values = self.get_app_values() if self.control_board.connected() and \ self.control_board.port != app_values['serial_port']: self.connect() def connect(self): self.current_frequency = None self.amplifier_gain_initialized = False if len(DmfControlBoardPlugin.serial_ports_): app_values = self.get_app_values() # try to connect to the last successful port try: self.control_board.connect(str(app_values['serial_port'])) except Exception, why: logger.warning( 'Could not connect to control board on port %s. ' 'Checking other ports...' % app_values['serial_port']) self.control_board.connect() app_values['serial_port'] = self.control_board.port self.set_app_values(app_values) else:
""" from flatland import Boolean, Form, String, Integer, Float def is_float(v): try: return (float(str(v)), True)[1] except (ValueError, TypeError), e: return False def is_int(v): try: return (int(str(v)), True)[1] except (ValueError, TypeError), e: return False def is_bool(v): return v in (True, False) schema_entries = [] for k, v in dict.iteritems(): if is_int(v): schema_entries.append(Integer.named(k).using(default=v, optional=True)) elif is_float(v): schema_entries.append(Float.named(k).using(default=v, optional=True)) elif is_bool(v): schema_entries.append(Boolean.named(k).using(default=v, optional=True)) elif type(v) == str: schema_entries.append(String.named(k).using(default=v, optional=True)) return Form.of(*schema_entries)
class ElectrodeControllerPlugin(Plugin, AppDataController, StepOptionsController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' AppFields = Form.of( String.named('hub_uri').using(optional=True, default='tcp://localhost:31000'), ) def __init__(self): self.name = self.plugin_name self.plugin = None self.plugin_timeout_id = None def get_schedule_requests(self, function_name): """ Returns a list of scheduling requests (i.e., ScheduleRequest instances) for the function specified by function_name. """ if function_name == 'on_plugin_enable': return [ScheduleRequest('wheelerlab.zmq_hub_plugin', self.name)] else: return [] def on_plugin_enable(self): """ Handler called once the plugin instance is enabled. Note: if you inherit your plugin from AppDataController and don't implement this handler, by default, it will automatically load all app options from the config file. If you decide to overide the default handler, you should call: AppDataController.on_plugin_enable(self) to retain this functionality. """ super(ElectrodeControllerPlugin, self).on_plugin_enable() app_values = self.get_app_values() self.cleanup() self.plugin = ElectrodeControllerZmqPlugin(self, self.name, app_values['hub_uri']) # Initialize sockets. self.plugin.reset() self.plugin_timeout_id = gobject.timeout_add(10, self.plugin.check_sockets) def cleanup(self): if self.plugin_timeout_id is not None: gobject.source_remove(self.plugin_timeout_id) if self.plugin is not None: self.plugin = None def on_plugin_disable(self): """ Handler called once the plugin instance is disabled. """ self.cleanup() def on_app_exit(self): """ Handler called just before the Microdrop application exits. """ self.cleanup() def on_step_swapped(self, old_step_number, step_number): if self.plugin is not None: self.plugin.execute_async('wheelerlab.electrode_controller_plugin', 'get_channel_states')
def get_channel_sweep_parameters(voltage=100, frequency=10e3, channels=None, parent=None): ''' Show dialog to select parameters for a sweep across a selected set of channels. Args ---- voltage (int) : Default actuation voltage. frequency (int) : Default actuation frequency. channels (pandas.Series) : Default channels selection, encoded as boolean array indexed by channel number, where `True` values indicate selected channel(s). parent (gtk.Window) : If not `None`, parent window for dialog. For example, display dialog at position relative to the parent window. Returns ------- (dict) : Values collected from widgets with the following keys: `'frequency'`, `voltage'`, and (optionally) `'channels'`. ''' # Create a form view containing widgets to set the waveform attributes # (i.e., voltage and frequency). form = Form.of(Float.named('voltage') .using(default=voltage, validators=[ValueAtLeast(minimum=0)]), Float.named('frequency') .using(default=frequency, validators=[ValueAtLeast(minimum=1)])) form_view = create_form_view(form) # If default channel selection was provided, create a treeview with one row # per channel, and a checkbox in each row to mark the selection status of # the corresponding channel. if channels is not None: df_channel_select = pd.DataFrame(channels.index, columns=['channel']) df_channel_select.insert(1, 'select', channels.values) view_channels = ListSelect(df_channel_select) # Create dialog window. dialog = gtk.Dialog(title='Channel sweep parameters', buttons=(gtk.STOCK_OK, gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)) # Add waveform widgets to dialog window. frame_waveform = gtk.Frame('Waveform properties') frame_waveform.add(form_view.widget) dialog.vbox.pack_start(child=frame_waveform, expand=False, fill=False, padding=5) # Add channel selection widgets to dialog window. if channels is not None: frame_channels = gtk.Frame('Select channels to sweep') frame_channels.add(view_channels.widget) dialog.vbox.pack_start(child=frame_channels, expand=True, fill=True, padding=5) # Mark all widgets as visible. dialog.vbox.show_all() if parent is not None: dialog.window.set_transient_for(parent) response = dialog.run() dialog.destroy() if response != gtk.RESPONSE_OK: raise RuntimeError('Dialog cancelled.') # Collection waveform and channel selection values from dialog. form_values = {name: f.element.value for name, f in form_view.form.fields.items()} if channels is not None: form_values['channels'] = (df_channel_select .loc[df_channel_select['select'], 'channel'].values) return form_values
class DmfDeviceUiPlugin(AppDataController, StepOptionsController, Plugin, pmh.BaseMqttReactor): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name AppFields = Form.of( String.named('video_config').using(default='', optional=True, properties={'show_in_gui': False}), String.named('surface_alphas').using(default='', optional=True, properties={'show_in_gui': False}), String.named('canvas_corners').using(default='', optional=True, properties={'show_in_gui': False}), String.named('frame_corners').using(default='', optional=True, properties={'show_in_gui': False}), Integer.named('x').using(default=None, optional=True, properties={'show_in_gui': False}), Integer.named('y').using(default=None, optional=True, properties={'show_in_gui': False}), Integer.named('width').using(default=400, optional=True, properties={'show_in_gui': False}), Integer.named('height').using(default=500, optional=True, properties={'show_in_gui': False})) StepFields = Form.of( Boolean.named('video_enabled').using(default=True, optional=True, properties={'title': 'Video'})) def __init__(self): self.name = self.plugin_name self.gui_process = None self.gui_heartbeat_id = None self._gui_enabled = False self.alive_timestamp = None self.should_terminate = False pmh.BaseMqttReactor.__init__(self) self.start() def reset_gui(self): py_exe = sys.executable # Set allocation based on saved app values (i.e., remember window size # and position from last run). app_values = self.get_app_values() allocation_args = ['-a', json.dumps(app_values)] app = get_app() if app.config.data.get('advanced_ui', False): debug_args = ['-d'] else: debug_args = [] self.gui_process = Popen( [py_exe, '-m', 'dmf_device_ui.bin.device_view', '-n', self.name] + allocation_args + debug_args + ['fixed', get_hub_uri()], creationflags=CREATE_NEW_PROCESS_GROUP) self.gui_process.daemon = False self._gui_enabled = True def keep_alive(): if not self._gui_enabled: self.alive_timestamp = None return False elif self.gui_process.poll() == 0: # GUI process has exited. Restart. self.cleanup() self.reset_gui() return False else: self.alive_timestamp = datetime.now() # Keep checking. return True # Go back to Undo 613 for working corners self.step_video_settings = None # Get current video settings from UI. app_values = self.get_app_values() # Convert JSON settings to 0MQ plugin API Python types. ui_settings = self.json_settings_as_python(app_values) self.set_ui_settings(ui_settings, default_corners=True) self.gui_heartbeat_id = gobject.timeout_add(1000, keep_alive) def cleanup(self): if self.gui_heartbeat_id is not None: gobject.source_remove(self.gui_heartbeat_id) self.alive_timestamp = None def get_schedule_requests(self, function_name): """ Returns a list of scheduling requests (i.e., ScheduleRequest instances) for the function specified by function_name. """ if function_name == 'on_plugin_enable': return [ScheduleRequest('droplet_planning_plugin', self.name)] elif function_name == 'on_dmf_device_swapped': # XXX Schedule `on_app_exit` handling before `device_info_plugin`, # since `hub_execute` uses the `device_info_plugin` service to # submit commands to through the 0MQ plugin hub. return [ScheduleRequest('microdrop.device_info_plugin', self.name)] elif function_name == 'on_app_exit': # XXX Schedule `on_app_exit` handling before `device_info_plugin`, # since `hub_execute` uses the `device_info_plugin` service to # submit commands to through the 0MQ plugin hub. return [ScheduleRequest(self.name, 'microdrop.device_info_plugin')] return [] def on_app_exit(self): self.should_terminate = True self.mqtt_client.publish( 'microdrop/dmf-device-ui-plugin/get-video-settings', json.dumps(None)) def json_settings_as_python(self, json_settings): ''' Convert DMF device UI plugin settings from json format to Python types. Python types are expected by DMF device UI plugin 0MQ command API. Args ---- json_settings (dict) : DMF device UI plugin settings in JSON-compatible format (i.e., only basic Python data types). Returns ------- (dict) : DMF device UI plugin settings in Python types expected by DMF device UI plugin 0MQ commands. ''' py_settings = {} corners = dict([(k, json_settings.get(k)) for k in ('canvas_corners', 'frame_corners')]) if all(corners.values()): # Convert CSV corners lists for canvas and frame to # `pandas.DataFrame` instances for k, v in corners.iteritems(): # Prepend `'df_'` to key to indicate the type as a data frame. py_settings['df_' + k] = pd.read_csv(io.BytesIO(bytes(v)), index_col=0) for k in ('video_config', 'surface_alphas'): if k in json_settings: if not json_settings[k]: py_settings[k] = pd.Series(None) else: py_settings[k] = pd.Series(json.loads(json_settings[k])) return py_settings def save_ui_settings(self, video_settings): ''' Save specified DMF device UI 0MQ plugin settings to persistent Microdrop configuration (i.e., settings to be applied when Microdrop is launched). Args ---- video_settings (dict) : DMF device UI plugin settings in JSON-compatible format returned by `get_ui_json_settings` method (i.e., only basic Python data types). ''' app_values = self.get_app_values() # Select subset of app values that are present in `video_settings`. app_video_values = dict([(k, v) for k, v in app_values.iteritems() if k in video_settings.keys()]) # If the specified video settings differ from app values, update # app values. if app_video_values != video_settings: app_values.update(video_settings) self.set_app_values(app_values) def set_ui_settings(self, ui_settings, default_corners=False): ''' Set DMF device UI settings from settings dictionary. Args ---- ui_settings (dict) : DMF device UI plugin settings in format returned by `json_settings_as_python` method. ''' if 'video_config' in ui_settings: msg = {} msg['video_config'] = ui_settings['video_config'].to_json() self.mqtt_client.publish( 'microdrop/dmf-device-ui-plugin/set-video-config', payload=json.dumps(msg), retain=True) if 'surface_alphas' in ui_settings: # TODO: Make Clear retained messages after exit msg = {} msg['surface_alphas'] = ui_settings['surface_alphas'].to_json() self.mqtt_client.publish( 'microdrop/dmf-device-ui-plugin/set-surface-alphas', payload=json.dumps(msg), retain=True) if all((k in ui_settings) for k in ('df_canvas_corners', 'df_frame_corners')): # TODO: Test With Camera msg = {} msg['df_canvas_corners'] = ui_settings[ 'df_canvas_corners'].to_json() msg['df_frame_corners'] = ui_settings['df_frame_corners'].to_json() if default_corners: self.mqtt_client.publish( 'microdrop/dmf-device-ui-plugin/' 'set-default-corners', payload=json.dumps(msg), retain=True) else: self.mqtt_client.publish( 'microdrop/dmf-device-ui-plugin/' 'set-corners', payload=json.dumps(msg), retain=True) # ######################################################################### # # Plugin signal handlers def on_connect(self, client, userdata, flags, rc): self.mqtt_client.subscribe( 'microdrop/dmf-device-ui/get-video-settings') self.mqtt_client.subscribe('microdrop/dmf-device-ui/update-protocol') def on_message(self, client, userdata, msg): if msg.topic == 'microdrop/dmf-device-ui/get-video-settings': self.video_settings = json.loads(msg.payload) self.save_ui_settings(self.video_settings) if self.should_terminate: self.mqtt_client.publish( 'microdrop/dmf-device-ui-plugin/terminate') if msg.topic == 'microdrop/dmf-device-ui/update-protocol': self.update_protocol(json.loads(msg.payload)) def on_plugin_disable(self): self._gui_enabled = False self.cleanup() def on_plugin_enable(self): super(DmfDeviceUiPlugin, self).on_plugin_enable() self.reset_gui() form = flatlandToDict(self.StepFields) self.mqtt_client.publish('microdrop/dmf-device-ui-plugin/schema', json.dumps(form), retain=True) defaults = {} for k, v in form.iteritems(): defaults[k] = v['default'] # defaults = map(lambda (k,v): {k: v['default']}, form.iteritems()) self.mqtt_client.publish('microdrop/dmf-device-ui-plugin/step-options', json.dumps([defaults], cls=PandasJsonEncoder), retain=True) def on_step_removed(self, step_number, step): self.update_steps() def on_step_options_changed(self, plugin, step_number): self.update_steps() def on_step_run(self): ''' Handler called whenever a step is executed. Plugins that handle this signal must emit the on_step_complete signal once they have completed the step. The protocol controller will wait until all plugins have completed the current step before proceeding. ''' app = get_app() # TODO: Migrate video commands to mqtt!! # if (app.realtime_mode or app.running) and self.gui_process is not None: # step_options = self.get_step_options() # if not step_options['video_enabled']: # hub_execute(self.name, 'disable_video', # wait_func=lambda *args: refresh_gui(), timeout_s=5, # silent=True) # else: # hub_execute(self.name, 'enable_video', # wait_func=lambda *args: refresh_gui(), timeout_s=5, # silent=True) emit_signal('on_step_complete', [self.name, None]) def update_steps(self): app = get_app() num_steps = len(app.protocol.steps) protocol = [] for i in range(num_steps): protocol.append(self.get_step_options(i)) self.mqtt_client.publish('microdrop/dmf-device-ui-plugin/step-options', json.dumps(protocol, cls=PandasJsonEncoder), retain=True) def update_protocol(self, protocol): app = get_app() for i, s in enumerate(protocol): step = app.protocol.steps[i] prevData = step.get_data(self.plugin_name) values = {} for k, v in prevData.iteritems(): if k in s: values[k] = s[k] step.set_data(self.plugin_name, values) emit_signal('on_step_options_changed', [self.plugin_name, i], interface=IPlugin)