From c27d9da79e4a93bf50d3f28c9aac08332889e256 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 31 Mar 2016 16:50:13 -0400 Subject: [PATCH 1/2] Add 0MQ plugin interface --- dstat_interface/interface/save.py | 84 +++--- dstat_interface/main.py | 417 +++++++++++++++--------------- dstat_interface/params.py | 40 +-- dstat_interface/plugin.py | 123 +++++++++ 4 files changed, 397 insertions(+), 267 deletions(-) create mode 100644 dstat_interface/plugin.py diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 0a033ec..73d9a80 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -1,20 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # DStat Interface - An interface for the open hardware DStat potentiostat -# Copyright (C) 2014 Michael D. M. Dryden - +# Copyright (C) 2014 Michael D. M. Dryden - # Wheeler Microfluidics Laboratory -# -# +# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see . @@ -32,22 +32,22 @@ def manSave(current_exp): fcd = gtk.FileChooserDialog("Save...", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK)) - + filters = [gtk.FileFilter()] filters[0].set_name("Space separated text (.txt)") filters[0].add_pattern("*.txt") - + fcd.set_do_overwrite_confirmation(True) for i in filters: fcd.add_filter(i) - + response = fcd.run() - + if response == gtk.RESPONSE_OK: path = fcd.get_filename() _logger.error(" ".join(("Selected filepath:", path)),'INFO') filter_selection = fcd.get_filter().get_name() - + if filter_selection.endswith("(.npy)"): if (current_exp.parameters['shutter_true'] and current_exp.parameters['sync_true']): npy(current_exp, current_exp.data, "-".join((path,'data'))) @@ -61,7 +61,7 @@ def manSave(current_exp): else: text(current_exp, current_exp.data, path, auto=True) fcd.destroy() - + elif response == gtk.RESPONSE_CANCEL: fcd.destroy() @@ -77,33 +77,33 @@ def plotSave(plots): filters.append(gtk.FileFilter()) filters[1].set_name("Portable Network Graphics (.png)") filters[1].add_pattern("*.png") - + fcd.set_do_overwrite_confirmation(True) for i in filters: fcd.add_filter(i) - + response = fcd.run() - + if response == gtk.RESPONSE_OK: path = fcd.get_filename() _logger.error(" ".join(("Selected filepath:", path)),'INFO') filter_selection = fcd.get_filter().get_name() - + for i in plots: path += '-' path += i - + if filter_selection.endswith("(.pdf)"): if not path.endswith(".pdf"): path += ".pdf" - + elif filter_selection.endswith("(.png)"): if not path.endswith(".png"): path += ".png" - + plots[i].figure.savefig(path) # determines format from file extension fcd.destroy() - + elif response == gtk.RESPONSE_CANCEL: fcd.destroy() @@ -114,28 +114,28 @@ def man_param_save(window): (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_OK) ) - + filters = [gtk.FileFilter()] filters[0].set_name("Parameter File (.yml)") filters[0].add_pattern("*.yml") - + fcd.set_do_overwrite_confirmation(True) for i in filters: fcd.add_filter(i) - + response = fcd.run() - + if response == gtk.RESPONSE_OK: path = fcd.get_filename() _logger.error(" ".join(("Selected filepath:", path)),'INFO') - + if not path.endswith(".yml"): path += '.yml' - + save_params(window, path) fcd.destroy() - + elif response == gtk.RESPONSE_CANCEL: fcd.destroy() @@ -146,24 +146,24 @@ def man_param_load(window): (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_OK) ) - + filters = [gtk.FileFilter()] filters[0].set_name("Parameter File (.yml)") filters[0].add_pattern("*.yml") for i in filters: fcd.add_filter(i) - + response = fcd.run() - + if response == gtk.RESPONSE_OK: path = fcd.get_filename() _logger.error(" ".join(("Selected filepath:", path)),'INFO') - + load_params(window, path) fcd.destroy() - + elif response == gtk.RESPONSE_CANCEL: fcd.destroy() @@ -175,7 +175,7 @@ def autoSave(current_exp, dir_button, name, expnumber): path += name path += '-' path += str(expnumber) - + if (current_exp.parameters['shutter_true'] and current_exp.parameters['sync_true']): text(current_exp, current_exp.data, "-".join((path,'data')), auto=True) text(current_exp, current_exp.ftdata, "-".join((path,'ft')), auto=True) @@ -186,7 +186,7 @@ def autoPlot(plots, dir_button, name, expnumber): for i in plots: if name == "": name = "file" - + path = dir_button.get_filename() path += '/' path += name @@ -194,17 +194,17 @@ def autoPlot(plots, dir_button, name, expnumber): path += str(expnumber) path += '-' path += i - + if path.endswith(".pdf"): path = path.rstrip(".pdf") - + j = 1 while os.path.exists("".join([path, ".pdf"])): if j > 1: path = path[:-len(str(j))] path += str(j) j += 1 - + path += ".pdf" plots[i].figure.savefig(path) @@ -225,25 +225,25 @@ def npy(exp, data, path, auto=False): def text(exp, data, path, auto=False): if path.endswith(".txt"): path = path.rstrip(".txt") - + if auto == True: j = 1 - + while os.path.exists("".join([path, ".txt"])): if j > 1: path = path[:-len(str(j))] path += str(j) j += 1 - + path += ".txt" file = open(path, 'w') - + time = exp.time header = "".join(['#', time.isoformat(), "\n#"]) for i in exp.commands: header += i - + try: if exp.ft_int: header += "\n" @@ -266,5 +266,5 @@ def text(exp, data, path, auto=False): for row in col: file.write(str(row)+ " ") file.write('\n') - + file.close() diff --git a/dstat_interface/main.py b/dstat_interface/main.py index a71bede..54c5f66 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -1,20 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # DStat Interface - An interface for the open hardware DStat potentiostat -# Copyright (C) 2014 Michael D. M. Dryden - +# Copyright (C) 2014 Michael D. M. Dryden - # Wheeler Microfluidics Laboratory -# -# +# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see . @@ -23,7 +23,8 @@ import sys import os import multiprocessing -import time +import uuid +from collections import OrderedDict from datetime import datetime try: @@ -43,7 +44,6 @@ except ImportError: print "ERR: gobject not available" sys.exit(1) from serial import SerialException -from datetime import datetime os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) @@ -53,12 +53,15 @@ import dstat_comm as comm import interface.exp_window as exp_window import interface.adc_pot as adc_pot import plot -import microdrop import params import parameter_test -from errors import InputError, VarError, ErrorLogger +import zmq +from errors import InputError, ErrorLogger _logger = ErrorLogger(sender="dstat-interface-main") +from plugin import DstatPlugin, get_hub_uri + + class Main(object): """Main program """ def __init__(self): @@ -77,32 +80,32 @@ class Main(object): self.stopbutton = self.builder.get_object('pot_stop') self.startbutton = self.builder.get_object('pot_start') self.adc_pot = adc_pot.adc_pot() - + self.error_context_id = self.statusbar.get_context_id("error") self.message_context_id = self.statusbar.get_context_id("message") - + self.plotwindow = self.builder.get_object('plotbox') self.ft_window = self.builder.get_object('ft_box') self.period_window = self.builder.get_object('period_box') - + self.exp_window = exp_window.Experiments(self.builder) - + # Setup Autosave self.autosave_checkbox = self.builder.get_object('autosave_checkbutton') self.autosavedir_button = self.builder.get_object('autosavedir_button') self.autosavename = self.builder.get_object('autosavename') - + # Setup Plots self.plot_notebook = self.builder.get_object('plot_notebook') - + self.plot = plot.plotbox(self.plotwindow) self.ft_plot = plot.ft_box(self.ft_window) - + #fill adc_pot_box self.adc_pot_box = self.builder.get_object('gain_adc_box') self.adc_pot_container = self.adc_pot.builder.get_object('vbox1') self.adc_pot_container.reparent(self.adc_pot_box) - + #fill serial self.serial_connect = self.builder.get_object('serial_connect') self.serial_pmt_connect = self.builder.get_object('pmt_mode') @@ -111,25 +114,25 @@ class Main(object): self.serial_combobox = self.builder.get_object('serial_combobox') self.serial_combobox.pack_start(self.cell, True) self.serial_combobox.add_attribute(self.cell, 'text', 0) - + self.serial_liststore = self.builder.get_object('serial_liststore') self.serial_devices = comm.SerialDevices() - + for i in self.serial_devices.ports: self.serial_liststore.append([i]) - + self.serial_combobox.set_active(0) - + #initialize experiment selection combobox self.expcombobox = self.builder.get_object('expcombobox') self.expcombobox.pack_start(self.cell, True) self.expcombobox.add_attribute(self.cell, 'text', 2) self.expcombobox.set_active(0) - + self.spinner = self.builder.get_object('spinner') self.mainwindow = self.builder.get_object('window1') - + # Set Version Strings try: ver = getVersion() @@ -138,30 +141,36 @@ class Main(object): _logger.error("Could not fetch version number", "WAR") self.mainwindow.set_title(" ".join(("DStat Interface", ver))) self.aboutdialog.set_version(ver) - + self.mainwindow.show_all() - + self.on_expcombobox_changed() self.expnumber = 0 - + self.connected = False self.pmt_mode = False - + self.menu_dropbot_connect = self.builder.get_object( 'menu_dropbot_connect') self.menu_dropbot_disconnect = self.builder.get_object( 'menu_dropbot_disconnect') self.dropbot_enabled = False self.dropbot_triggered = False - + self.plot_notebook.get_nth_page( self.plot_notebook.page_num(self.ft_window)).hide() self.plot_notebook.get_nth_page( self.plot_notebook.page_num(self.period_window)).hide() - + self.params_loaded = False - + # Disable 0MQ plugin API by default. + self.plugin = None + self.plugin_timeout_id = None + # UUID for active experiment. + self.active_experiment_id = None + # UUIDs for completed experiments. + self.completed_experiment_ids = OrderedDict() def on_window1_destroy(self, object, data=None): """ Quit when main window closed.""" @@ -170,11 +179,11 @@ class Main(object): def on_gtk_quit_activate(self, menuitem, data=None): """Quit when Quit selected from menu.""" self.quit() - + def quit(self): """Disconnect and save parameters on quit.""" params.save_params(self, 'last_params.yml') - + self.on_serial_disconnect_clicked() gtk.main_quit() @@ -196,25 +205,25 @@ class Main(object): """Refresh list of serial devices.""" self.serial_devices.refresh() self.serial_liststore.clear() - + for i in self.serial_devices.ports: self.serial_liststore.append([i]) - + def on_serial_connect_clicked(self, data=None): """Connect and retrieve DStat version.""" - + try: self.serial_connect.set_sensitive(False) self.version = comm.version_check(self.serial_liststore.get_value( self.serial_combobox.get_active_iter(), 0)) - + self.statusbar.remove_all(self.error_context_id) - + if not len(self.version) == 2: self.statusbar.push(self.error_context_id, "Communication Error") return - + else: self.adc_pot.set_version(self.version) self.statusbar.push(self.error_context_id, @@ -222,33 +231,33 @@ class Main(object): str(self.version[0]), ".", str(self.version[1])]) ) - + comm.read_settings() - + self.start_ocp() self.connected = True self.serial_connect.set_sensitive(False) self.serial_pmt_connect.set_sensitive(False) self.serial_disconnect.set_sensitive(True) - + except AttributeError as err: _logger.error(err, 'WAR') self.serial_connect.set_sensitive(True) except TypeError as err: _logger.error(err, 'WAR') self.serial_connect.set_sensitive(True) - + if self.params_loaded == False: try: params.load_params(self, 'last_params.yml') except IOError: _logger.error("No previous parameters found.", 'INFO') - + def on_serial_disconnect_clicked(self, data=None): """Disconnect from DStat.""" if self.connected == False: return - + try: if self.ocp_is_running: self.stop_ocp() @@ -256,18 +265,18 @@ class Main(object): self.on_pot_stop_clicked() comm.serial_instance.ctrl_pipe_p.send("DISCONNECT") comm.serial_instance.proc.terminate() - + except AttributeError as err: _logger.error(err, 'WAR') pass - + self.pmt_mode = False self.connected = False self.serial_connect.set_sensitive(True) self.serial_pmt_connect.set_sensitive(True) self.serial_disconnect.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) - + def on_pmt_mode_clicked(self, data=None): """Connect in PMT mode""" self.pmt_mode = True @@ -277,29 +286,29 @@ class Main(object): def start_ocp(self): """Start OCP measurements.""" - + if self.version[0] >= 1 and self.version[1] >= 2: # Flush data pipe while comm.serial_instance.data_pipe_p.poll(): comm.serial_instance.data_pipe_p.recv() - + if self.pmt_mode == True: _logger.error("Start PMT idle mode", "INFO") comm.serial_instance.proc_pipe_p.send(comm.PMTIdle()) - + else: _logger.error("Start OCP", "INFO") comm.serial_instance.proc_pipe_p.send(comm.OCPExp()) - + self.ocp_proc = (gobject.timeout_add(300, self.ocp_running_data), gobject.timeout_add(250, self.ocp_running_proc) ) self.ocp_is_running = True - + else: _logger.error("OCP measurements not supported on v1.1 boards.",'INFO') return - + def stop_ocp(self): """Stop OCP measurements.""" @@ -317,37 +326,38 @@ class Main(object): self.ocp_is_running = False self.ocp_disp.set_text("") else: - logger.error("OCP measurements not supported on v1.1 boards.",'INFO') + _logger.error("OCP measurements not supported on v1.1 boards.", + 'INFO') return - + def ocp_running_data(self): """Receive OCP value from experiment process and update ocp_disp field - + Returns: True -- when experiment is continuing to keep function in GTK's queue. False -- when experiment process signals EOFError or IOError to remove function from GTK's queue. """ - + try: - if comm.serial_instance.data_pipe_p.poll(): + if comm.serial_instance.data_pipe_p.poll(): incoming = comm.serial_instance.data_pipe_p.recv() - + if isinstance(incoming, basestring): # test if incoming is str self.on_serial_disconnect_clicked() return False - + data = "".join(["OCP: ", "{0:.3f}".format(incoming), " V"]) self.ocp_disp.set_text(data) - + if comm.serial_instance.data_pipe_p.poll(): self.ocp_running_data() return True - + return True - + except EOFError: return False except IOError: @@ -355,58 +365,64 @@ class Main(object): def ocp_running_proc(self): """Handles signals on proc_pipe_p for OCP. - + Returns: True -- when experiment is continuing to keep function in GTK's queue. False -- when experiment process signals EOFError or IOError to remove function from GTK's queue. """ - + try: - if comm.serial_instance.proc_pipe_p.poll(): + if comm.serial_instance.proc_pipe_p.poll(): proc_buffer = comm.serial_instance.proc_pipe_p.recv() _logger.error("".join(("ocp_running_proc: ", proc_buffer)), 'DBG') - if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: + if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: if proc_buffer == "SERIAL_ERROR": self.on_serial_disconnect_clicked() - + while comm.serial_instance.data_pipe_p.poll(): comm.serial_instance.data_pipe_p.recv() - + gobject.source_remove(self.ocp_proc[0]) return False - + return True - + return True - + except EOFError: return False except IOError: return False - + def on_pot_start_clicked(self, data=None): + try: + self.run_active_experiment() + except (ValueError, KeyError, InputError, SerialException, + AssertionError): + # Ignore expected exceptions when triggering experiment from UI. + pass + + def run_active_experiment(self): """Run currently visible experiment.""" + # Assign current experiment a unique identifier. + experiment_id = uuid.uuid4() + self.active_experiment_id = uuid.uuid4() + def exceptions(): """ Cleans up after errors """ - if self.dropbot_enabled == True: - if self.dropbot_triggered == True: - self.dropbot_triggered = False - self.microdrop.reply(microdrop.EXPFINISHED) - self.microdrop_proc = gobject.timeout_add(500, - self.microdrop_listen) self.spinner.stop() self.startbutton.set_sensitive(True) self.stopbutton.set_sensitive(False) self.start_ocp() - + def run_experiment(): """ Starts experiment """ self.plot.clearall() self.plot.changetype(self.current_exp) - + nb = self.plot_notebook - + if (parameters['sync_true'] and parameters['shutter_true']): nb.get_nth_page( nb.page_num(self.ft_window)).show() @@ -419,7 +435,7 @@ class Main(object): # nb.get_nth_page(nb.page_num(self.period_window)).hide() comm.serial_instance.proc_pipe_p.send(self.current_exp) - + # Flush data pipe while comm.serial_instance.data_pipe_p.poll(): comm.serial_instance.data_pipe_p.recv() @@ -430,18 +446,17 @@ class Main(object): gobject.idle_add(self.experiment_running_data), gobject.idle_add(self.experiment_running_proc) ) - - + self.stop_ocp() self.statusbar.remove_all(self.error_context_id) - + while comm.serial_instance.data_pipe_p.poll(): # Clear data pipe comm.serial_instance.data_pipe_p.recv() - + selection = self.expcombobox.get_active() parameters = {} parameters['version'] = self.version - + # Make sure these are defined parameters['sync_true'] = False parameters['shutter_true'] = False @@ -451,7 +466,7 @@ class Main(object): self.line = 0 self.lastline = 0 self.lastdataline = 0 - + self.spinner.start() self.startbutton.set_sensitive(False) self.stopbutton.set_sensitive(True) @@ -463,131 +478,134 @@ class Main(object): if not parameters['potential']: raise InputError(parameters['potential'], "Step table is empty") - - + self.current_exp = comm.Chronoamp(parameters) - + self.rawbuffer.set_text("") self.rawbuffer.place_cursor(self.rawbuffer.get_start_iter()) - + for i in self.current_exp.commands: self.rawbuffer.insert_at_cursor(i) - + run_experiment() - - return - + + return experiment_id + elif selection == 1: # LSV parameters.update(self.exp_window.get_params('lsv')) parameter_test.lsv_test(parameters) - + self.current_exp = comm.LSVExp(parameters) run_experiment() - return - + return experiment_id + elif selection == 2: # CV parameters.update(self.exp_window.get_params('cve')) - parameter_test.cv_test(parameters) - + parameter_test.cv_test(parameters) + self.current_exp = comm.CVExp(parameters) run_experiment() - - return - + + return experiment_id + elif selection == 3: # SWV parameters.update(self.exp_window.get_params('swv')) parameter_test.swv_test(parameters) - + self.current_exp = comm.SWVExp(parameters) run_experiment() - - return - + + return experiment_id + elif selection == 4: # DPV parameters.update(self.exp_window.get_params('dpv')) parameter_test.dpv_test(parameters) - + self.current_exp = comm.DPVExp(parameters) run_experiment() - - return - - elif selection == 6: # PD + + return experiment_id + + elif selection == 6: # PD parameters.update(self.exp_window.get_params('pde')) parameter_test.pd_test(parameters) - + self.current_exp = comm.PDExp(parameters) run_experiment() - - return - + + return experiment_id + elif selection == 7: # POT if not (self.version[0] >= 1 and self.version[1] >= 2): - self.statusbar.push(self.error_context_id, + self.statusbar.push(self.error_context_id, "v1.1 board does not support potentiometry.") exceptions() return - + parameters.update(self.exp_window.get_params('pot')) parameter_test.pot_test(parameters) - + self.current_exp = comm.PotExp(parameters) run_experiment() - - return - + + return experiment_id + else: - self.statusbar.push(self.error_context_id, + self.statusbar.push(self.error_context_id, "Experiment not yet implemented.") exceptions() - + except ValueError as i: _logger.error(i, "INFO") - self.statusbar.push(self.error_context_id, + self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.") exceptions() - + raise + except KeyError as i: _logger.error("KeyError: %s" % i, "INFO") self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.") exceptions() - + raise + except InputError as err: _logger.error(err, "INFO") self.statusbar.push(self.error_context_id, err.msg) exceptions() - + raise + except SerialException as err: _logger.error(err, "INFO") - self.statusbar.push(self.error_context_id, + self.statusbar.push(self.error_context_id, "Could not establish serial connection.") exceptions() + raise except AssertionError as err: _logger.error(err, "INFO") self.statusbar.push(self.error_context_id, str(err)) exceptions() - + raise def experiment_running_data(self): """Receive data from experiment process and add to current_exp.data. Run in GTK main loop. - + Returns: True -- when experiment is continuing to keep function in GTK's queue. False -- when experiment process signals EOFError or IOError to remove function from GTK's queue. """ try: - if comm.serial_instance.data_pipe_p.poll(): + if comm.serial_instance.data_pipe_p.poll(): incoming = comm.serial_instance.data_pipe_p.recv() # if isinstance(incoming, basestring): # Test if incoming is str # self.experiment_done() # self.on_serial_disconnect_clicked() # return False - + self.line, data = incoming if self.line > self.lastdataline: self.current_exp.data += [[], []] @@ -602,7 +620,7 @@ class Main(object): if comm.serial_instance.data_pipe_p.poll(): self.experiment_running_data() return True - + return True except EOFError as err: @@ -613,32 +631,32 @@ class Main(object): print err self.experiment_done() return False - + def experiment_running_proc(self): """Receive proc signals from experiment process. Run in GTK main loop. - + Returns: True -- when experiment is continuing to keep function in GTK's queue. False -- when experiment process signals EOFError or IOError to remove function from GTK's queue. """ try: - if comm.serial_instance.proc_pipe_p.poll(): + if comm.serial_instance.proc_pipe_p.poll(): proc_buffer = comm.serial_instance.proc_pipe_p.recv() - + if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: self.experiment_done() if proc_buffer == "SERIAL_ERROR": self.on_serial_disconnect_clicked() - + else: e = "Unrecognized experiment return code " e += proc_buffer _logger.error(e, 'WAR') - + return False - + return True except EOFError as err: @@ -649,7 +667,7 @@ class Main(object): _logger.error(err, 'WAR') self.experiment_done() return False - + def experiment_running_plot(self): """Plot all data in current_exp.data. Run in GTK main loop. Always returns True so must be manually @@ -658,7 +676,7 @@ class Main(object): if self.line > self.lastline: self.plot.addline() # make sure all of last line is added - self.plot.updateline(self.current_exp, self.lastline) + self.plot.updateline(self.current_exp, self.lastline) self.lastline = self.line self.plot.updateline(self.current_exp, self.line) self.plot.redraw() @@ -672,10 +690,10 @@ class Main(object): gobject.source_remove(self.experiment_proc[0]) gobject.source_remove(self.plot_proc) # stop automatic plot update self.experiment_running_plot() # make sure all data updated on plot - + if (self.current_exp.parameters['shutter_true'] and self.current_exp.parameters['sync_true']): - self.ft_plot.updateline(self.current_exp, 0) + self.ft_plot.updateline(self.current_exp, 0) self.ft_plot.redraw() self.current_exp.data_extra = self.current_exp.ftdata self.statusbar.push( @@ -699,37 +717,32 @@ class Main(object): for row in col: self.rawbuffer.insert_at_cursor(str(row)+ " ") self.rawbuffer.insert_at_cursor("\n") - + if self.current_exp.data_extra: for col in zip(*self.current_exp.data_extra): for row in col: self.databuffer.insert_at_cursor(str(row)+ " ") self.databuffer.insert_at_cursor("\n") - + if self.autosave_checkbox.get_active(): save.autoSave(self.current_exp, self.autosavedir_button, self.autosavename.get_text(), self.expnumber) plots = {'data':self.plot} - + if (self.current_exp.parameters['shutter_true'] and self.current_exp.parameters['sync_true']): plots['ft'] = self.ft_plot - + save.autoPlot(plots, self.autosavedir_button, self.autosavename.get_text(), self.expnumber) self.expnumber += 1 - - if self.dropbot_enabled == True: - if self.dropbot_triggered == True: - self.dropbot_triggered = False - self.microdrop.reply(microdrop.EXPFINISHED) - self.microdrop_proc = gobject.timeout_add(500, - self.microdrop_listen) - + self.spinner.stop() self.startbutton.set_sensitive(True) self.stopbutton.set_sensitive(False) self.start_ocp() + self.completed_experiment_ids[self.active_experiment_id] =\ + datetime.utcnow() def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" @@ -740,88 +753,80 @@ class Main(object): pass except: _logger.error(sys.exc_info(),'WAR') - + def on_file_save_exp_activate(self, menuitem, data=None): """Activate dialogue to save current experiment data. """ if self.current_exp: save.manSave(self.current_exp) - + def on_file_save_plot_activate(self, menuitem, data=None): """Activate dialogue to save current plot.""" plots = {'data':self.plot} - + if (self.current_exp.parameters['shutter_true'] and self.current_exp.parameters['sync_true']): plots['ft'] = self.ft_plot - + save.plotSave(plots) - + def on_file_save_params_activate(self, menuitem, data=None): """Activate dialogue to save current experiment parameters. """ save.man_param_save(self) - + def on_file_load_params_activate(self, menuitem, data=None): """Activate dialogue to load experiment parameters from file. """ save.man_param_load(self) - + def on_menu_dropbot_connect_activate(self, menuitem, data=None): """Listen for remote control connection from µDrop.""" - self.microdrop = microdrop.microdropConnection() + + # Prompt user for 0MQ plugin hub URI. + zmq_plugin_hub_uri = get_hub_uri(parent=self.window) + self.dropbot_enabled = True self.menu_dropbot_connect.set_sensitive(False) self.menu_dropbot_disconnect.set_sensitive(True) self.statusbar.push(self.message_context_id, "Waiting for µDrop to connect…") - self.microdrop_proc = gobject.timeout_add(500, self.microdrop_listen) - + self.enable_plugin(zmq_plugin_hub_uri) + def on_menu_dropbot_disconnect_activate(self, menuitem=None, data=None): """Disconnect µDrop connection and stop listening.""" - gobject.source_remove(self.microdrop_proc) - self.microdrop.reset() - del self.microdrop + self.cleanup_plugin() self.dropbot_enabled = False self.menu_dropbot_connect.set_sensitive(True) self.menu_dropbot_disconnect.set_sensitive(False) self.statusbar.push(self.message_context_id, "µDrop disconnected.") - def microdrop_listen(self): - """Manage signals from µDrop. Must be added to GTK's main loop to - run periodically. - """ - drdy, data = self.microdrop.listen() - if drdy == False: - return True + def enable_plugin(self, hub_uri): + ''' + Connect to 0MQ plugin hub to expose public D-Stat API. - if data == microdrop.EXP_FINISH_REQ: - if self.dropbot_triggered: - if self.connected: - self.on_pot_start_clicked() - else: - _logger.error("µDrop requested experiment but DStat disconnected", - 'WAR') - self.statusbar.push(self.message_context_id, - "Listen stopped—DStat disconnected.") - self.microdrop.reply(microdrop.EXPFINISHED) - self.on_menu_dropbot_disconnect_activate() - return False # Removes function from GTK's main loop - else: - _logger.error("µDrop requested experiment finish confirmation without starting experiment.", - 'WAR') - self.microdrop.reply(microdrop.EXPFINISHED) - - elif data == microdrop.STARTEXP: - self.microdrop.connected = True - self.statusbar.push(self.message_context_id, "µDrop connected.") - self.dropbot_triggered = True - self.microdrop.reply(microdrop.START_REP) - else: - _logger.error("Received invalid command from µDrop",'WAR') - self.microdrop.reply(microdrop.INVAL_CMD) - return True + Args + ---- + + hub_uri (str) : URI for 0MQ plugin hub. + ''' + self.cleanup_plugin() + # Initialize 0MQ hub plugin and subscribe to hub messages. + self.plugin = DstatPlugin(self, 'dstat-interface', 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(500, + self.plugin.check_sockets) + + 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 __name__ == "__main__": multiprocessing.freeze_support() gobject.threads_init() MAIN = Main() - gtk.main() \ No newline at end of file + gtk.main() diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 565fe4b..4dc0d53 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -1,20 +1,20 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # DStat Interface - An interface for the open hardware DStat potentiostat -# Copyright (C) 2014 Michael D. M. Dryden - +# Copyright (C) 2014 Michael D. M. Dryden - # Wheeler Microfluidics Laboratory -# -# +# +# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program. If not, see . @@ -25,51 +25,53 @@ _logger = ErrorLogger(sender="dstat-interface-params") def get_params(window): """Fetches and returns dict of all parameters for saving.""" - + parameters = {} - + selection = window.exp_window.select_to_key[window.expcombobox.get_active()] parameters['experiment_index'] = selection - + try: parameters['version'] = window.version except AttributeError: # Will be thrown if not connected to DStat pass - + try: parameters.update(window.adc_pot.params) except InputError: _logger.error("No gain selected.", 'INFO') parameters.update(window.exp_window.get_params(selection)) - + return parameters def save_params(window, path): """Fetches current params and saves to path.""" - + params = get_params(window) - + with open(path, 'w') as f: yaml.dump(params, f) - + def load_params(window, path): """Loads params from a path into UI elements.""" - + try: get_params(window) except InputError: # Will be thrown because no experiment will be selected pass - + with open(path, 'r') as f: params = yaml.load(f) - + set_params(window, params) + +def set_params(window, params): window.adc_pot.params = params - + if not 'experiment_index' in params: _logger.error("Missing experiment parameters.", 'WAR') return window.expcombobox.set_active( window.exp_window.classes[params['experiment_index']][0]) window.exp_window.set_params(params['experiment_index'], params) - - window.params_loaded = True \ No newline at end of file + + window.params_loaded = True diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py new file mode 100644 index 0000000..b1d1f23 --- /dev/null +++ b/dstat_interface/plugin.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +import logging + +from params import get_params, set_params, load_params, save_params +from zmq_plugin.plugin import Plugin as ZmqPlugin +from zmq_plugin.schema import decode_content_data +import gtk +import zmq + +logger = logging.getLogger(__name__) + + +def get_hub_uri(default='tcp://localhost:31000', parent=None): + message = 'Please enter 0MQ hub URI:' + d = gtk.MessageDialog(parent=parent, flags=gtk.DIALOG_MODAL | + gtk.DIALOG_DESTROY_WITH_PARENT, + type=gtk.MESSAGE_INFO, buttons=gtk.BUTTONS_OK_CANCEL, + message_format=message) + entry = gtk.Entry() + entry.set_text(default) + d.vbox.pack_end(entry) + d.vbox.show_all() + entry.connect('activate', lambda _: d.response(gtk.RESPONSE_OK)) + d.set_default_response(gtk.RESPONSE_OK) + + r = d.run() + text = entry.get_text().decode('utf8') + d.destroy() + if r == gtk.RESPONSE_OK: + return text + else: + return None + + +class DstatPlugin(ZmqPlugin): + ''' + Public 0MQ plugin API. + ''' + def __init__(self, parent, *args, **kwargs): + self.parent = parent + super(DstatPlugin, self).__init__(*args, **kwargs) + + def check_sockets(self): + ''' + Check for messages on command and subscription sockets and process + any messages accordingly. + ''' + try: + msg_frames = self.command_socket.recv_multipart(zmq.NOBLOCK) + except zmq.Again: + pass + else: + self.on_command_recv(msg_frames) + + try: + msg_frames = self.subscribe_socket.recv_multipart(zmq.NOBLOCK) + source, target, msg_type, msg_json = msg_frames + self.most_recent = msg_json + except zmq.Again: + pass + except: + logger.error('Error processing message from subscription ' + 'socket.', exc_info=True) + return True + + def on_execute__load_params(self, request): + ''' + Args + ---- + + params_path (str) : Path to file for parameters yaml file. + ''' + data = decode_content_data(request) + load_params(self.parent, data['params_path']) + + def on_execute__save_params(self, request): + ''' + Args + ---- + + params_path (str) : Path to file for parameters yaml file. + ''' + data = decode_content_data(request) + save_params(self.parent, data['params_path']) + + def on_execute__set_params(self, request): + ''' + Args + ---- + + (dict) : Parameters dictionary in format returned by `get_params`. + ''' + data = decode_content_data(request) + set_params(self.parent, data['params']) + + def on_execute__get_params(self, request): + return get_params(self.parent) + + def on_execute__run_active_experiment(self, request): + self.parent.statusbar.push(self.parent.message_context_id, "µDrop " + "acquisition requested.") + return self.parent.run_active_experiment() + + def on_execute__acquisition_complete(self, request): + ''' + Args + ---- + + Returns + ------- + + (datetime.datetime or None) : The completion time of the experiment + corresponding to the specified UUID. + ''' + data = decode_content_data(request) + self.parent.statusbar.push(self.parent.message_context_id, "µDrop " + "notified of completed acquisition.") + if data['experiment_id'] in self.parent.completed_experiment_ids: + return self.parent.completed_experiment_ids[data['experiment_id']] + elif data['experiment_id'] == self.parent.active_experiment_id: + return None + else: + raise KeyError('Unknown experiment ID: %s' % data['experiment_id']) -- GitLab From b175d7d6204073d0703b32de591e68395f8fa7f4 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 31 Mar 2016 16:52:03 -0400 Subject: [PATCH 2/2] Add zmq-plugin as required package --- pavement.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pavement.py b/pavement.py index 86235fd..0aea7f5 100644 --- a/pavement.py +++ b/pavement.py @@ -17,14 +17,14 @@ setup(name='dstat_interface', url='http://microfluidics.utoronto.ca/dstat', license='GPLv3', packages=['dstat_interface', ], - install_requires=['matplotlib', 'numpy', 'pyserial', - 'pyzmq', 'pyyaml','seaborn'], + install_requires=['matplotlib', 'numpy', 'pyserial', 'pyzmq', + 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2'], # Install data listed in `MANIFEST.in` include_package_data=True) @task -@needs('generate_setup', 'minilib', 'setuptools.command.sdist') +@needs('generate_setup', 'minilib', 'setuptools.command.sdist') def sdist(): """Overrides sdist to make sure that our setup.py is generated.""" pass -- GitLab