From c27d9da79e4a93bf50d3f28c9aac08332889e256 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 31 Mar 2016 16:50:13 -0400 Subject: [PATCH 01/10] 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 02/10] 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 From 23e361d8cab58ac72b4b3d98aa1a855eaaea5fb4 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Mon, 4 Apr 2016 21:10:57 -0400 Subject: [PATCH 03/10] Moved all main and dstat_comm logging to native python logging. See #18 --- dstat_interface/dstat_comm.py | 92 +++++++++++++++-------------------- dstat_interface/main.py | 65 ++++++++++++++----------- 2 files changed, 76 insertions(+), 81 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index d002e00..e4aef62 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -22,13 +22,20 @@ from serial.tools import list_ports import time import struct import multiprocessing as mp -from errors import InputError, VarError, ErrorLogger -_logger = ErrorLogger(sender="dstat_comm") +import logging + +from errors import InputError, VarError + +logger = logging.getLogger("dstat.comm") +dstat_logger = logging.getLogger("dstat.comm.DSTAT") +exp_logger = logging.getLogger("dstat.comm.Experiment") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): + ser_logger = logging.getLogger("dstat.comm._serial_process") + ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) - _logger.error("_serial_process() Connecting", 'INFO') + ser_logger.info("Connecting") ser.write("ck") @@ -50,23 +57,20 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): if ctrl_buffer == ('a' or "DISCONNECT"): proc_pipe.send("ABORT") ser.write('a') - _logger.error("_serial_process(): ABORT", 'INFO') + ser_logger.info("ABORT") if ctrl_buffer == "DISCONNECT": - _logger.error("_serial_process(): DISCONNECT", 'INFO') + ser_logger.info("DISCONNECT") ser.close() proc_pipe.send("DISCONNECT") return False - elif proc_pipe.poll(): while ctrl_pipe.poll(): ctrl_pipe.recv() return_code = proc_pipe.recv().run(ser, ctrl_pipe, data_pipe) - e = "_serial_process: " - e += str(return_code) - _logger.error(e,'INFO') + ser_logger.info('Return code: %s', str(return_code)) proc_pipe.send(return_code) @@ -104,27 +108,24 @@ class VersionCheck: if line.startswith('V'): input = line.lstrip('V') elif line.startswith("#"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "INFO") + dstat_logger.info(line.lstrip().rstrip()) elif line.lstrip().startswith("no"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "DBG") + dstat_logger.debug(line.lstrip().rstrip()) ser.flushInput() break parted = input.rstrip().split('.') - e = "DStat PCB version: " + e = "PCB version: " e += str(input.rstrip()) - _logger.error(e, "INFO") + dstat_logger.info(e) data_pipe.send((int(parted[0]), int(parted[1]))) status = "DONE" except UnboundLocalError as e: - _logger.error(e, "ERR") status = "SERIAL_ERROR" except SerialException as e: - _logger.error(e, "ERR") + logger.error('SerialException: %s', e) status = "SERIAL_ERROR" finally: @@ -147,7 +148,7 @@ def version_check(ser_port): buffer = 1 else: buffer = serial_instance.data_pipe_p.recv() - _logger.error("version_check done", "DBG") + logger.debug("version_check done") return buffer @@ -191,11 +192,9 @@ class Settings: if line.lstrip().startswith('S'): input = line.lstrip().lstrip('S') elif line.lstrip().startswith("#"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "INFO") + dstat_logger.info(line.lstrip().rstrip()) elif line.lstrip().startswith("no"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "DBG") + dstat_logger.debug(line.lstrip().rstrip()) self.ser.flushInput() break @@ -240,8 +239,7 @@ def read_settings(): serial_instance.proc_pipe_p.send(Settings(task='r')) settings = serial_instance.data_pipe_p.recv() - _logger.error("".join(("read_settings: ", - serial_instance.proc_pipe_p.recv())),'DBG') + logger.debug("read_settings: %s", serial_instance.proc_pipe_p.recv()) return @@ -254,8 +252,7 @@ def write_settings(): serial_instance.proc_pipe_p.send(Settings(task='w', settings=settings)) - _logger.error("".join(("write_settings: ", - serial_instance.proc_pipe_p.recv())),'DBG') + logger.debug("write_settings: %s", serial_instance.proc_pipe_p.recv()) return @@ -281,11 +278,9 @@ class LightSensor: if line.lstrip().startswith('T'): input = line.lstrip().lstrip('T') elif line.lstrip().startswith("#"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "INFO") + dstat_logger.info(line.lstrip().rstrip()) elif line.lstrip().startswith("no"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "DBG") + dstat_logger.debug(line.lstrip().rstrip()) ser.flushInput() break @@ -306,8 +301,7 @@ def read_light_sensor(): serial_instance.proc_pipe_p.send(LightSensor()) - _logger.error("".join(("read_light_sensor: ", - serial_instance.proc_pipe_p.recv())),'DBG') + logger.info("read_light_sensor: %s", serial_instance.proc_pipe_p.recv()) return serial_instance.data_pipe_p.recv() @@ -328,7 +322,7 @@ class SerialDevices(object): self.ports, _, _ = zip(*list_ports.comports()) except ValueError: self.ports = [] - _logger.error("No serial ports found", "ERR") + logger.error("No serial ports found") def refresh(self): """Refreshes list of ports.""" @@ -392,14 +386,14 @@ class Experiment(object): self.ctrl_pipe = ctrl_pipe self.data_pipe = data_pipe - _logger.error("Experiment running", "INFO") + exp_logger.info("Experiment running") try: self.serial.flushInput() status = "DONE" for i in self.commands: - _logger.error("".join(("Command: ",i)), "INFO") + logger.info("Command: %s", i) self.serial.write('!') while not self.serial.read().startswith("C"): @@ -427,17 +421,17 @@ class Experiment(object): while True: if self.ctrl_pipe.poll(): input = self.ctrl_pipe.recv() - _logger.error("".join(("serial_handler: ", input)),"DBG") + logger.debug("serial_handler: %s", input) if input == ('a' or "DISCONNECT"): self.serial.write('a') - _logger.error("serial_handler: ABORT pressed!","INFO") + logger.info("serial_handler: ABORT pressed!") return False for line in self.serial: if self.ctrl_pipe.poll(): if self.ctrl_pipe.recv() == 'a': self.serial.write('a') - _logger.error("serial_handler: ABORT pressed!","INFO") + logger.info("serial_handler: ABORT pressed!") return False if line.startswith('B'): @@ -449,12 +443,10 @@ class Experiment(object): scan += 1 elif line.lstrip().startswith("#"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "INFO") + dstat_logger.info(line.lstrip().rstrip()) elif line.lstrip().startswith("no"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "DBG") + dstat_logger.debug(line.lstrip().rstrip()) self.serial.flushInput() return True @@ -509,17 +501,17 @@ class CALExp(Experiment): while True: if self.ctrl_pipe.poll(): input = self.ctrl_pipe.recv() - _logger.error("".join(("serial_handler: ", input))) + logger.debug("serial_handler: %s", input) if input == ('a' or "DISCONNECT"): self.serial.write('a') - _logger.error("serial_handler: ABORT pressed!","INFO") + logger.info("serial_handler: ABORT pressed!") return False for line in self.serial: if self.ctrl_pipe.poll(): if self.ctrl_pipe.recv() == 'a': self.serial.write('a') - _logger.error("serial_handler: ABORT pressed!","INFO") + logger.info("serial_handler: ABORT pressed!") return False if line.startswith('B'): @@ -527,12 +519,10 @@ class CALExp(Experiment): self.serial.read(size=self.databytes))) elif line.lstrip().startswith("#"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "INFO") + dstat_logger.info(line.lstrip().rstrip()) elif line.lstrip().startswith("no"): - _logger.error("".join( - ("DSTAT: ",line.lstrip().rstrip())), "DBG") + dstat_logger.debug(line.lstrip().rstrip()) self.serial.flushInput() return True @@ -889,9 +879,7 @@ def measure_offset(time): for i in range(1,8): parameters['gain'] = i serial_instance.proc_pipe_p.send(CALExp(parameters)) - _logger.error("".join( - ("measure_offset: ", serial_instance.proc_pipe_p.recv())), - "INFO") + logger.info("measure_offset: %s", serial_instance.proc_pipe_p.recv()) gain_offset[gain_trim_table[i]] = serial_instance.data_pipe_p.recv() return gain_offset \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index e482f85..bc2af93 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -45,7 +45,7 @@ except ImportError: print "ERR: gobject not available" sys.exit(1) from serial import SerialException - +import logging os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) from version import getVersion @@ -58,11 +58,22 @@ import params import parameter_test import analysis import zmq -from errors import InputError, ErrorLogger -_logger = ErrorLogger(sender="dstat-interface-main") +from errors import InputError from plugin import DstatPlugin, get_hub_uri +# Setup Logging +root_logger = logging.getLogger("dstat") +root_logger.setLevel(level=logging.INFO) +log_handler = logging.StreamHandler() +log_formatter = logging.Formatter( + fmt='%(asctime)s [%(name)s](%(levelname)s) %(message)s', + datefmt='%H:%M:%S' + ) +log_handler.setFormatter(log_formatter) +root_logger.addHandler(log_handler) + +logger = logging.getLogger("dstat.main") class Main(object): """Main program """ @@ -141,7 +152,7 @@ class Main(object): ver = getVersion() except ValueError: ver = "1.x" - _logger.error("Could not fetch version number", "WAR") + logger.warning("Could not fetch version number") self.mainwindow.set_title(" ".join(("DStat Interface", ver))) self.aboutdialog.set_version(ver) @@ -247,17 +258,17 @@ class Main(object): self.serial_disconnect.set_sensitive(True) except AttributeError as err: - _logger.error(err, 'WAR') + logger.warning("AttributeError: %s", err) self.serial_connect.set_sensitive(True) except TypeError as err: - _logger.error(err, 'WAR') + logger.warning("TypeError: %s", err) 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') + logger.info("No previous parameters found.") def on_serial_disconnect_clicked(self, data=None): """Disconnect from DStat.""" @@ -273,7 +284,7 @@ class Main(object): comm.serial_instance.proc.terminate() except AttributeError as err: - _logger.error(err, 'WAR') + logger.warning("AttributeError: %s", err) pass self.pmt_mode = False @@ -299,11 +310,11 @@ class Main(object): comm.serial_instance.data_pipe_p.recv() if self.pmt_mode == True: - _logger.error("Start PMT idle mode", "INFO") + logger.info("Start PMT idle mode") comm.serial_instance.proc_pipe_p.send(comm.PMTIdle()) else: - _logger.error("Start OCP", "INFO") + logger.info("Start OCP") comm.serial_instance.proc_pipe_p.send(comm.OCPExp()) self.ocp_proc = (gobject.timeout_add(300, self.ocp_running_data), @@ -312,7 +323,7 @@ class Main(object): self.ocp_is_running = True else: - _logger.error("OCP measurements not supported on v1.1 boards.",'INFO') + logger.info("OCP measurements not supported on v1.1 boards.") return def stop_ocp(self): @@ -320,9 +331,9 @@ class Main(object): if self.version[0] >= 1 and self.version[1] >= 2: if self.pmt_mode == True: - _logger.error("Stop PMT idle mode",'INFO') + logger.info("Stop PMT idle mode") else: - _logger.error("Stop OCP",'INFO') + logger.info("Stop OCP") comm.serial_instance.ctrl_pipe_p.send('a') for i in self.ocp_proc: @@ -332,8 +343,7 @@ 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.") return def ocp_running_data(self): @@ -381,15 +391,13 @@ class Main(object): try: 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') + logger.debug("ocp_running_proc: %s", proc_buffer) 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 @@ -564,34 +572,34 @@ class Main(object): exceptions() except ValueError as i: - _logger.error(i, "INFO") + logger.info("ValueError: %s",i) self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.") exceptions() raise except KeyError as i: - _logger.error("KeyError: %s" % i, "INFO") + logger.info("KeyError: %s", i) self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.") exceptions() raise except InputError as err: - _logger.error(err, "INFO") + logger.info("InputError: %s", err) self.statusbar.push(self.error_context_id, err.msg) exceptions() raise except SerialException as err: - _logger.error(err, "INFO") + logger.info("SerialException: %s", err) self.statusbar.push(self.error_context_id, "Could not establish serial connection.") exceptions() raise except AssertionError as err: - _logger.error(err, "INFO") + logger.info("AssertionError: %s", err) self.statusbar.push(self.error_context_id, str(err)) exceptions() raise @@ -652,20 +660,19 @@ class Main(object): self.on_serial_disconnect_clicked() else: - e = "Unrecognized experiment return code " - e += proc_buffer - _logger.error(e, 'WAR') + logger.warning("Unrecognized experiment return code: %s", + proc_buffer) return False return True except EOFError as err: - _logger.error(err, 'WAR') + logger.warning("EOFError: %s", err) self.experiment_done() return False except IOError as err: - _logger.error(err, 'WAR') + logger.warning("IOError: %s", err) self.experiment_done() return False @@ -789,7 +796,7 @@ class Main(object): except AttributeError: pass except: - _logger.error(sys.exc_info(),'WAR') + logger.warning(sys.exc_info()) def on_file_save_exp_activate(self, menuitem, data=None): """Activate dialogue to save current experiment data. """ -- GitLab From 64f4b3412a6e33b2fe13583de3ef5cbff0832ddd Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 5 Apr 2016 17:50:37 -0400 Subject: [PATCH 04/10] Moved rest of source to native logging. --- dstat_interface/analysis.py | 2 +- dstat_interface/interface/save.py | 14 ++++++++------ dstat_interface/parameter_test.py | 7 +++++-- dstat_interface/params.py | 4 ++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index fe73ea4..366c38a 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -26,7 +26,7 @@ import pygtk import gtk from numpy import mean, trapz -logger = logging.getLogger(__name__) +logger = logging.getLogger('dstat.analysis') class AnalysisOptions(object): """Analysis options window.""" diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 98d8abd..6908500 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -23,9 +23,11 @@ import os import gtk import numpy as np +import logging -from errors import InputError, VarError, ErrorLogger -_logger = ErrorLogger(sender="dstat-interface-save") +logger = logging.getLogger("dstat.interface.save") + +from errors import InputError, VarError from params import save_params, load_params def manSave(current_exp): @@ -45,7 +47,7 @@ def manSave(current_exp): if response == gtk.RESPONSE_OK: path = fcd.get_filename() - _logger.error(" ".join(("Selected filepath:", path)),'INFO') + logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() if filter_selection.endswith("(.npy)"): @@ -86,7 +88,7 @@ def plotSave(plots): if response == gtk.RESPONSE_OK: path = fcd.get_filename() - _logger.error(" ".join(("Selected filepath:", path)),'INFO') + logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() for i in plots: @@ -128,7 +130,7 @@ def man_param_save(window): if response == gtk.RESPONSE_OK: path = fcd.get_filename() - _logger.error(" ".join(("Selected filepath:", path)),'INFO') + logger.info("Selected filepath: %s", path) if not path.endswith(".yml"): path += '.yml' @@ -159,7 +161,7 @@ def man_param_load(window): if response == gtk.RESPONSE_OK: path = fcd.get_filename() - _logger.error(" ".join(("Selected filepath:", path)),'INFO') + logger.info("Selected filepath: %s", path) load_params(window, path) diff --git a/dstat_interface/parameter_test.py b/dstat_interface/parameter_test.py index 4330815..5600a8d 100755 --- a/dstat_interface/parameter_test.py +++ b/dstat_interface/parameter_test.py @@ -18,8 +18,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from errors import ErrorLogger, InputError -_logger = ErrorLogger(sender="dstat-interface-parameters") +import logging + +from errors import InputError + +logger = logging.getLogger("dstat.parameter_test") def lsv_test(params): """Test LSV parameters for sanity""" diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 25f3c90..3ad6181 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -39,7 +39,7 @@ def get_params(window): try: parameters.update(window.adc_pot.params) except InputError: - _logger.error("No gain selected.", 'INFO') + logger.info("No gain selected.") parameters.update(window.exp_window.get_params(selection)) parameters.update(window.analysis_opt_window.params) @@ -69,7 +69,7 @@ def set_params(window, params): window.adc_pot.params = params if not 'experiment_index' in params: - _logger.error("Missing experiment parameters.", 'WAR') + logger.warning("Missing experiment parameters.") return window.expcombobox.set_active( window.exp_window.classes[params['experiment_index']][0]) -- GitLab From ccbf379ad38d59bbcbb3ea0e166303b2b80f08e1 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 5 Apr 2016 17:51:28 -0400 Subject: [PATCH 05/10] Remove unused source files. --- dstat_interface/build_windows.py | 30 ------------------------------ dstat_interface/setup.py | 25 ------------------------- 2 files changed, 55 deletions(-) delete mode 100644 dstat_interface/build_windows.py delete mode 100644 dstat_interface/setup.py diff --git a/dstat_interface/build_windows.py b/dstat_interface/build_windows.py deleted file mode 100644 index d8e31ca..0000000 --- a/dstat_interface/build_windows.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# DStat Interface - An interface for the open hardware DStat potentiostat -# 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 . - -__requires__ = 'PyInstaller==2.1' - - -import os, sys -os.chdir(os.path.dirname(sys.argv[0])) - -args = ['dstat.spec'] -args.extend(sys.argv[1:]) - -import PyInstaller.main as pyi #For some reason, it gets the path here, so working dir must be set first -pyi.run(args) diff --git a/dstat_interface/setup.py b/dstat_interface/setup.py deleted file mode 100644 index bbecb41..0000000 --- a/dstat_interface/setup.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# DStat Interface - An interface for the open hardware DStat potentiostat -# 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 . - -from distutils.core import setup -from Cython.Build import cythonize - -setup( - ext_modules = cythonize("*.pyx") - ) \ No newline at end of file -- GitLab From 4dca984ab7cd3bf9a494ad240e8dd6af702d318b Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 5 Apr 2016 18:52:20 -0400 Subject: [PATCH 06/10] Moved plots to dict attribute of Experiment instances. Separated plot save dialogue from actual saving. --- dstat_interface/dstat_comm.py | 1 + dstat_interface/interface/save.py | 74 ++++++++++++++----------------- dstat_interface/main.py | 12 ++--- dstat_interface/plot.py | 12 +++-- 4 files changed, 46 insertions(+), 53 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index e4aef62..4c83d66 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -339,6 +339,7 @@ class Experiment(object): self.databytes = 8 self.scan = 0 self.time = 0 + self.plots = {} # list of scans, tuple of dimensions, list of data self.data = [([], [])] diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 6908500..3f11d9f 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -50,14 +50,9 @@ def manSave(current_exp): logger.info("Selected filepath: %s", path) 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'))) - npy(current_exp, current_exp.ftdata, "-".join((path,'ft'))) - else: - npy(current_exp, current_exp.data, path, auto=True) - elif filter_selection.endswith("(.txt)"): - if (current_exp.parameters['shutter_true'] and current_exp.parameters['sync_true']): + if filter_selection.endswith("(.txt)"): + if (current_exp.parameters['shutter_true'] and + current_exp.parameters['sync_true']): text(current_exp, current_exp.data, "-".join((path,'data'))) text(current_exp, current_exp.ftdata, "-".join((path,'ft'))) else: @@ -67,7 +62,7 @@ def manSave(current_exp): elif response == gtk.RESPONSE_CANCEL: fcd.destroy() -def plotSave(plots): +def plot_save_dialog(plots): fcd = gtk.FileChooserDialog("Save Plot…", None, gtk.FILE_CHOOSER_ACTION_SAVE, (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, @@ -90,26 +85,42 @@ def plotSave(plots): path = fcd.get_filename() logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() + + if filter_selection.endswith("(.pdf)"): + if not path.endswith(".pdf"): + path += ".pdf" - for i in plots: - save_path = path - save_path += '-' - save_path += i - - if filter_selection.endswith("(.pdf)"): - if not save_path.endswith(".pdf"): - save_path += ".pdf" - - elif filter_selection.endswith("(.png)"): - if not save_path.endswith(".png"): - save_path += ".png" + elif filter_selection.endswith("(.png)"): + if not path.endswith(".png"): + path += ".png" + + save_plot(plots, path) - plots[i].figure.savefig(save_path) # determines format from file extension fcd.destroy() elif response == gtk.RESPONSE_CANCEL: fcd.destroy() +def save_plot(plots, path): + """Saves everything in plots to path. Appends a number for duplicates. + If no file extension or unknown, uses pdf. + """ + root_path = path + name, _sep, ext = root_path.rpartition('.') + if ext == '': + ext = 'pdf' + + num = '' + j = 0 + + for i in plots: # Test for any existing files + while os.path.exists("%s%s-%s.%s" % (name, num, i, ext)): + j += 1 + num = j + + for i in plots: # save data + plots[i].figure.savefig("%s%s-%s.%s" % (name, num, i, ext)) + def man_param_save(window): fcd = gtk.FileChooserDialog("Save Parameters…", None, @@ -176,8 +187,6 @@ def autoSave(current_exp, dir_button, name, expnumber): path = dir_button.get_filename() path += '/' 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) @@ -211,28 +220,13 @@ def autoPlot(plots, dir_button, name, expnumber): path += ".pdf" plots[i].figure.savefig(path) -def npy(exp, data, path, auto=False): - if path.endswith(".npy"): - path = path.rstrip(".npy") - - if auto == True: - j = 1 - while os.path.exists("".join([path, ".npy"])): - if j > 1: - path = path[:-len(str(j))] - path += str(j) - j += 1 - - np.save(path, data) - 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"])): + while os.path.exists("%s.txt" % path): if j > 1: path = path[:-len(str(j))] path += str(j) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index bc2af93..75e2486 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -112,8 +112,8 @@ class Main(object): # 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) + 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') @@ -805,13 +805,7 @@ class Main(object): 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) + save.plot_save_dialog(self.current_exp.plots) def on_file_save_params_activate(self, menuitem, data=None): """Activate dialogue to save current experiment parameters. """ diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 4509428..50b431e 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -90,7 +90,7 @@ def findBounds(y): return (start_index, stop_index) -class plotbox(object): +class PlotBox(object): """Contains main data plot and associated methods.""" def __init__(self, plotwindow_instance): """Creates plot and moves it to a gtk container. @@ -147,11 +147,13 @@ class plotbox(object): def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored - in the Experiment instance. + in the Experiment instance. Stores class instance in Experiment. """ self.axe1.set_xlabel(Experiment.xlabel) self.axe1.set_ylabel(Experiment.ylabel) self.axe1.set_xlim(Experiment.xmin, Experiment.xmax) + + Experiment.plots['data'] = self self.figure.canvas.draw() @@ -163,7 +165,7 @@ class plotbox(object): return True -class ft_box(plotbox): +class FT_Box(PlotBox): def updateline(self, Experiment, line_number): def search_value(data, target): for i in range(len(data)): @@ -187,11 +189,13 @@ class ft_box(plotbox): def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored - in the Experiment instance. + in the Experiment instance. Stores class instance in Experiment. """ self.axe1.set_xlabel("Freq (Hz)") self.axe1.set_ylabel("|Y| (A/Hz)") self.axe1.set_xlim(0, Experiment.parameters['adc_rate_hz']/2) + + Experiment.plots['ft'] = self self.figure.canvas.draw() -- GitLab From 4ccf3c2d2e8c4c04fe022f19d9d8c0e9e57d05c8 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 5 Apr 2016 20:47:04 -0400 Subject: [PATCH 07/10] Separate more save stuff from UI. --- dstat_interface/analysis.py | 12 +- dstat_interface/dstat_comm.py | 7 +- dstat_interface/interface/save.py | 182 +++++++++++++----------------- dstat_interface/main.py | 62 ++++++---- dstat_interface/plot.py | 12 +- 5 files changed, 134 insertions(+), 141 deletions(-) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index 366c38a..33144c0 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -109,24 +109,24 @@ def do_analysis(experiment): if experiment.parameters['stats_start_true']: start = experiment.parameters['stats_start'] else: - start = min(experiment.data[0][0]) + start = min(experiment.data['data'][0][0]) if experiment.parameters['stats_stop_true']: stop = experiment.parameters['stats_stop'] else: - stop = min(experiment.data[0][0]) + stop = min(experiment.data['data'][0][0]) - data = _data_slice(experiment.data, + data = _data_slice(experiment.data['data'], start, stop ) else: - data = experiment.data + data = experiment.data['data'] experiment.analysis.update(_summary_stats(data)) try: - x, y = experiment.ftdata[0] + x, y = experiment.data['ft'][0] experiment.analysis['FT Integral'] = _integrateSpectrum( x, y, @@ -134,7 +134,7 @@ def do_analysis(experiment): float(experiment.parameters['fft_int']) ) - except AttributeError: + except KeyError: pass def _data_slice(data, start, stop): diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 4c83d66..ffa43f2 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -340,9 +340,10 @@ class Experiment(object): self.scan = 0 self.time = 0 self.plots = {} + self.data = {} # list of scans, tuple of dimensions, list of data - self.data = [([], [])] + self.data['data'] = [([], [])] self.line_data = ([], []) major, minor = self.parameters['version'] @@ -744,7 +745,7 @@ class SWVExp(Experiment): self.datatype = "SWVData" self.xlabel = "Voltage (mV)" self.ylabel = "Current (A)" - self.data = [([], [], [], [])] # voltage, current, forwards, reverse + self.data['data'] = [([], [], [], [])] # voltage, current, forwards, reverse self.line_data = ([], [], [], []) self.datalength = 2 * self.parameters['scans'] self.databytes = 10 @@ -803,7 +804,7 @@ class DPVExp(SWVExp): self.datatype = "SWVData" self.xlabel = "Voltage (mV)" self.ylabel = "Current (A)" - self.data = [([], [], [], [])] # voltage, current, forwards, reverse + self.data['data'] = [([], [], [], [])] # voltage, current, forwards, reverse self.line_data = ([], [], [], []) self.datalength = 2 self.databytes = 10 diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 3f11d9f..675bcd1 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -51,12 +51,8 @@ def manSave(current_exp): filter_selection = fcd.get_filter().get_name() if filter_selection.endswith("(.txt)"): - if (current_exp.parameters['shutter_true'] and - current_exp.parameters['sync_true']): - text(current_exp, current_exp.data, "-".join((path,'data'))) - text(current_exp, current_exp.ftdata, "-".join((path,'ft'))) - else: - text(current_exp, current_exp.data, path, auto=True) + save_text(current_exp, path) + fcd.destroy() elif response == gtk.RESPONSE_CANCEL: @@ -101,25 +97,6 @@ def plot_save_dialog(plots): elif response == gtk.RESPONSE_CANCEL: fcd.destroy() -def save_plot(plots, path): - """Saves everything in plots to path. Appends a number for duplicates. - If no file extension or unknown, uses pdf. - """ - root_path = path - name, _sep, ext = root_path.rpartition('.') - if ext == '': - ext = 'pdf' - - num = '' - j = 0 - - for i in plots: # Test for any existing files - while os.path.exists("%s%s-%s.%s" % (name, num, i, ext)): - j += 1 - num = j - - for i in plots: # save data - plots[i].figure.savefig("%s%s-%s.%s" % (name, num, i, ext)) def man_param_save(window): fcd = gtk.FileChooserDialog("Save Parameters…", @@ -181,98 +158,101 @@ def man_param_load(window): elif response == gtk.RESPONSE_CANCEL: fcd.destroy() -def autoSave(current_exp, dir_button, name, expnumber): +def autoSave(exp, path, name): if name == "": name = "file" - path = dir_button.get_filename() + path += '/' path += name + + save_text(exp, path) - 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) - else: - text(current_exp, current_exp.data, path, auto=True) - -def autoPlot(plots, dir_button, name, expnumber): - for i in plots: - if name == "": - name = "file" - - path = dir_button.get_filename() - path += '/' - path += name - path += '-' - 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 +def autoPlot(exp, path, name): + if name == "": + name = "file" + + path += '/' + path += name + if not (path.endswith(".pdf") or path.endswith(".png")): path += ".pdf" - plots[i].figure.savefig(path) - -def text(exp, data, path, auto=False): - if path.endswith(".txt"): - path = path.rstrip(".txt") - - if auto == True: - j = 1 - while os.path.exists("%s.txt" % path): - if j > 1: - path = path[:-len(str(j))] - path += str(j) - j += 1 - path += ".txt" - file = open(path, 'w') + save_plot(exp, path) - time = exp.time +def save_text(exp, path): + name, _sep, ext = path.rpartition('.') # ('','',string) if no match + if _sep == '': + name = ext + ext = 'txt' + + num = '' + j = 0 + + for dname in exp.data: # Test for any existing files + while os.path.exists("%s%s-%s.%s" % (name, num, dname, ext)): + j += 1 + num = j + + for dname in exp.data: # save data + file = open("%s%s-%s.%s" % (name, num, dname, ext), 'w') - header = "".join(['# TIME ', time.isoformat(), "\n"]) + time = exp.time + header = "".join(['# TIME ', time.isoformat(), "\n"]) - header += "# DSTAT COMMANDS\n# " - for i in exp.commands: - header += i + header += "# DSTAT COMMANDS\n# " + for i in exp.commands: + header += i - file.write("".join([header, '\n'])) + file.write("".join([header, '\n'])) - analysis_buffer = [] + analysis_buffer = [] - if exp.analysis != {}: - analysis_buffer.append("# ANALYSIS") - for key, value in exp.analysis.iteritems(): - analysis_buffer.append("# %s:" % key) - for scan in value: - number, result = scan - analysis_buffer.append( - "# Scan %s -- %s" % (number, result) - ) + if exp.analysis != {}: + analysis_buffer.append("# ANALYSIS") + for key, value in exp.analysis.iteritems(): + analysis_buffer.append("# %s:" % key) + for scan in value: + number, result = scan + analysis_buffer.append( + "# Scan %s -- %s" % (number, result) + ) - for i in analysis_buffer: - file.write("%s\n" % i) + for i in analysis_buffer: + file.write("%s\n" % i) - # Write out actual data - line_buffer = [] + # Write out actual data + line_buffer = [] - for scan in zip(*data): - for dimension in scan: - for i in range(len(dimension)): - try: - line_buffer[i] += "%s " % dimension[i] - except IndexError: - line_buffer.append("") - line_buffer[i] += "%s " % dimension[i] + for scan in zip(*exp.data[dname]): + for dimension in scan: + for i in range(len(dimension)): + try: + line_buffer[i] += "%s " % dimension[i] + except IndexError: + line_buffer.append("") + line_buffer[i] += "%s " % dimension[i] - for i in line_buffer: - file.write("%s\n" % i) + for i in line_buffer: + file.write("%s\n" % i) - file.close() + file.close() + +def save_plot(exp, path): + """Saves everything in exp.plots to path. Appends a number for duplicates. + If no file extension or unknown, uses pdf. + """ + name, _sep, ext = path.rpartition('.') + if _sep == '': + name = ext + ext = 'pdf' + + num = '' + j = 0 + + for i in exp.plots: # Test for any existing files + while os.path.exists("%s%s-%s.%s" % (name, num, i, ext)): + j += 1 + num = j + + for i in exp.plots: # save data + exp.plots[i].figure.savefig("%s%s-%s.%s" % (name, num, i, ext)) \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 75e2486..ff14497 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -605,7 +605,8 @@ class Main(object): raise def experiment_running_data(self): - """Receive data from experiment process and add to current_exp.data. + """Receive data from experiment process and add to + current_exp.data['data]. Run in GTK main loop. Returns: @@ -619,12 +620,12 @@ class Main(object): self.line, data = incoming if self.line > self.lastdataline: - self.current_exp.data.append( + self.current_exp.data['data'].append( deepcopy(self.current_exp.line_data)) self.lastdataline = self.line - for i in range(len(self.current_exp.data[self.line])): - self.current_exp.data[self.line][i].append(data[i]) + for i in range(len(self.current_exp.data['data'][self.line])): + self.current_exp.data['data'][self.line][i].append(data[i]) if comm.serial_instance.data_pipe_p.poll(): self.experiment_running_data() @@ -698,8 +699,6 @@ 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 - - self.databuffer.set_text("") self.databuffer.place_cursor(self.databuffer.get_start_iter()) @@ -712,10 +711,20 @@ class Main(object): self.current_exp.parameters['sync_true']): self.ft_plot.updateline(self.current_exp, 0) self.ft_plot.redraw() - for col in zip(*self.current_exp.ftdata): - for row in col: - self.databuffer.insert_at_cursor(str(row)+ " ") - self.databuffer.insert_at_cursor("\n") + + line_buffer = [] + + for scan in self.current_exp.data['ft']: + for dimension in scan: + for i in range(len(dimension)): + try: + line_buffer[i] += "%s " % dimension[i] + except IndexError: + line_buffer.append("") + line_buffer[i] += "%s " % dimension[i] + + for i in line_buffer: + self.databuffer.insert_at_cursor("%s\n" % i) # Run Analysis analysis.do_analysis(self.current_exp) @@ -752,7 +761,7 @@ class Main(object): line_buffer = [] - for scan in self.current_exp.data: + for scan in self.current_exp.data['data']: for dimension in scan: for i in range(len(dimension)): try: @@ -766,17 +775,15 @@ class Main(object): # Autosaving 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 + save.autoSave(self.current_exp, + self.autosavedir_button.get_filename(), + self.autosavename.get_text() + ) + + save.autoPlot(self.current_exp, + self.autosavedir_button.get_filename(), + self.autosavename.get_text() + ) # uDrop # UI stuff @@ -800,12 +807,17 @@ class Main(object): def on_file_save_exp_activate(self, menuitem, data=None): """Activate dialogue to save current experiment data. """ - if self.current_exp: + try: save.manSave(self.current_exp) - + except AttributeError: + logger.warning("Tried to save with no experiment run") + def on_file_save_plot_activate(self, menuitem, data=None): """Activate dialogue to save current plot.""" - save.plot_save_dialog(self.current_exp.plots) + try: + save.plot_save_dialog(self.current_exp) + except AttributeError: + logger.warning("Tried to save with no experiment run") def on_file_save_params_activate(self, menuitem, data=None): """Activate dialogue to save current experiment parameters. """ diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 50b431e..7c5c017 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -138,12 +138,12 @@ class PlotBox(object): the Experiment instance. """ # limits display to 2000 data points per line - divisor = len(Experiment.data[line_number][0]) // 2000 + 1 + divisor = len(Experiment.data['data'][line_number][0]) // 2000 + 1 self.axe1.lines[line_number].set_ydata( - Experiment.data[line_number][1][1::divisor]) + Experiment.data['data'][line_number][1][1::divisor]) self.axe1.lines[line_number].set_xdata( - Experiment.data[line_number][0][1::divisor]) + Experiment.data['data'][line_number][0][1::divisor]) def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored @@ -172,8 +172,8 @@ class FT_Box(PlotBox): if data[i] > target: return i - y = Experiment.data[line_number][1] - x = Experiment.data[line_number][0] + y = Experiment.data['data'][line_number][1] + x = Experiment.data['data'][line_number][0] freq = Experiment.parameters['adc_rate_hz'] i = search_value(x, float(Experiment.parameters['fft_start'])) y1 = y[i:] @@ -185,7 +185,7 @@ class FT_Box(PlotBox): f, Y = plotSpectrum(y1[min_index:max_index],freq) self.axe1.lines[line_number].set_ydata(Y) self.axe1.lines[line_number].set_xdata(f) - Experiment.ftdata = [(f, Y)] + Experiment.data['ft'] = [(f, Y)] def changetype(self, Experiment): """Change plot type. Set axis labels and x bounds to those stored -- GitLab From 8badd4ba0c2ffce210e0b5dec9bad0d042918f4e Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 6 Apr 2016 15:07:17 -0400 Subject: [PATCH 08/10] Add save functions to plugin. --- dstat_interface/plugin.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index b1d1f23..e4744ed 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -2,6 +2,7 @@ import logging from params import get_params, set_params, load_params, save_params +from interface.save import save_text, save_plot from zmq_plugin.plugin import Plugin as ZmqPlugin from zmq_plugin.schema import decode_content_data import gtk @@ -100,6 +101,26 @@ class DstatPlugin(ZmqPlugin): self.parent.statusbar.push(self.parent.message_context_id, "µDrop " "acquisition requested.") return self.parent.run_active_experiment() + + def on_execute__save_text(self, request): + ''' + Args + ---- + + save_data_path (str) : Path to file to save text data. + ''' + data = decode_content_data(request) + save_text(self.parent.current_exp, data['save_data_path']) + + def on_execute__save_plot(self, request): + ''' + Args + ---- + + save_plot_path (str) : Path to file to save text data. + ''' + data = decode_content_data(request) + save_text(self.parent.current_exp, data['save_plot_path']) def on_execute__acquisition_complete(self, request): ''' -- GitLab From 7a1de68eb21dfe25e4246eac19ec0e5006da1285 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Fri, 8 Apr 2016 13:18:58 -0400 Subject: [PATCH 09/10] Fix typo in experiment UUID code --- dstat_interface/main.py | 50 +++++++++++++++++++-------------------- dstat_interface/plugin.py | 2 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index ff14497..146c83b 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -205,7 +205,7 @@ class Main(object): """Display the about window.""" self.response = self.aboutdialog.run() # waits for user to click close self.aboutdialog.hide() - + def on_menu_analysis_options_activate(self, menuitem, data=None): self.analysis_opt_window.show() @@ -421,7 +421,7 @@ class Main(object): """Run currently visible experiment.""" # Assign current experiment a unique identifier. experiment_id = uuid.uuid4() - self.active_experiment_id = uuid.uuid4() + self.active_experiment_id = experiment_id def exceptions(): """ Cleans up after errors """ @@ -605,7 +605,7 @@ class Main(object): raise def experiment_running_data(self): - """Receive data from experiment process and add to + """Receive data from experiment process and add to current_exp.data['data]. Run in GTK main loop. @@ -626,7 +626,7 @@ class Main(object): for i in range(len(self.current_exp.data['data'][self.line])): self.current_exp.data['data'][self.line][i].append(data[i]) - + if comm.serial_instance.data_pipe_p.poll(): self.experiment_running_data() return True @@ -699,21 +699,21 @@ 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 - + self.databuffer.set_text("") self.databuffer.place_cursor(self.databuffer.get_start_iter()) self.rawbuffer.insert_at_cursor("\n") self.rawbuffer.set_text("") self.rawbuffer.place_cursor(self.rawbuffer.get_start_iter()) - + # Shutter stuff 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() - + line_buffer = [] - + for scan in self.current_exp.data['ft']: for dimension in scan: for i in range(len(dimension)): @@ -722,22 +722,22 @@ class Main(object): except IndexError: line_buffer.append("") line_buffer[i] += "%s " % dimension[i] - + for i in line_buffer: self.databuffer.insert_at_cursor("%s\n" % i) - + # Run Analysis analysis.do_analysis(self.current_exp) - + # Write DStat commands for i in self.current_exp.commands: self.rawbuffer.insert_at_cursor(i) self.rawbuffer.insert_at_cursor("\n") - + try: self.statusbar.push( - self.message_context_id, + self.message_context_id, "Integral: %s A" % self.current_exp.analysis['FT Integral'][0][1] ) except KeyError: @@ -745,7 +745,7 @@ class Main(object): # Data Output analysis_buffer = [] - + if self.current_exp.analysis != {}: analysis_buffer.append("# ANALYSIS") for key, value in self.current_exp.analysis.iteritems(): @@ -755,12 +755,12 @@ class Main(object): analysis_buffer.append( "# Scan %s -- %s" % (number, result) ) - + for i in analysis_buffer: self.rawbuffer.insert_at_cursor("%s\n" % i) - + line_buffer = [] - + for scan in self.current_exp.data['data']: for dimension in scan: for i in range(len(dimension)): @@ -769,28 +769,28 @@ class Main(object): except IndexError: line_buffer.append("") line_buffer[i] += "%s " % dimension[i] - + for i in line_buffer: self.rawbuffer.insert_at_cursor("%s\n" % i) - + # Autosaving if self.autosave_checkbox.get_active(): - save.autoSave(self.current_exp, + save.autoSave(self.current_exp, self.autosavedir_button.get_filename(), self.autosavename.get_text() ) - save.autoPlot(self.current_exp, + save.autoPlot(self.current_exp, self.autosavedir_button.get_filename(), self.autosavename.get_text() ) - + # uDrop # UI stuff 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() @@ -811,7 +811,7 @@ class Main(object): save.manSave(self.current_exp) except AttributeError: logger.warning("Tried to save with no experiment run") - + def on_file_save_plot_activate(self, menuitem, data=None): """Activate dialogue to save current plot.""" try: diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index e4744ed..038073a 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -101,7 +101,7 @@ class DstatPlugin(ZmqPlugin): self.parent.statusbar.push(self.parent.message_context_id, "µDrop " "acquisition requested.") return self.parent.run_active_experiment() - + def on_execute__save_text(self, request): ''' Args -- GitLab From 0f5300c16ebb2b5caf1df443ae29f7cd65fc76ac Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Fri, 8 Apr 2016 13:19:15 -0400 Subject: [PATCH 10/10] Fix typo in save_plot 0MQ plugin interface --- dstat_interface/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index 038073a..ebe2493 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -117,10 +117,10 @@ class DstatPlugin(ZmqPlugin): Args ---- - save_plot_path (str) : Path to file to save text data. + save_plot_path (str) : Path to file to save plot. ''' data = decode_content_data(request) - save_text(self.parent.current_exp, data['save_plot_path']) + save_plot(self.parent.current_exp, data['save_plot_path']) def on_execute__acquisition_complete(self, request): ''' -- GitLab