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