From a62ee801449780011fe0ecbd8afbca7186218647 Mon Sep 17 00:00:00 2001
From: "Michael D. M. Dryden" <mdryden@chem.utoronto.ca>
Date: Wed, 6 Aug 2014 17:18:47 -0400
Subject: [PATCH] Microdrop interface using zmq. Currently defaults to
 listening on all interfaces for TCP port 6789.

---
 .../dstatInterface.xcodeproj/project.pbxproj  |  2 +
 dstatInterface/interface/dstatinterface.glade | 17 ++--
 dstatInterface/interface_test.py              | 93 +++++++++++++++----
 dstatInterface/microdrop.py                   | 51 ++++++++++
 4 files changed, 139 insertions(+), 24 deletions(-)
 create mode 100644 dstatInterface/microdrop.py

diff --git a/dstatInterface/dstatInterface.xcodeproj/project.pbxproj b/dstatInterface/dstatInterface.xcodeproj/project.pbxproj
index 6371382..dccb0b5 100644
--- a/dstatInterface/dstatInterface.xcodeproj/project.pbxproj
+++ b/dstatInterface/dstatInterface.xcodeproj/project.pbxproj
@@ -8,6 +8,7 @@
 
 /* Begin PBXFileReference section */
 		5F87883C19072E86007B53E0 /* mpltest.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = mpltest.py; sourceTree = "<group>"; };
+		5FB0B8E1198ACD4B00FA6CB7 /* microdrop.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = microdrop.py; sourceTree = "<group>"; };
 		5FCB541B190591CD00CEB148 /* interface */ = {isa = PBXFileReference; lastKnownFileType = folder; path = interface; sourceTree = "<group>"; };
 		5FCB541D1905923800CEB148 /* interface_test.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = interface_test.py; sourceTree = "<group>"; };
 		5FCB54231905B6EE00CEB148 /* dstat_comm.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = dstat_comm.py; sourceTree = "<group>"; };
@@ -23,6 +24,7 @@
 				5FF00FDC1942BD16004D38A8 /* setup.py */,
 				5F87883C19072E86007B53E0 /* mpltest.py */,
 				5FCB541D1905923800CEB148 /* interface_test.py */,
+				5FB0B8E1198ACD4B00FA6CB7 /* microdrop.py */,
 				5FCB54231905B6EE00CEB148 /* dstat_comm.py */,
 				5FCB541B190591CD00CEB148 /* interface */,
 				5FDC0DFD18FDAD79003F857A /* mpl.py */,
diff --git a/dstatInterface/interface/dstatinterface.glade b/dstatInterface/interface/dstatinterface.glade
index a608eaa..6b1062a 100644
--- a/dstatInterface/interface/dstatinterface.glade
+++ b/dstatInterface/interface/dstatinterface.glade
@@ -174,31 +174,32 @@
               </object>
             </child>
             <child>
-              <object class="GtkMenuItem" id="menuitem2">
+              <object class="GtkMenuItem" id="menu_dropbot">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="label" translatable="yes">_Edit</property>
+                <property name="label" translatable="yes">Dropbot</property>
                 <property name="use_underline">True</property>
                 <child type="submenu">
                   <object class="GtkMenu" id="menu2">
                     <property name="visible">True</property>
                     <property name="can_focus">False</property>
                     <child>
-                      <object class="GtkImageMenuItem" id="imagemenuitem7">
-                        <property name="label">gtk-copy</property>
+                      <object class="GtkImageMenuItem" id="menu_dropbot_connect">
+                        <property name="label">gtk-connect</property>
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
-                        <property name="use_underline">True</property>
                         <property name="use_stock">True</property>
+                        <signal name="activate" handler="on_menu_dropbot_connect_activate" swapped="no"/>
                       </object>
                     </child>
                     <child>
-                      <object class="GtkImageMenuItem" id="imagemenuitem8">
-                        <property name="label">gtk-paste</property>
+                      <object class="GtkImageMenuItem" id="menu_dropbot_disconnect">
+                        <property name="label">gtk-disconnect</property>
                         <property name="visible">True</property>
+                        <property name="sensitive">False</property>
                         <property name="can_focus">False</property>
-                        <property name="use_underline">True</property>
                         <property name="use_stock">True</property>
+                        <signal name="activate" handler="on_menu_dropbot_disconnect_activate" swapped="no"/>
                       </object>
                     </child>
                   </object>
diff --git a/dstatInterface/interface_test.py b/dstatInterface/interface_test.py
index e6d74e8..6daf7d7 100644
--- a/dstatInterface/interface_test.py
+++ b/dstatInterface/interface_test.py
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
 
 import sys
 try:
@@ -34,6 +35,7 @@ import multiprocessing
 import time
 
 import mpltest
+import microdrop
 
 class Error(Exception):
     pass
@@ -76,6 +78,7 @@ class main:
         self.pd = pd.pd()
         
         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')
         
@@ -143,6 +146,11 @@ class main:
         self.pd_container.hide()
 
         self.expnumber = 0
+        
+        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
 
     def exp_param_show(self, selection):
         self.chronoamp_container.hide()
@@ -196,6 +204,18 @@ class main:
             self.serial_liststore.append([i])
 
     def on_pot_start_clicked(self, data=None):
+    
+        def exceptions(): #cleans up after errors
+            if self.dropbot_enabled == True:
+                if self.dropbot_triggered == True:
+                    self.dropbot_triggered = False
+                    print "finallydone"
+                    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)
+            
         selection = self.expcombobox.get_active()
         parameters = {}
         view_parameters = {}
@@ -247,6 +267,7 @@ class main:
                 
                 self.plot_proc = gobject.timeout_add(200, self.experiment_running_plot)
                 gobject.idle_add(self.experiment_running)
+                return
         
             elif selection == 1: #LSV
                 parameters['clean_mV'] = int(self.lsv.clean_mV.get_text())
@@ -292,6 +313,7 @@ class main:
 
                 self.plot_proc = gobject.timeout_add(200, self.experiment_running_plot)
                 gobject.idle_add(self.experiment_running)
+                return
             
             elif selection == 2: #CV
                 parameters['clean_mV'] = int(self.cv.clean_mV.get_text())
@@ -343,6 +365,7 @@ class main:
                 
                 self.plot_proc = gobject.timeout_add(200, self.experiment_running_plot)
                 gobject.idle_add(self.experiment_running)
+                return
                 
             elif selection == 3: #SWV
                 parameters['clean_mV'] = int(self.swv.clean_mV.get_text())
@@ -401,6 +424,7 @@ class main:
                 
                 self.plot_proc = gobject.timeout_add(200, self.experiment_running_plot)
                 gobject.idle_add(self.experiment_running)
+                return
         
             elif selection == 4: #DPV
                 parameters['clean_mV'] = int(self.dpv.clean_mV.get_text())
@@ -457,35 +481,27 @@ class main:
 
                 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.")
-                self.startbutton.set_sensitive(True)
-                self.stopbutton.set_sensitive(False)
+                exceptions()
                 
         except ValueError:
-            self.spinner.stop()
             self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.")
-            self.startbutton.set_sensitive(True)
-            self.stopbutton.set_sensitive(False)
+            exceptions()
         
         except InputError as e:
-            self.spinner.stop()
             self.statusbar.push(self.error_context_id, e.msg)
-            self.startbutton.set_sensitive(True)
-            self.stopbutton.set_sensitive(False)
+            exceptions()
         
         except SerialException:
-            self.spinner.stop()
             self.statusbar.push(self.error_context_id, "Could not establish serial connection.")
-            self.startbutton.set_sensitive(True)
-            self.stopbutton.set_sensitive(False)
+            exceptions()
 
         except AssertionError as e:
-            self.spinner.stop()
             self.statusbar.push(self.error_context_id, str(e))
-            self.startbutton.set_sensitive(True)
-            self.stopbutton.set_sensitive(False)
+            exceptions()
 
     def experiment_running(self):
         try:
@@ -534,7 +550,6 @@ class main:
                 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:
@@ -546,6 +561,13 @@ class main:
             save.autoPlot(self.plot, 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
+                print "expdone"
+                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)
@@ -560,7 +582,46 @@ class main:
             save_inst = save.manSave(self.current_exp)
     
     def on_file_save_plot_activate(self, menuitem, data=None):
-            save_inst = save.plotSave(self.plot)
+        save_inst = save.plotSave(self.plot)
+    
+    def on_menu_dropbot_connect_activate(self, menuitem, data=None):
+        self.microdrop = microdrop.microdropConnection()
+        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)
+    
+    def on_menu_dropbot_disconnect_activate(self, menuitem, data= None):
+        self.microdrop.reset()
+        del self.microdrop
+        self.dropbot_enabled = False
+        self.menu_dropbot_connect.set_sensitive(True)
+        self.menu_dropbot_disconnect.set_sensitive(False)
+
+    def microdrop_listen(self):
+        drdy, data = self.microdrop.listen()
+        if drdy == False:
+            return True
+        if self.microdrop.connected == False:
+            if data == microdrop.CONREQ:
+                print "INFO: µDrop connected"
+                self.statusbar.push(self.message_context_id, "µDrop connected.")
+                self.microdrop.reply(microdrop.CONREP)
+                self.microdrop.connected = True
+            else:
+                print "WAR: Invalid µDrop connection request."
+                self.microdrop.reply(microdrop.INVAL_CMD)
+        elif data == microdrop.STARTEXP:
+            self.dropbot_triggered = True
+            self.on_pot_start_clicked()
+            return False
+        else:
+            print "WAR: Received invalid command from µDrop"
+            self.microdrop.reply(microdrop.INVAL_CMD)
+        return True
+
+
 
 
 if __name__ == "__main__":
diff --git a/dstatInterface/microdrop.py b/dstatInterface/microdrop.py
new file mode 100644
index 0000000..95ea34c
--- /dev/null
+++ b/dstatInterface/microdrop.py
@@ -0,0 +1,51 @@
+import zmq
+import zmq.error
+
+#signals
+CONREQ = "0"
+CONREP = "1"
+STARTEXP = "10"
+EXPFINISHED = "11"
+INVAL_CMD = "99"
+
+#States
+RECV = 0
+SEND = 1
+
+class microdropConnection:
+
+    def __init__(self, port=6789):
+        self.port = port
+        self.connected = False
+        self.state = RECV
+    
+        self.ctx = zmq.Context()
+        self.soc = zmq.Socket(self.ctx, zmq.REP)
+        self.soc.bind("".join(['tcp://*:',str(self.port)]))
+    
+    def listen(self):
+        if self.state == SEND:
+            print "WAR: Microdrop Connection state invalid, resetting..."
+            self.reset()
+            self.__init__(self.port)
+        try:
+            message = self.soc.recv(flags=zmq.NOBLOCK, copy=True)
+            self.state = SEND
+            return (True, message)
+        except zmq.Again:
+            return (False, "")
+
+    def reply(self, data):
+        if self.state == RECV:
+            print "WAR: Microdrop Connection state invalid, resetting..."
+            self.reset()
+            self.__init__(self.port)
+            return False
+        self.state = RECV
+        self.soc.send(data)
+        return True
+        
+    def reset(self):
+        self.soc.unbind("".join(['tcp://*:',str(self.port)]))
+        del self.soc
+        del self.ctx
-- 
GitLab