Exemplo n.º 1
0
class MqttPlugin(pmh.BaseMqttReactor, Plugin):
    """
    This class is automatically registered with the PluginManager.
    """
    implements(IPlugin)
    version = __version__
    plugin_name = get_plugin_info(ph.path(__file__).parent).plugin_name

    def __init__(self):
        super(MqttPlugin, self).__init__()
        self.name = self.plugin_name

    ###########################################################################
    # MicroDrop pyutilib plugin handlers
    # ==================================
    def on_plugin_disable(self):
        """
        Handler called once the plugin instance is disabled.
        """
        # Stop MQTT reactor.
        self.stop()

    def on_plugin_enable(self):
        """
        Handler called once the plugin instance is enabled.
        """
        # Start MQTT reactor.
        self.start()
Exemplo n.º 2
0
class MinimalPlugin(Plugin):
    implements(IPlugin)

    plugin_name = 'minimal_plugin'
    version = __version__

    @property
    def name(self):
        return self.plugin_name

    @name.setter
    def name(self, value):
        pass

    @asyncio.coroutine
    def on_step_run(self, plugin_kwargs, signals):
        _L().debug('doing some work when a step is run')
        raise asyncio.Return()
Exemplo n.º 3
0
class EmqttdPlugin(Plugin):
    """
    This class is automatically registered with the PluginManager.
    """
    implements(IPlugin)
    version = __version__
    plugin_name = get_plugin_info(ph.path(__file__).parent).plugin_name

    def __init__(self):
        self.name = self.plugin_name
        # Flag to indicate whether or not we started the `emqttd` service.
        # Used to determine if we should **stop** the service when the plugin
        # is disabled.
        self.launched_service = False

    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(EmqttdPlugin, self).on_plugin_enable()

        # Launch `emqttd` service (if not already running).
        self.launched_service = emqttd_start()

    def on_plugin_disable(self):
        """
        Handler called once the plugin instance is disabled.
        """
        if self.launched_service:
            # We started the `emqttd` service, so stop it.
            emqttd_stop()
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
class ProtocolTranslatorPlugin(Plugin):
    """
    This class is automatically registered with the PluginManager.
    """
    implements(IPlugin)
    version = __version__
    plugin_name = get_plugin_info(ph.path(__file__).parent).plugin_name

    def __init__(self):
        self.menu = None

    def on_plugin_enable(self):
        @gtk_threadsafe
        def init_ui():
            if self.menu is None:
                # Schedule initialization of menu user interface.  Calling
                # `create_ui()` directly is not thread-safe, since it includes GTK
                # code.
                self.create_ui()
            else:
                self.menu.show()

        init_ui()

    def on_plugin_disable(self):
        if self.menu is not None:
            self.menu.hide()

    def create_ui(self):
        self.menu = gtk.MenuItem('E_xport 2.35+ protocol...')
        self.menu.set_tooltip_text('Export protocol compatible with MicroDrop '
                                   '2.35+.')
        self.menu.show_all()
        app = get_app()
        # Add main DropBot menu to MicroDrop `Tools` menu.
        app.main_window_controller.menu_tools.append(self.menu)
        self.menu.connect('activate',
                          lambda menu_item: self._export_protocol())

    @gtk_threadsafe
    def _export_protocol(self):
        app = get_app()

        filter_ = gtk.FileFilter()
        filter_.set_name(' MicroDrop protocols (*.json)')
        filter_.add_pattern("*.json")

        dialog = gtk.FileChooserDialog(
            title="Export protocol",
            action=gtk.FILE_CHOOSER_ACTION_SAVE,
            buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE,
                     gtk.RESPONSE_OK))
        dialog.add_filter(filter_)
        dialog.set_default_response(gtk.RESPONSE_OK)
        dialog.set_current_name(app.protocol.name)
        dialog.set_current_folder(
            os.path.join(app.get_device_directory(), app.dmf_device.name,
                         "protocols"))
        response = dialog.run()
        try:
            if response == gtk.RESPONSE_OK:
                filename = ph.path(dialog.get_filename())
                if filename.ext.lower() != '.json':
                    filename = filename + '.json'
                logger = _L()  # use logger with method context
                try:
                    with open(filename, 'w') as output:
                        protocol_dict_to_json(upgrade_protocol(app.protocol),
                                              ostream=output,
                                              validate=False,
                                              json_kwargs={'indent': 2})
                except md.protocol.SerializationError, exception:
                    plugin_exception_counts = Counter(
                        [e['plugin'] for e in exception.exceptions])
                    logger.info('%s: `%s`', exception, exception.exceptions)
                    result = yesno(
                        'Error exporting data for the following '
                        'plugins: `%s`\n\n'
                        'Would you like to exclude this data and '
                        'export anyway?' %
                        ', '.join(sorted(plugin_exception_counts.keys())))
                    if result == gtk.RESPONSE_YES:
                        # Delete plugin data that is causing serialization
                        # errors.
                        protocol = copy.deepcopy(app.protocol)
                        protocol.remove_exceptions(exception.exceptions,
                                                   inplace=True)
                        with open(filename, 'w') as output:
                            protocol_dict_to_json(upgrade_protocol(
                                app.protocol),
                                                  ostream=output,
                                                  validate=False,
                                                  json_kwargs={'indent': 2})
                    else:
                        # Abort export.
                        logger.warn('Export cancelled.')
                        return
                logger.info('exported protocol to %s', filename)
                app = get_app()
                parent_window = app.main_window_controller.view
                message = 'Exported protocol to:\n\n%s' % filename
                ok_dialog = gtk.MessageDialog(parent=parent_window,
                                              message_format=message,
                                              type=gtk.MESSAGE_OTHER,
                                              buttons=gtk.BUTTONS_OK)
                # Increase default dialog size.
                ok_dialog.set_size_request(450, 150)
                ok_dialog.props.title = 'Export complete'
                ok_dialog.props.use_markup = True
                ok_dialog.run()
                ok_dialog.destroy()
        finally:
            dialog.destroy()
Exemplo n.º 6
0
class WaitForAckPlugin(Plugin):
    implements(IPlugin)

    plugin_name = 'wait_for_ack_plugin'
    version = __version__

    @property
    def name(self):
        return self.plugin_name

    @name.setter
    def name(self, value):
        pass

    @asyncio.coroutine
    def on_step_run(self, plugin_kwargs, signals):
        '''Wait for operator to manually acknowledge that step is complete.

        Display a GTK dialog with **OK** and **Cancel** buttons.  If **Cancel**
        is pressed, raise a ``RuntimeError`` exception is raised.  If **OK** is
        pressed, finish step execution.

        Parameters
        ----------
        message : str
            Message displayed to user in dialog.
        description : str, optional
            Title of the prompt (if specified).

        Raises
        ------
        RuntimeError
            If **Cancel** button is pressed.
        '''
        message = 'Click OK to continue or Cancel to stop protocol'

        # Use `asyncio_helpers.sync` decorator to:
        #  1. Launch dialog from main GTK thread (through `gtk_threadsafe`)
        #  2. Provide an asyncio wrapper around async GTK call, which returns
        #     only after the scheduled call has finished executing. This makes
        #     it possible to coordinate between UI code and protocol asyncio code.
        @sync(gtk_threadsafe)
        def _acknowledge():
            app = md.app_context.get_app()
            parent_window = app.main_window_controller.view
            dialog = gtk.MessageDialog(parent=parent_window,
                                       message_format=message,
                                       type=gtk.MESSAGE_OTHER,
                                       buttons=gtk.BUTTONS_OK_CANCEL)
            # Increase default dialog size.
            dialog.props.title = 'Wait for acknowledgement'
            dialog.props.use_markup = True

            response_code = dialog.run()
            dialog.destroy()
            return response_code

        _L().debug('wait for user to acknowledge step completion')

        # Queue dialog to be launched in GTK thread and wait for response.
        response = yield asyncio.From(_acknowledge())
        if response != gtk.RESPONSE_OK:
            raise RuntimeError('Cancelled in response to message `%s`.' % message)
        else:
            _L().debug('user acknowledged step completion')
Exemplo n.º 7
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)
Exemplo n.º 8
0
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 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'])
Exemplo n.º 10
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)
Exemplo n.º 11
0
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()
Exemplo n.º 12
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')
from logging_helpers import _L
from microdrop.plugin_manager import (PluginGlobals, Plugin, IPlugin,
                                      implements)
import trollius as asyncio

from ._version import get_versions

__version__ = get_versions()['version']
del get_versions

PluginGlobals.push_env('microdrop.managed')


class {{cookiecutter.plugin_name.split('_')|map('title')|join('')}}(Plugin):
    implements(IPlugin)

    plugin_name = '{{cookiecutter.plugin_name}}'
    version = __version__

    @property
    def name(self):
        return self.plugin_name

    @name.setter
    def name(self, value):
        pass

    @asyncio.coroutine
    def on_step_run(self, plugin_kwargs, signals):
        _L().debug('doing some work when a step is run')
        raise asyncio.Return()
Exemplo n.º 14
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)
Exemplo n.º 15
0
class JoypadControlPlugin(Plugin):
    '''
    Trigger electrode state directional controls using a joypad.

     - Up, down, left, and right: corresponding directional control
     - Button 0: clear all electrode states
     - Button 3: actuate electrodes where liquid is detected
    '''
    implements(IPlugin)
    version = __version__
    plugin_name = 'joypad_control_plugin'

    def __init__(self):
        self.name = self.plugin_name
        self.signals = blinker.Namespace()
        self.task = None
        self._most_recent_message = {}

    def on_plugin_enable(self):
        # Start joypad listener.
        self.task = ah.cancellable(check_joypad)
        self.signals.clear()

        liquid_state = {}

        def _on_changed(message):
            self._most_recent_message = message

            if ((abs(message['new']['axes']['x']) > .4)  ^
                (abs(message['new']['axes']['y']) > .4)):

                # Either **x** or **y** (_not_ both) is pressed.
                if message['new']['axes']['x'] > .4:
                    # Right.
                    direction = 'right'
                elif message['new']['axes']['x'] < -.4:
                    # Left.
                    direction = 'left'
                if message['new']['axes']['y'] > .4:
                    # Down.
                    direction = 'down'
                elif message['new']['axes']['y'] < -.4:
                    # Up.
                    direction = 'up'

                if all((direction in ('right', 'left'), liquid_state,
                        message['new']['button_states'][3])):
                    electrodes = liquid_state['electrodes']

                    if 'i' in liquid_state:
                        i = liquid_state['i'] + (1 if direction == 'right'
                                                 else -1)
                    else:
                        i = (0 if direction == 'right'
                             else len(electrodes) - 1)
                    i = i % len(electrodes)
                    hub_execute_async('dropbot_plugin', 'identify_electrode',
                                      electrode_id=electrodes[i])
                    liquid_state['i'] = i
                else:
                    hub_execute_async('microdrop.electrode_controller_plugin',
                                      'set_electrode_direction_states',
                                      direction=direction)

        def _on_buttons_changed(message):
            if message['buttons'] == {0: True}:
                # Button 0 was pressed.
                hub_execute_async('microdrop.electrode_controller_plugin',
                                  'clear_electrode_states')
            elif message['buttons'] == {3: True}:
                # Button 3 was pressed.
                i = liquid_state.get('i')
                liquid_state.clear()

                def _on_found(zmq_response):
                    data = decode_content_data(zmq_response)
                    liquid_state['electrodes'] = data
                    if i is not None and i < len(liquid_state['electrodes']):
                        liquid_state['i'] = i

                hub_execute_async('dropbot_plugin', 'find_liquid',
                                  callback=_on_found)
            elif message['buttons'] == {3: False}:
                # Button 3 was released.
                _L().info('Button 3 was released. `%s`', liquid_state)
                i = liquid_state.get('i')
                if i is not None:
                    selected_electrode = liquid_state['electrodes'][i]
                    electrode_states = pd.Series(1, index=[selected_electrode])
                    hub_execute_async('microdrop.electrode_controller_plugin',
                                      'clear_electrode_states')
                    hub_execute_async('microdrop.electrode_controller_plugin',
                                      'set_electrode_states',
                                      electrode_states=electrode_states)
            elif message['buttons'] == {4: True}:
                # Button 4 was pressed.
                if message['new']['button_states'][8]:
                    # Button 8 was also held down.
                    hub_execute_async('microdrop.gui.protocol_controller',
                                      'first_step')
                else:
                    hub_execute_async('microdrop.gui.protocol_controller',
                                      'prev_step')
            elif message['buttons'] == {5: True}:
                # Button 5 was pressed.
                if message['new']['button_states'][8]:
                    # Button 8 was also held down.
                    hub_execute_async('microdrop.gui.protocol_controller',
                                      'last_step')
                else:
                    hub_execute_async('microdrop.gui.protocol_controller',
                                      'next_step')
            elif message['buttons'] == {9: True}:
                # Button 9 was pressed.
                hub_execute_async('microdrop.gui.protocol_controller',
                                  'run_protocol')
            elif all(message['buttons'].values()):
                _L().info('%s', message)

        self.signals.signal('state-changed').connect(_on_changed, weak=False)
        self.signals.signal('buttons-changed').connect(_on_buttons_changed,
                                                       weak=False)
        thread = threading.Thread(target=self.task, args=(self.signals, 0))
        thread.daemon = True
        thread.start()

    def on_plugin_disable(self):
        # Stop joypad listener.
        if self.task is not None:
            self.task.cancel()
            self.task = None
        self.signals.clear()
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)})
Exemplo n.º 17
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])
Exemplo n.º 18
0
class DropBotPlugin(Plugin, StepOptionsController, AppDataController,
                    pmh.BaseMqttReactor):
    """
    This class is automatically registered with the PluginManager.
    """
    implements(IPlugin)
    implements(IWaveformGenerator)

    version = __version__
    plugin_name = str(ph.path(__file__).realpath().parent.name)

    @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 = self.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
        self.menu_items = []
        self.menu = None
        self.menu_item_root = None
        self.diagnostics_results_dir = '.dropbot-diagnostics'
        self.channels = None
        self.electrodes = None
        pmh.BaseMqttReactor.__init__(self)
        self.start()

    def start(self):
        # Connect to MQTT broker.
        self._connect()
        self.mqtt_client.loop_start()

    def on_channels_set(self, payload, args):
        self.channels = payload
        self.read_capacitance()

    def on_electrodes_set(self, payload, args):
        self.electrodes = payload
        if (self.channels is None):
            return
        channel_states = pd.Series(name="channels")
        for electode_id, state in self.electrodes.iteritems():
            c = self.channels[self.channels['electrode_id'] == electode_id]
            # XXX: Assuming electrodes only have one channel:
            channel = c['channel'].values[0]
            channel_states[str(channel)] = state

        self.update_channel_states(channel_states)
        self.read_capacitance()

    def on_plugin_changed(self, payload, args):
        self.set_voltage(payload['default_voltage'])
        self.set_frequency(payload['default_frequency'])

    def on_running_state_requested(self, payload, args):
        self.trigger("send-running-state", self.plugin_path)

    def listen(self):
        # TODO: Move running state messages to base class:
        self.bindSignalMsg("running", "send-running-state")
        self.bindStateMsg("stats", "set-stats")
        self.bindStateMsg("voltage", "set-voltage")
        self.bindStateMsg("frequency", "set-frequency")
        self.bindStateMsg("capacitance", "set-capacitance")
        self.onStateMsg("electrodes-model", "channels", self.on_channels_set)
        self.onStateMsg("electrodes-model", "electrodes",
                        self.on_electrodes_set)
        self.onSignalMsg("web-server", "running-state-requested",
                         self.on_running_state_requested)
        self.onSignalMsg("{pluginName}",
                         self.url_safe_plugin_name + "-changed",
                         self.on_plugin_changed)
        # Update schema:
        self.bindPutMsg("schema-model", "schema", "put-schema")
        form = flatlandToDict(self.AppFields)
        self.trigger("put-schema", {
            'schema': form,
            'pluginName': self.url_safe_plugin_name
        })

    def read_capacitance(self):
        capacitance = None
        if self.control_board:
            capacitance = self.control_board.measure_capacitance()
        print("<DropbotPlugin#read_capacitance> Capacitance: %s" % capacitance)

        self.trigger("set-capacitance", {
            'capacitance': capacitance,
            'pluginName': self.url_safe_plugin_name
        })

    @property
    def status(self):
        '''
        .. versionadded:: 0.14
        '''
        if self.control_board is None:
            return 'disconnected'
        else:
            return 'connected'

    @gtk_threadsafe  # Execute in GTK main thread
    @error_ignore(lambda exception, func, self, test_name, *args: logger.error(
        'Error executing: "%s"', test_name, exc_info=True))
    @require_connection  # Display error dialog if DropBot is not connected.
    def execute_test(self, test_name, axis_count=1):
        '''
        Run single DropBot on-board self-diagnostic test.

        Record test results as JSON and display dialog to show text summary
        (and plot, where applicable).

        .. versionadded:: 0.14
        '''
        test_func = getattr(db.hardware_test, test_name)
        results = {test_name: test_func(self.control_board)}
        db.hardware_test.log_results(results, self.diagnostics_results_dir)
        format_func = getattr(db.self_test, 'format_%s_results' % test_name)
        message = format_func(results[test_name])
        map(logger.info, map(unicode.rstrip, unicode(message).splitlines()))

        app = get_app()
        parent = app.main_window_controller.view
        dialog = results_dialog(test_name,
                                results,
                                parent=parent,
                                axis_count=axis_count)
        dialog.run()
        dialog.destroy()

    @gtk_threadsafe  # Execute in GTK main thread
    @error_ignore(lambda *args: logger.error(
        'Error executing DropBot self tests.', exc_info=True))
    @require_connection  # Display error dialog if DropBot is not connected.
    @require_test_board  # Prompt user to insert DropBot test board
    def run_all_tests(self):
        '''
        Run all DropBot on-board self-diagnostic tests.

        Record test results as JSON and results summary as a Word document.

        .. versionadded:: 0.14

        .. versionchanged:: 0.16
            Prompt user to insert DropBot test board.
        '''
        results = db.self_test.self_test(self.control_board)
        results_dir = ph.path(self.diagnostics_results_dir)
        results_dir.makedirs_p()

        # Create unique output filenames based on current timestamp.
        timestamp = dt.datetime.now().isoformat().replace(':', '_')
        json_path = results_dir.joinpath('results-%s.json' % timestamp)
        report_path = results_dir.joinpath('results-%s.docx' % timestamp)

        # Write test results encoded as JSON.
        with json_path.open('w') as output:
            # XXX Use `json_tricks` rather than standard `json` to support
            # serializing [Numpy arrays and scalars][1].
            #
            # [1]: http://json-tricks.readthedocs.io/en/latest/#numpy-arrays
            output.write(json_tricks.dumps(results, indent=4))

        # Generate test result summary report as Word document.
        db.self_test.generate_report(results,
                                     output_path=report_path,
                                     force=True)
        # Launch Word document report.
        report_path.launch()

    def create_ui(self):
        '''
        Create user interface elements (e.g., menu items).

        .. versionchanged:: 0.14
            Add "Run all on-board self-tests..." menu item.

            Add "On-board self-tests" menu.

        .. versionchanged:: 0.15
            Add "Help" menu item.

        .. versionchanged:: 0.16
            Prompt user to insert DropBot test board before running channels
            test.
        '''
        # Create head for DropBot on-board tests sub-menu.
        tests_menu_head = gtk.MenuItem('On-board self-_tests')

        # Create main DropBot menu.
        self.menu_items = [
            gtk.MenuItem('Run _all on-board self-tests...'),
            gtk.MenuItem('_Help...'),
            gtk.SeparatorMenuItem(), tests_menu_head
        ]
        self.menu_items[0].connect('activate',
                                   lambda menu_item: self.run_all_tests())
        help_url = 'https://github.com/sci-bots/microdrop.dropbot-plugin/wiki/Quick-start-guide'
        self.menu_items[1].connect(
            'activate', lambda menu_item: webbrowser.open_new_tab(help_url))
        app = get_app()
        self.menu = gtk.Menu()
        self.menu.show_all()
        self.menu_item_root = gtk.MenuItem('_DropBot')
        self.menu_item_root.set_submenu(self.menu)
        self.menu_item_root.show_all()
        for menu_item_i in self.menu_items:
            self.menu.append(menu_item_i)
            menu_item_i.show()

        # Add main DropBot menu to MicroDrop `Tools` menu.
        app.main_window_controller.menu_tools.append(self.menu_item_root)

        # Create DropBot on-board tests sub-menu.
        tests_menu = gtk.Menu()
        tests_menu_head.set_submenu(tests_menu)

        # List of on-board self-tests.
        tests = [{
            'test_name': 'test_voltage',
            'title': 'Test high _voltage'
        }, {
            'test_name': 'test_on_board_feedback_calibration',
            'title': 'On-board _feedback calibration'
        }, {
            'test_name': 'test_shorts',
            'title': 'Detect _shorted '
            'channels'
        }, {
            'test_name': 'test_channels',
            'title': 'Scan test _board'
        }]

        # Add a menu item for each test to on-board tests sub-menu.
        for i, test_i in enumerate(tests):
            axis_count_i = 2 if test_i['test_name'] == 'test_channels' else 1
            menu_item_i = gtk.MenuItem(test_i['title'])

            def _exec_test(menu_item, test_name, axis_count):
                self.execute_test(test_name, axis_count)

            if test_i['test_name'] == 'test_channels':
                # Test board is required for `test_channels` test.
                _exec_test = require_test_board(_exec_test)

            menu_item_i.connect('activate', _exec_test, test_i['test_name'],
                                axis_count_i)
            menu_item_i.show()
            tests_menu.append(menu_item_i)

    @property
    def AppFields(self):
        serial_ports = list(get_serial_ports())
        if len(serial_ports):
            default_port = serial_ports[0]
        else:
            default_port = None

        return Form.of(
            Enum.named('serial_port').using(
                default=default_port, optional=True).valued(*serial_ports),
            Float.named('default_duration').using(default=1000, optional=True),
            Float.named('default_voltage').using(default=80, optional=True),
            Float.named('default_frequency').using(default=10e3,
                                                   optional=True),
            Boolean.named('Auto-run diagnostic tests').using(default=True,
                                                             optional=True))

    def get_step_form_class(self):
        """
        Override to set default values based on their corresponding app options.
        """
        app_values = self.get_app_values()
        return Form.of(
            Integer.named('duration').using(
                default=app_values['default_duration'],
                optional=True,
                validators=[ValueAtLeast(minimum=0)]),
            Float.named('voltage').using(
                default=app_values['default_voltage'],
                optional=True,
                validators=[ValueAtLeast(minimum=0), max_voltage]),
            Float.named('frequency').using(
                default=app_values['default_frequency'],
                optional=True,
                validators=[ValueAtLeast(minimum=0), check_frequency]))

    def update_channel_states(self, channel_states):
        logging.info('update_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 is not 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
        if self.control_board is not None:
            self.control_board.terminate()
            self.control_board = None

    def on_plugin_enable(self):
        super(DropBotPlugin, self).on_plugin_enable()
        if not self.menu_items:
            # Schedule initialization of menu user interface.  Calling
            # `create_ui()` directly is not thread-safe, since it includes GTK
            # code.
            gobject.idle_add(self.create_ui)

        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
            self.control_board.terminate()
            self.control_board = None
        except Exception:
            # 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.
        """
        if self.control_board:
            self.control_board.terminate()
            self.control_board = None
        self.current_frequency = None
        serial_ports = list(get_serial_ports())
        if serial_ports:
            app_values = self.get_app_values()
            # try to connect to the last successful port
            try:
                port = app_values.get('serial_port')
                self.control_board = SerialProxy(port=port)
            except Exception:
                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 DropBot
         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")

            host_software_version = utility.Version.fromstring(
                str(self.control_board.host_software_version))
            remote_software_version = utility.Version.fromstring(
                str(self.control_board.remote_software_version))

            @gtk_threadsafe
            def _firmware_check():
                # 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 DropBot 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()

            # Call as thread-safe function, since function uses GTK.
            _firmware_check()
        except pkg_resources.DistributionNotFound:
            logger.debug(
                'No distribution found for `%s`.  This may occur if, '
                'e.g., `%s` is installed using `conda develop .`',
                name,
                name,
                exc_info=True)
        except Exception, why:
            logger.warning("%s" % why)

        self.update_connection_status()
Exemplo n.º 19
0
class ZmqHubPlugin(Plugin, AppDataController):
    """
    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://*: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=(Hub(app_values['hub_uri'], self.name),
                                         getattr(
                                             logging,
                                             app_values['log_level'].upper())))
        self.hub_process.daemon = False
        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 on_app_exit(self):
        """
        Handler called just before the Microdrop application exits.
        """
        if self.hub_process is not None:
            self.hub_process.terminate()
            self.hub_process = None