Ejemplo n.º 1
0
class PersonSchema(Form):

    name = String

    age = Integer.using(validators=[
        ValueAtLeast(minimum=18),
        ValueAtMost(maximum=120)
    ])

    weight = Integer.using(validators=[
        ValueAtLeast(minimum=0),
        ValueAtMost(maximum=300)
    ])
    weight.render_options = dict(
        style='slider'
    )

    friendly = Boolean

    address = String.using()
    address.render_options = dict(
        style='multiline'
    )

    happy = Boolean.using()
    happy.render_options = dict(
        style='toggle'
    )
Ejemplo n.º 2
0
def get_flatland_field(jsonschema_field):
    kwargs = {'optional': True}
    for k in ('default', ):
        if k in jsonschema_field.attributes:
            kwargs[k] = jsonschema_field.attributes[k]

    JSONSCHEMA_TO_FLATLAND_TYPES = {
        'boolean': Boolean,
        'integer': Integer,
        'number': Float,
        'string': String,
    }

    if 'enum' in jsonschema_field.attributes:
        # JSON schema field has enumerated values.  `Enum` is a special
        # type in Flatland.
        flatland_type = Enum
    else:
        flatland_type = JSONSCHEMA_TO_FLATLAND_TYPES[jsonschema_field
                                                     .field_type]
    if flatland_type == String:
        # Do not strip string.
        kwargs['strip'] = False
    kwargs['validators'] = []
    if 'minimum' in jsonschema_field.attributes:
        kwargs['validators'] += [ValueAtLeast(jsonschema_field
                                              .attributes['minimum'])]
    if 'maximum' in jsonschema_field.attributes:
        kwargs['validators'] += [ValueAtMost(jsonschema_field
                                             .attributes['maximum'])]
    flatland_field = flatland_type.using(**kwargs)
    if 'enum' in jsonschema_field.attributes:
        flatland_field = flatland_field.valued(*jsonschema_field.attributes
                                               ['enum'])
    return flatland_field
Ejemplo n.º 3
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]))
Ejemplo n.º 4
0
def integer_entry_dialog(name,
                         value=0,
                         title='Input value',
                         min_value=None,
                         max_value=None,
                         parent=None,
                         use_markup=True):
    if parent is None:
        parent = DEFAULTS.parent_widget
    validators = []
    if min_value is not None:
        ValueAtLeast(minimum=min_value)
    if max_value is not None:
        ValueAtMost(maximum=max_value)

    valid, response = field_entry_dialog(
        Integer.named(name).using(validators=validators),
        value,
        title,
        parent=parent,
        use_markup=use_markup)
    if valid:
        return response
    return None
Ejemplo n.º 5
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
Ejemplo n.º 6
0
Quicklinks = MyJoinedString.of(String).with_properties(
    widget=WIDGET_MULTILINE_TEXT, rows=ROWS,
    cols=COLS).using(label=L_('Quick Links'),
                     optional=True,
                     separator='\n',
                     separator_regex=re.compile(r'[\r\n]+'))

Search = Text.using(default='', optional=True).with_properties(
    widget=WIDGET_SEARCH, placeholder=L_("Search Query"))

_Integer = Integer.validated_by(Converted())

AnyInteger = _Integer.with_properties(widget=WIDGET_ANY_INTEGER)

Natural = AnyInteger.validated_by(ValueAtLeast(0))

SmallNatural = _Integer.with_properties(widget=WIDGET_SMALL_NATURAL)

RadioChoice = Text.with_properties(widget=WIDGET_RADIO_CHOICE)


class DateTimeUNIX(_DateTime):
    """
    A DateTime that uses a UNIX timestamp instead of datetime as internal
    representation of DateTime.
    """
    def serialize(self, value):
        """Serializes value to string."""
        if isinstance(value, int):
            try:
Ejemplo n.º 7
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)
    def create_ui(self):
        self.widget = gtk.Assistant()
        self.widget.connect("prepare", self.assistant_prepared)
        self.widget.connect("cancel", self.cancel_button_clicked)
        self.widget.connect("close", self.close_button_clicked)
        self.widget.connect("apply", self.apply_button_clicked)

        # # Introduction #
        box = gtk.HBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_INTRO)
        self.widget.set_page_title(box, "Introduction")
        content = ('This wizard will guide you through the process of '
                   'calibrating the high-voltage reference load feedback '
                   'measurement circuit.  This feedback circuit is used to '
                   'measure the output voltage of the amplifier on the control'
                   'board.\n\nSee '
                   r'<a href="http://microfluidics.utoronto.ca/trac/dropbot/wiki/Control board calibration#high-voltage-attenuation-calibration">'
                   'here</a> for more details.')
        label = gtk.Label(content)
        label.set_use_markup(True)
        label.set_line_wrap(True)
        image = gtk.Image()
        img_path = pkg_resources.resource_filename(
            'dmf_control_board_firmware', 'gui/reference_feedback_intro.png')
        image.set_from_file(str(img_path))
        box.pack_start(label, True, False, padding=15)
        box.pack_start(image, True, True, padding=5)
        self.widget.set_page_complete(box, True)

        # # Connect hardware #
        box = gtk.HBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_CONTENT)
        self.widget.set_page_title(box, "Connect hardware")
        label = gtk.Label(' - Connect DropBot "<tt>Out to Amp</tt>" to amplifier input.\n'
                          ' - Use T-splitter to connect amplifier output to:\n'
                          '   1) DropBot "<tt>In from Amp</tt>".\n'
                          '   2) Oscilloscope input.')
        image = gtk.Image()
        img_path = pkg_resources.resource_filename(
            'dmf_control_board_firmware', 'gui/reference_feedback_setup.png')
        image.set_from_file(str(img_path))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        box.pack_start(label, True, False, padding=15)
        box.pack_start(image, True, True, padding=5)
        self.widget.set_page_complete(box, True)

        # # Select frequencies #
        minimum = self.control_board.min_waveform_frequency
        maximum = self.control_board.max_waveform_frequency
        form = Form.of(
            Integer.named('start_frequency').using(
                default=minimum, optional=True,
                validators=[ValueAtLeast(minimum=minimum), ]),
            Integer.named('end_frequency').using(
                default=maximum, optional=True,
                validators=[ValueAtLeast(minimum=minimum), ]),
            Integer.named('number_of_steps').using(
                default=10, optional=True,
                validators=[ValueAtLeast(minimum=2), ]),
        )
        box = gtk.HBox()
        self.form_view = create_form_view(form)
        self.form_view.form.proxies.connect('changed', display)
        box.pack_start(self.form_view.widget, fill=False, padding=40)
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_CONTENT)
        self.widget.set_page_title(box, "Select calibration frequencies")
        self.widget.set_page_complete(box, True)

        # # Record measurements #
        box1 = gtk.VBox()
        self.widget.append_page(box1)
        self.widget.set_page_type(box1, gtk.ASSISTANT_PAGE_PROGRESS)
        self.widget.set_page_title(box1, "Record measurements")
        self.measurements_label = gtk.Label('Ready')
        self.measurements_label.set_line_wrap(True)
        self.measure_progress = gtk.ProgressBar()
        self.measure_progress.set_size_request(300, 40)
        box1.pack_start(self.measurements_label, True, True, 0)
        box1.pack_start(self.measure_progress, expand=False, fill=False,
                        padding=15)
        self.box1 = box1

        # # Confirm fitted parameters #
        box = gtk.VBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_CONFIRM)
        self.widget.set_page_title(box, "Confirm fitted parameters")
        figure = Figure(figsize=(14, 8), dpi=60)
        self.canvas = FigureCanvasGTK(figure)
        toolbar = NavigationToolbar(self.canvas, self.widget)
        self.axis = figure.add_subplot(111)
        box.pack_start(self.canvas)
        box.pack_start(toolbar, False, False)
        self.widget.set_page_complete(box, True)

        # # Summary #
        box = gtk.VBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_SUMMARY)
        self.widget.set_page_title(box, "Summary")
        label = gtk.Label('Calibration of reference load feedback circuit is '
                          'complete.  The high-voltage output from amplifier '
                          'should now be measured accurately by the control '
                          'board.')
        label.set_line_wrap(True)
        box.pack_start(label, True, True, 0)
        self.widget.set_page_complete(box, True)
Ejemplo n.º 9
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:
Ejemplo n.º 10
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()
Ejemplo n.º 11
0
    def create_ui(self):
        self.widget = gtk.Assistant()
        self.widget.connect("prepare", self.assistant_prepared)
        self.widget.connect("cancel", self.cancel_button_clicked)
        self.widget.connect("close", self.close_button_clicked)
        self.widget.connect("apply", self.apply_button_clicked)

        # # Introduction #
        box = gtk.HBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_INTRO)
        self.widget.set_page_title(box, "Introduction")
        content = (
            'This wizard will guide you through the process of '
            'calibrating the device load feedback measurement circuit. '
            'This feedback circuit is used to measure the impedance '
            'between the actuated area and ground.  This impedance is '
            'related to the volume of liquid between the actuated area'
            '\n\nSee '
            r'<a href="http://microfluidics.utoronto.ca/trac/dropbot/wiki/Control board calibration#device-load-impedance-calibration">'
            'here</a> for more details.')
        label = gtk.Label(content)
        label.set_use_markup(True)
        label.set_line_wrap(True)
        image = gtk.Image()
        img_path = pkg_resources.resource_filename(
            'dmf_control_board_firmware', 'gui/impedance_feedback_intro.png')
        image.set_from_file(str(img_path))
        box.pack_start(label, True, False, padding=15)
        box.pack_start(image, True, True, padding=5)
        self.widget.set_page_complete(box, True)

        # # Connect hardware #
        box = gtk.HBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_CONTENT)
        self.widget.set_page_title(box, "Connect hardware")
        label = gtk.Label(
            ' - Connect DropBot "<tt>Out to Amp</tt>" to amplifier input.\n'
            ' - Connect amplifier output to DropBot "<tt>In from Amp</tt>".\n'
            ' - Connect DropBot "<tt>0-39</tt>" to test board.')
        image = gtk.Image()
        img_path = pkg_resources.resource_filename(
            'dmf_control_board_firmware', 'gui/impedance_feedback_setup.png')
        image.set_from_file(str(img_path))
        label.set_line_wrap(True)
        label.set_use_markup(True)
        box.pack_start(label, True, False, padding=15)
        box.pack_start(image, True, True, padding=5)
        self.widget.set_page_complete(box, True)

        # # Select frequencies #
        form = Form.of(
            Integer.named('start_frequency').using(
                default=self.control_board.min_waveform_frequency,
                optional=True,
                validators=[
                    ValueAtLeast(
                        minimum=self.control_board.min_waveform_frequency),
                    ValueAtMost(
                        maximum=self.control_board.max_waveform_frequency)
                ]),
            Integer.named('end_frequency').using(
                default=self.control_board.max_waveform_frequency,
                optional=True,
                validators=[
                    ValueAtLeast(
                        minimum=self.control_board.min_waveform_frequency),
                    ValueAtMost(
                        maximum=self.control_board.max_waveform_frequency)
                ]),
            Integer.named('number_of_steps').using(default=10,
                                                   optional=True,
                                                   validators=[
                                                       ValueAtLeast(minimum=2),
                                                   ]),
            Integer.named('RMS_voltage').using(
                default=min(100, self.control_board.max_waveform_voltage),
                optional=True,
                validators=[
                    ValueAtLeast(minimum=10),
                    ValueAtMost(
                        maximum=self.control_board.max_waveform_voltage)
                ]))
        box = gtk.HBox()
        self.form_view = create_form_view(form)
        box.pack_start(self.form_view.widget, fill=False, padding=40)
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_CONTENT)
        self.widget.set_page_title(box, "Select calibration frequencies")
        self.widget.set_page_complete(box, True)

        ## # Record measurements #
        box1 = gtk.VBox()
        self.widget.append_page(box1)
        self.widget.set_page_type(box1, gtk.ASSISTANT_PAGE_PROGRESS)
        self.widget.set_page_title(box1, "Record measurements")
        self.measurements_label = gtk.Label('Ready.')
        self.measurements_label.set_line_wrap(True)
        self.measure_progress = gtk.ProgressBar()
        box1.pack_start(self.measurements_label, True, True, 0)
        box1.pack_start(self.measure_progress,
                        expand=False,
                        fill=False,
                        padding=15)
        self.box1 = box1

        # # Confirm fitted parameters #
        box = gtk.VBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_CONFIRM)
        self.widget.set_page_title(box, "Confirm fitted parameters")
        self.figure = Figure(dpi=72)
        self.canvas = FigureCanvasGTK(self.figure)
        toolbar = NavigationToolbar(self.canvas, self.widget)
        #self.axis = figure.add_subplot(111)
        box.pack_start(self.canvas)
        box.pack_start(toolbar, False, False)
        self.widget.set_page_complete(box, True)

        ## # Summary #
        box = gtk.VBox()
        self.widget.append_page(box)
        self.widget.set_page_type(box, gtk.ASSISTANT_PAGE_SUMMARY)
        self.widget.set_page_title(box, "Summary")
        label = gtk.Label('Calibration of device load feedback circuit is '
                          'complete.  The impedance between actuated device '
                          'area and ground should now be measured accurately '
                          'by the control board.')
        label.set_line_wrap(True)
        box.pack_start(label, True, True, 0)
        self.widget.set_page_complete(box, True)
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'])
Ejemplo n.º 13
0
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)