From 9b3e7c71d8609f4a6f366b35b0789aa3f9faee26 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" <mdryden@chem.utoronto.ca> Date: Fri, 28 Nov 2014 17:22:37 -0500 Subject: [PATCH] Implements Potentiometry and OCP. Completes #18. --- dstat-interface/dstat-interface/dstat_comm.py | 59 +++++++++- .../interface/dstatinterface.glade | 22 +++- .../dstat-interface/interface/exp_int.py | 8 ++ .../dstat-interface/interface/exp_window.py | 1 + .../dstat-interface/interface/potexp.glade | 102 ++++++++++++++++++ dstat-interface/dstat-interface/main.py | 97 ++++++++++++++++- 6 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 dstat-interface/dstat-interface/interface/potexp.glade diff --git a/dstat-interface/dstat-interface/dstat_comm.py b/dstat-interface/dstat-interface/dstat_comm.py index 426acfa..4a400a3 100644 --- a/dstat-interface/dstat-interface/dstat_comm.py +++ b/dstat-interface/dstat-interface/dstat_comm.py @@ -44,14 +44,18 @@ def version_check(ser_port): Arguments: ser_port -- address of serial port to use """ + ser = delayedSerial(ser_port, 1024000, timeout=1) ser.write("ck") ser.flushInput() ser.write('!') - while not ser.read().startswith("C"): + while not ser.read()=="C": + time.sleep(.5) ser.write('!') + + ser.write('V') for line in ser: if line.startswith('V'): @@ -158,6 +162,7 @@ class Experiment(object): self.serial.write(i) if not self.serial_handler(): + self.main_pipe.send("ABORT") break self.data_postprocessing() @@ -174,13 +179,16 @@ class Experiment(object): if self.main_pipe.poll(): if self.main_pipe.recv() == 'a': self.serial.write('a') + print "ABORT!" return False for line in self.serial: if self.main_pipe.poll(): if self.main_pipe.recv() == 'a': self.serial.write('a') + print "ABORT!" return False + if line.startswith('B'): self.main_pipe.send(self.data_handler( (scan, self.serial.read(size=self.databytes)))) @@ -245,6 +253,32 @@ class Chronoamp(Experiment): return (scan, [seconds+milliseconds/1000., current*(1.5/self.gain/8388607)]) +class PotExp(Experiment): + """Potentiometry experiment""" + def __init__(self, parameters, main_pipe): + super(PotExp, self).__init__(parameters, main_pipe) + + self.datatype = "linearData" + self.xlabel = "Time (s)" + self.ylabel = "Voltage (V)" + self.data = [[], []] + self.datalength = 2 + self.databytes = 8 + self.xmin = 0 + self.xmax = self.parameters['time'] + + self.commands += "P" + self.commands[2] += str(self.parameters['time']) + self.commands[2] += " 1 " #potentiometry mode + + def data_handler(self, data_input): + """Overrides Experiment method to not convert x axis to mV.""" + scan, data = data_input + # 2*uint16 + int32 + seconds, milliseconds, voltage = struct.unpack('<HHl', data) + return (scan, + [seconds+milliseconds/1000., voltage*(1.5/8388607.)]) + class LSVExp(Experiment): """Linear Scan Voltammetry experiment""" def __init__(self, parameters, main_pipe): @@ -406,3 +440,26 @@ class DPVExp(SWVExp): self.commands[2] += " " self.commands[2] += str(self.parameters['width']) self.commands[2] += " " + +class OCPExp(Experiment): + """Open circuit potential measumement in statusbar.""" + def __init__(self, main_pipe): + """Only needs data pipe.""" + self.main_pipe = main_pipe + self.databytes = 8 + + self.commands = ["A", "P"] + + self.commands[0] += "2 " # input buffer + self.commands[0] += "3 " # 2.5 Hz sample rate + self.commands[0] += "1 " # 2x PGA + + self.commands[1] += "0 " # no timeout + self.commands[1] += "0 " # OCP measurement mode + + def data_handler(self, data_input): + """Overrides Experiment method to only send ADC values.""" + scan, data = data_input + # 2*uint16 + int32 + seconds, milliseconds, voltage = struct.unpack('<HHl', data) + return (voltage/5.592405e6) \ No newline at end of file diff --git a/dstat-interface/dstat-interface/interface/dstatinterface.glade b/dstat-interface/dstat-interface/interface/dstatinterface.glade index 644e430..9c2346c 100644 --- a/dstat-interface/dstat-interface/interface/dstatinterface.glade +++ b/dstat-interface/dstat-interface/interface/dstatinterface.glade @@ -47,6 +47,11 @@ <col id="1" translatable="yes">pde</col> <col id="2" translatable="yes">Photodiode</col> </row> + <row> + <col id="0">7</col> + <col id="1" translatable="yes">pot</col> + <col id="2" translatable="yes">Potentiometry</col> + </row> </data> </object> <object class="GtkAboutDialog" id="aboutdialog1"> @@ -1225,6 +1230,21 @@ Thanks to Christian Fobel for help with Dropbot Plugin</property> <property name="position">3</property> </packing> </child> + <child> + <object class="GtkLabel" id="ocp_disp"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="xalign">0</property> + <property name="xpad">5</property> + <property name="label" translatable="yes">OCP:</property> + <property name="single_line_mode">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">4</property> + </packing> + </child> <child> <object class="GtkStatusbar" id="statusbar"> <property name="visible">True</property> @@ -1234,7 +1254,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin</property> <packing> <property name="expand">True</property> <property name="fill">True</property> - <property name="position">4</property> + <property name="position">5</property> </packing> </child> </object> diff --git a/dstat-interface/dstat-interface/interface/exp_int.py b/dstat-interface/dstat-interface/interface/exp_int.py index 8e2e01c..2e67a1e 100644 --- a/dstat-interface/dstat-interface/interface/exp_int.py +++ b/dstat-interface/dstat-interface/interface/exp_int.py @@ -201,4 +201,12 @@ class PD(ExpInterface): super(PD, self).__init__('interface/pd.glade') self.entry['voltage'] = self.builder.get_object('voltage_entry') + self.entry['time'] = self.builder.get_object('time_entry') + +class POT(ExpInterface): + """Experiment class for Potentiometry.""" + def __init__(self): + """Adds entry listings to superclass's self.entry dict""" + super(POT, self).__init__('interface/potexp.glade') + self.entry['time'] = self.builder.get_object('time_entry') \ No newline at end of file diff --git a/dstat-interface/dstat-interface/interface/exp_window.py b/dstat-interface/dstat-interface/interface/exp_window.py index 79301eb..f5bf561 100644 --- a/dstat-interface/dstat-interface/interface/exp_window.py +++ b/dstat-interface/dstat-interface/interface/exp_window.py @@ -31,6 +31,7 @@ class Experiments: self.classes['dpv'] = exp.DPV() self.classes['acv'] = exp.ACV() self.classes['pde'] = exp.PD() + self.classes['pot'] = exp.POT() #fill exp_section exp_section = self.builder.get_object('exp_section_box') diff --git a/dstat-interface/dstat-interface/interface/potexp.glade b/dstat-interface/dstat-interface/interface/potexp.glade new file mode 100644 index 0000000..4d8d915 --- /dev/null +++ b/dstat-interface/dstat-interface/interface/potexp.glade @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interface> + <requires lib="gtk+" version="2.24"/> + <!-- interface-naming-policy project-wide --> + <object class="GtkListStore" id="ca_list"> + <columns> + <!-- column-name millivolts --> + <column type="gint"/> + <!-- column-name seconds --> + <column type="guint"/> + </columns> + </object> + <object class="GtkWindow" id="window1"> + <property name="can_focus">False</property> + <property name="default_width">300</property> + <property name="default_height">500</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hscrollbar_policy">automatic</property> + <property name="vscrollbar_policy">automatic</property> + <child> + <object class="GtkViewport" id="viewport1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="shadow_type">none</property> + <child> + <object class="GtkVBox" id="vbox1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTable" id="table1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="n_rows">2</property> + <property name="n_columns">2</property> + <child> + <object class="GtkLabel" id="label2"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Time (s)</property> + </object> + <packing> + <property name="bottom_attach">2</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="time_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="invisible_char">â—</property> + <property name="width_chars">5</property> + <property name="text" translatable="yes">0</property> + <property name="xalign">1</property> + <property name="truncate_multiline">True</property> + <property name="invisible_char_set">True</property> + <property name="caps_lock_warning">False</property> + <property name="primary_icon_activatable">False</property> + <property name="secondary_icon_activatable">False</property> + <property name="primary_icon_sensitive">True</property> + <property name="secondary_icon_sensitive">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="right_attach">2</property> + <property name="bottom_attach">2</property> + <property name="x_options">GTK_EXPAND</property> + <property name="y_options">GTK_SHRINK</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="label1"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Connect the electrodes to the RE input and the W_SHIELD connectors. +The ADC's PGA can be used to amplify the input signal, but note that the plot's y-axis is only correct for PGA 2x.</property> + <property name="wrap">True</property> + <property name="width_chars">30</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + </child> + </object> +</interface> diff --git a/dstat-interface/dstat-interface/main.py b/dstat-interface/dstat-interface/main.py index 9425a42..94d91d2 100644 --- a/dstat-interface/dstat-interface/main.py +++ b/dstat-interface/dstat-interface/main.py @@ -60,6 +60,7 @@ class Main(object): #create instance of interface components self.statusbar = self.builder.get_object('statusbar') + self.ocp_disp = self.builder.get_object('ocp_disp') self.window = self.builder.get_object('window1') self.aboutdialog = self.builder.get_object('aboutdialog1') self.rawbuffer = self.builder.get_object('databuffer1') @@ -125,10 +126,12 @@ class Main(object): def on_window1_destroy(self, object, data=None): """ Quit when main window closed.""" + self.on_pot_stop_clicked() gtk.main_quit() def on_gtk_quit_activate(self, menuitem, data=None): """Quit when Quit selected from menu.""" + self.on_pot_stop_clicked() gtk.main_quit() def on_gtk_about_activate(self, menuitem, data=None): @@ -155,6 +158,11 @@ class Main(object): def on_serial_version_clicked(self, data=None): """Retrieve DStat version.""" + try: + self.on_pot_stop_clicked() + except AttributeError: + pass + self.version = comm.version_check(self.serial_liststore.get_value( self.serial_combobox.get_active_iter(), 0)) @@ -170,6 +178,23 @@ class Main(object): "".join(["DStat version: ", str(self.version[0]), ".", str(self.version[1])]) ) + self.start_ocp() + + def start_ocp(self): + """Start OCP measurements.""" + if self.version[0] >= 1 and self.version[1] >= 2: + self.recv_p, self.send_p = multiprocessing.Pipe(duplex=True) + self.current_exp = comm.OCPExp(self.send_p) + + self.current_exp.run_wrapper(self.serial_liststore.get_value( + self.serial_combobox.get_active_iter(), 0)) + + self.send_p.close() # need for EOF signal to work + + self.ocp_proc = gobject.idle_add(self.ocp_running) + else: + print "OCP measurements not supported on v1.1 boards." + return def on_pot_start_clicked(self, data=None): """Run currently visible experiment.""" @@ -185,7 +210,12 @@ class Main(object): self.spinner.stop() self.startbutton.set_sensitive(True) self.stopbutton.set_sensitive(False) - + self.start_ocp() + + # Stop OCP measurements + self.on_pot_stop_clicked() + gobject.source_remove(self.ocp_proc) + selection = self.expcombobox.get_active() parameters = {} parameters['version'] = self.version @@ -467,7 +497,38 @@ class Main(object): self.experiment_running_plot) gobject.idle_add(self.experiment_running) return + elif selection == 7: # POT + if not (self.version[0] >= 1 and self.version[1] >= 2): + self.statusbar.push(self.error_context_id, + "v1.1 board does not support potentiometry.") + exceptions() + return + + parameters.update(self.exp_window.get_params('pot')) + + if (parameters['time'] <= 0): + raise InputError(parameters['clean_s'], + "Time must be greater than zero.") + if (parameters['time'] > 65535): + raise InputError(parameters['clean_s'], + "Time must fit in 16-bit counter.") + + self.recv_p, self.send_p = multiprocessing.Pipe(duplex=True) + self.current_exp = comm.PotExp(parameters, self.send_p) + + self.plot.clearall() + self.plot.changetype(self.current_exp) + + self.current_exp.run_wrapper( + self.serial_liststore.get_value( + self.serial_combobox.get_active_iter(), 0)) + + self.send_p.close() + self.plot_proc = gobject.timeout_add(200, + self.experiment_running_plot) + gobject.idle_add(self.experiment_running) + return else: self.statusbar.push(self.error_context_id, "Experiment not yet implemented.") @@ -523,6 +584,29 @@ class Main(object): self.experiment_done() return False + def ocp_running(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 self.recv_p.poll(): + data = "".join(["OCP: ", + "{0:.3f}".format(self.recv_p.recv()), + " V"]) + self.ocp_disp.set_text(data) + + else: + time.sleep(.001) + return True + except EOFError: + return False + except IOError: + 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 @@ -582,12 +666,21 @@ class Main(object): self.spinner.stop() self.startbutton.set_sensitive(True) self.stopbutton.set_sensitive(False) + self.start_ocp() def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" - if self.recv_p: + try: print "stop" self.recv_p.send('a') + while True: + if self.recv_p.poll(): + if self.recv_p.recv() == "ABORT": + return + except AttributeError: + pass + except IOError: + pass def on_file_save_exp_activate(self, menuitem, data=None): """Activate dialogue to save current experiment data. """ -- GitLab