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()
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 max_voltage(element, state): """Verify that the voltage is below a set maximum""" service = get_service_instance_by_name( get_plugin_info(path(__file__).parent).plugin_name) if service.control_board and \ element.value > service.control_board.max_waveform_voltage: return element.errors.append( 'Voltage exceeds the maximum value ' '(%d V).' % service.control_board.max_waveform_voltage) else: return True
def max_voltage(element, state): """Verify that the voltage is below a set maximum""" service = get_service_instance_by_name( get_plugin_info(path(__file__).parent).plugin_name) if service.control_board.connected() and \ element.value > service.control_board.max_waveform_voltage: return element.errors.append('Voltage exceeds the maximum value ' '(%d V).' % service.control_board.max_waveform_voltage) else: return True
def check_frequency(element, state): """Verify that the frequency is within the valid range""" service = get_service_instance_by_name( get_plugin_info(path(__file__).parent).plugin_name) if service.control_board and \ (element.value < service.control_board.min_waveform_frequency or \ element.value > service.control_board.max_waveform_frequency): return element.errors.append( 'Frequency is outside of the valid range ' '(%.1f - %.1f Hz).' % (service.control_board.min_waveform_frequency, service.control_board.max_waveform_frequency)) else: return True
def check_frequency(element, state): """Verify that the frequency is within the valid range""" service = get_service_instance_by_name( get_plugin_info(path(__file__).parent).plugin_name) if service.control_board.connected() and \ (element.value < service.control_board.min_waveform_frequency or \ element.value > service.control_board.max_waveform_frequency): return element.errors.append('Frequency is outside of the valid range ' '(%.1f - %.1f Hz).' % (service.control_board.min_waveform_frequency, service.control_board.max_waveform_frequency) ) else: return True
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()
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)
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()
class SyringePumpPlugin(Plugin, AppDataController, StepOptionsController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name serial_ports_ = [port for port in get_serial_ports()] if len(serial_ports_): default_port_ = serial_ports_[0] else: default_port_ = None ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' AppFields = Form.of( Enum.named('serial_port').using(default=default_port_, optional=True).valued(*serial_ports_), Float.named('steps_per_microliter').using(optional=True, default=1.0), ) ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Float.named('microliters_per_min').using( optional=True, default=60, #validators= #[ValueAtLeast(minimum=0), # ValueAtMost(maximum=100000)] ), Float.named('microliters').using( optional=True, default=10, #validators= #[ValueAtLeast(minimum=0), # ValueAtMost(maximum=100000)] ), ) def __init__(self): self.name = self.plugin_name self.proxy = None self.initialized = False # Latch to, e.g., config menus, only once def connect(self): """ Try to connect to the syring pump at the default serial port selected in the Microdrop application options. If unsuccessful, try to connect to the proxy on any available serial port, one-by-one. """ from stepper_motor_controller import SerialProxy if len(SyringePumpPlugin.serial_ports_): app_values = self.get_app_values() # try to connect to the last successful port try: self.proxy = SerialProxy(port=str(app_values['serial_port'])) except: logger.warning( 'Could not connect to the syringe pump on port %s. ' 'Checking other ports...', app_values['serial_port'], exc_info=True) self.proxy = SerialProxy() app_values['serial_port'] = self.proxy.port self.set_app_values(app_values) else: raise Exception("No serial ports available.") def check_device_name_and_version(self): """ Check to see if: a) The connected device is a what we are expecting b) The device firmware matches the host driver API version In the case where the device firmware version does not match, display a dialog offering to flash the device with the firmware version that matches the host driver API version. """ try: self.connect() properties = self.proxy.properties package_name = properties['package_name'] display_name = properties['display_name'] if package_name != self.proxy.host_package_name: raise Exception("Device is not a %s" % properties['display_name']) host_software_version = self.proxy.host_software_version remote_software_version = self.proxy.remote_software_version # Reflash the firmware if it is not the right version. if host_software_version != remote_software_version: response = yesno("The %s firmware version (%s) " "does not match the driver version (%s). " "Update firmware?" % (display_name, remote_software_version, host_software_version)) if response == gtk.RESPONSE_YES: self.on_flash_firmware() except Exception, why: logger.warning("%s" % why)
class 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])
def __init__(self): self.control_board = OpenDropBoard() self.name = get_plugin_info(path(__file__).parent).plugin_name self.connection_status = "Not connected" self.current_frequency = None self.timeout_id = None
class DropletPlanningPlugin(Plugin, StepOptionsController, pmh.BaseMqttReactor): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugin_name = get_plugin_info(path(__file__).parent).plugin_name ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Integer.named('trail_length').using(default=1, optional=True, validators= [ValueAtLeast(minimum=1)]), Integer.named('route_repeats').using(default=1, optional=True, validators= [ValueAtLeast(minimum=1)]), Integer.named('repeat_duration_s').using(default=0, optional=True), Integer.named('transition_duration_ms') .using(optional=True, default=750, validators=[ValueAtLeast(minimum=0)])) def __init__(self,*args, **kwargs): self.name = self.plugin_name self.step_start_time = None self.route_controller = None pmh.BaseMqttReactor.__init__(self) self.start() def get_schedule_requests(self, function_name): """ Returns a list of scheduling requests (i.e., ScheduleRequest instances) for the function specified by function_name. """ if function_name in ['on_step_run']: # Execute `on_step_run` before control board. return [ScheduleRequest(self.name, 'dmf_control_board_plugin')] return [] def on_connect(self, client, userdata, flags, rc): self.mqtt_client.subscribe("microdrop/dmf-device-ui/add-route") self.mqtt_client.subscribe("microdrop/dmf-device-ui/get-routes") self.mqtt_client.subscribe("microdrop/dmf-device-ui/clear-routes") self.mqtt_client.subscribe('microdrop/dmf-device-ui/execute-routes') self.mqtt_client.subscribe('microdrop/dmf-device-ui/update-protocol') self.mqtt_client.subscribe("microdrop/mqtt-plugin/step-inserted") def on_message(self, client, userdata, msg): ''' Callback for when a ``PUBLISH`` message is received from the broker. ''' logger.info('[on_message] %s: "%s"', msg.topic, msg.payload) if msg.topic == 'microdrop/dmf-device-ui/add-route': self.add_route(json.loads(msg.payload)) self.get_routes() if msg.topic == 'microdrop/dmf-device-ui/get-routes': self.get_routes() if msg.topic == 'microdrop/dmf-device-ui/clear-routes': data = json.loads(msg.payload) if data: self.clear_routes(electrode_id=data['electrode_id']) else: self.clear_routes() if msg.topic == 'microdrop/dmf-device-ui/execute-routes': self.execute_routes(json.loads(msg.payload)) if msg.topic == 'microdrop/dmf-device-ui/update-protocol': self.update_protocol(json.loads(msg.payload)) if msg.topic == "microdrop/mqtt-plugin/step-inserted": self.step_inserted(json.loads(msg.payload)) def on_plugin_enable(self): self.route_controller = RouteController(self) form = flatlandToDict(self.StepFields) self.mqtt_client.publish('microdrop/droplet-planning-plugin/schema', json.dumps(form), retain=True) defaults = {} for k,v in form.iteritems(): defaults[k] = v['default'] self.mqtt_client.publish('microdrop/droplet-planning-plugin/step-options', json.dumps([defaults], cls=PandasJsonEncoder), retain=True) def on_plugin_disable(self): """ Handler called once the plugin instance is disabled. """ pass def on_app_exit(self): """ Handler called just before the Microdrop application exits. """ pass ########################################################################### # Step event handler methods def on_error(self, *args): logger.error('Error executing routes.', exc_info=True) # An error occurred while initializing Analyst remote control. emit_signal('on_step_complete', [self.name, 'Fail']) def on_protocol_pause(self): self.kill_running_step() def kill_running_step(self): # Stop execution of any routes that are currently running. if self.route_controller is not None: self.route_controller.reset() def on_step_run(self): """ Handler called whenever a step is executed. Note that this signal is only emitted in realtime mode or if a protocol is running. Plugins that handle this signal must emit the on_step_complete signal once they have completed the step. The protocol controller will wait until all plugins have completed the current step before proceeding. return_value can be one of: None 'Repeat' - repeat the step or 'Fail' - unrecoverable error (stop the protocol) """ app = get_app() if not app.running: return self.kill_running_step() step_options = self.get_step_options() try: self.repeat_i = 0 self.step_start_time = datetime.now() df_routes = self.get_routes() self.route_controller.execute_routes( df_routes, step_options['transition_duration_ms'], trail_length=step_options['trail_length'], on_complete=self.on_step_routes_complete, on_error=self.on_error) except: self.on_error() def on_step_routes_complete(self, start_time, electrode_ids): ''' Callback function executed when all concurrent routes for a step have completed a single run. If repeats are requested, either through repeat counts or a repeat duration, *cycle* routes (i.e., routes that terminate at the start electrode) will repeat as necessary. ''' step_options = self.get_step_options() step_duration_s = (datetime.now() - self.step_start_time).total_seconds() if ((step_options['repeat_duration_s'] > 0 and step_duration_s < step_options['repeat_duration_s']) or (self.repeat_i + 1 < step_options['route_repeats'])): # Either repeat duration has not been met, or the specified number # of repetitions has not been met. Execute another iteration of # the routes. self.repeat_i += 1 df_routes = self.get_routes() self.route_controller.execute_routes( df_routes, step_options['transition_duration_ms'], trail_length=step_options['trail_length'], cyclic=True, acyclic=False, on_complete=self.on_step_routes_complete, on_error=self.on_error) else: logger.info('Completed routes (%s repeats in %ss)', self.repeat_i + 1, si_format(step_duration_s)) # Transitions along all droplet routes have been processed. # Signal step has completed and reset plugin step state. emit_signal('on_step_complete', [self.name, None]) def on_step_options_swapped(self, plugin, old_step_number, step_number): """ Handler called when the step options are changed for a particular plugin. This will, for example, allow for GUI elements to be updated based on step specified. Parameters: plugin : plugin instance for which the step options changed step_number : step number that the options changed for """ logger.info('[on_step_swapped] old step=%s, step=%s', old_step_number, step_number) self.kill_running_step() def on_step_removed(self, step_number, step): self.update_steps() def on_step_options_changed(self, plugin, step_number): self.update_steps() def on_step_swapped(self, old_step_number, step_number): """ Handler called when the current step is swapped. """ logger.info('[on_step_swapped] old step=%s, step=%s', old_step_number, step_number) self.kill_running_step() self.get_routes() def on_step_inserted(self, step_number, *args): self.step_inserted(step_number) def step_inserted(self, step_number): app = get_app() logger.info('[on_step_inserted] current step=%s, created step=%s', app.protocol.current_step_number, step_number) self.clear_routes(step_number=step_number) ########################################################################### # Step options dependent methods def update_protocol(self, protocol): app = get_app() for i, s in enumerate(protocol): step = app.protocol.steps[i] prevData = step.get_data(self.plugin_name) values = {} for k,v in prevData.iteritems(): if k in s: values[k] = s[k] step.set_data(self.plugin_name, values) emit_signal('on_step_options_changed', [self.plugin_name, i], interface=IPlugin) def add_route(self, electrode_ids): ''' Add droplet route. Args: electrode_ids (list) : Ordered list of identifiers of electrodes on route. ''' drop_routes = self.get_routes() route_i = (drop_routes.route_i.max() + 1 if drop_routes.shape[0] > 0 else 0) drop_route = (pd.DataFrame(electrode_ids, columns=['electrode_i']) .reset_index().rename(columns={'index': 'transition_i'})) drop_route.insert(0, 'route_i', route_i) drop_routes = drop_routes.append(drop_route, ignore_index=True) self.set_routes(drop_routes) return {'route_i': route_i, 'drop_routes': drop_routes} def clear_routes(self, electrode_id=None, step_number=None): ''' Clear all drop routes for protocol step that include the specified electrode (identified by string identifier). ''' step_options = self.get_step_options(step_number) if electrode_id is None: # No electrode identifier specified. Clear all step routes. df_routes = RouteController.default_routes() else: df_routes = step_options['drop_routes'] # Find indexes of all routes that include electrode. routes_to_clear = df_routes.loc[df_routes.electrode_i == electrode_id, 'route_i'] # Remove all routes that include electrode. df_routes = df_routes.loc[~df_routes.route_i .isin(routes_to_clear.tolist())].copy() step_options['drop_routes'] = df_routes self.set_step_values(step_options, step_number=step_number) self.get_routes() def get_routes(self, step_number=None): step_options = self.get_step_options(step_number=step_number) x = step_options.get('drop_routes', RouteController.default_routes()) msg = json.dumps(x, cls=PandasJsonEncoder) self.mqtt_client.publish('microdrop/droplet-planning-plugin/routes-set', msg, retain=True) return x def execute_routes(self, data): # TODO allow for passing of both electrode_id and route_i # Currently electrode_id only try: df_routes = self.get_routes() step_options = self.get_step_options() if 'transition_duration_ms' in data: transition_duration_ms = data['transition_duration_ms'] else: transition_duration_ms = step_options['transition_duration_ms'] if 'trail_length' in data: trail_length = data['trail_length'] else: trail_length = step_options['trail_length'] if 'route_i' in data: df_routes = df_routes.loc[df_routes.route_i == data['route_i']] elif 'electrode_i' in data: if data['electrode_i'] is not None: routes_to_execute = df_routes.loc[df_routes.electrode_i == data['electrode_i'], 'route_i'] df_routes = df_routes.loc[df_routes.route_i .isin(routes_to_execute .tolist())].copy() route_controller = RouteController(self) route_controller.execute_routes(df_routes, transition_duration_ms, trail_length=trail_length) except: logger.error(str(data), exc_info=True) def update_steps(self): app = get_app() num_steps = len(app.protocol.steps) protocol = [] for i in range(num_steps): protocol.append(self.get_step_options(i)) self.mqtt_client.publish('microdrop/droplet-planning-plugin/step-options', json.dumps(protocol, cls=PandasJsonEncoder), retain=True) def set_routes(self, df_routes, step_number=None): step_options = self.get_step_options(step_number=step_number) step_options['drop_routes'] = df_routes self.set_step_values(step_options, step_number=step_number)
class TestPlugin(Plugin, AppDataController, StepOptionsController): """ This class is automatically registered with the PluginManager. """ implements(IPlugin) version = get_plugin_info(path(__file__).parent).version plugins_name = get_plugin_info(path(__file__).parent).plugin_name ''' AppFields --------- A flatland Form specifying application options for the current plugin. Note that nested Form objects are not supported. Since we subclassed AppDataController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the app options dialog (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently in the microdrop config file, in a section named after this plugin's name attribute ''' serial_ports_ = [port for port in serial_device.get_serial_ports()] if len(serial_ports_): default_port_ = serial_ports_[0] else: default_port_ = None AppFields = Form.of( Enum.named('serial_port').using(default=default_port_, optional=True)\ .valued(*serial_ports_), ) ''' StepFields --------- A flatland Form specifying the per step options for the current plugin. Note that nested Form objects are not supported. Since we subclassed StepOptionsController, an API is available to access and modify these attributes. This API also provides some nice features automatically: -all fields listed here will be included in the protocol grid view (unless properties=dict(show_in_gui=False) is used) -the values of these fields will be stored persistently for each step ''' StepFields = Form.of( Boolean.named('led_on').using(default=False, optional=True), ) def __init__(self): self.name = self.plugins_name self.proxy = None def on_plugin_enable(self): # We need to call AppDataController's on_plugin_enable() to update the # application options data. AppDataController.on_plugin_enable(self) self.on_app_init() app_values = self.get_app_values() try: self.proxy = SerialProxy(port=app_values['serial_port']) self.proxy.pin_mode(pin=13, mode=1) logger.info('Connected to %s on port %s', self.proxy.properties.display_name, app_values['serial_port']) except Exception, e: logger.error('Could not connect to base-node-rpc on port %s: %s.', app_values['serial_port'], e) if get_app().protocol: pgc = get_service_instance(ProtocolGridController, env='microdrop') pgc.update_grid()
class 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')
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'])
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