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
Exemple #3
0
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)
Exemple #6
0
    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))
Exemple #7
0
 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]))
Exemple #8
0
    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)
Exemple #9
0
    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
Exemple #10
0
    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
Exemple #11
0
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)})
Exemple #14
0
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
Exemple #15
0
    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 AppFields(self):
     return Form.of(
         Directory.named('notebook_directory').using(default='',
                                                     optional=True), )
Exemple #19
0
    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()
Exemple #23
0
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)
Exemple #24
0
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
Exemple #25
0
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()])
Exemple #26
0
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])
Exemple #27
0
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)
Exemple #28
0
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
Exemple #31
0
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()
Exemple #32
0
    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())
Exemple #33
0
    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())
Exemple #34
0
    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 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()])
Exemple #36
0
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)
Exemple #38
0
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')
Exemple #39
0
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)