From cbbb9216baa771952ef7ed08190131178114bd49 Mon Sep 17 00:00:00 2001
From: Michael DM Dryden <mdryden@chem.utoronto.ca>
Date: Mon, 3 Apr 2017 20:25:41 -0400
Subject: [PATCH] Moved serial instance into common state file.

---
 dstat_interface/dstat_comm.py                 | 204 +++++++++++++-----
 dstat_interface/experiments/cal.py            |   8 +-
 .../experiments/experiment_template.py        |   4 +-
 dstat_interface/interface/exp_int.py          |  53 ++---
 dstat_interface/main.py                       |  80 +++----
 dstat_interface/state.py                      |   4 +
 6 files changed, 218 insertions(+), 135 deletions(-)
 create mode 100644 dstat_interface/state.py

diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py
index cc81ce3..a468d47 100755
--- a/dstat_interface/dstat_comm.py
+++ b/dstat_interface/dstat_comm.py
@@ -22,24 +22,47 @@ from serial.tools import list_ports
 import time
 import struct
 import multiprocessing as mp
+from collections import OrderedDict
 import logging
 
+try:
+    import gi
+    gi.require_version('Gtk', '3.0')
+    from gi.repository import Gtk, GObject
+except ImportError:
+    print "ERR: GTK not available"
+    sys.exit(1)
+
 from errors import InputError, VarError
 
 logger = logging.getLogger("dstat.comm")
 dstat_logger = logging.getLogger("dstat.comm.DSTAT")
 exp_logger = logging.getLogger("dstat.comm.Experiment")
 
-serial_instance = None
-settings = {}
+import state
+
+class AlreadyConnectedError(Exception):
+    def __init__(self):
+        super(AlreadyConnectedError, self).__init__(self,
+            "Serial instance already connected.")
+
+class NotConnectedError(Exception):
+    def __init__(self):
+        super(NotConnectedError, self).__init__(self,
+            "Serial instance not connected.")
 
 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)
-    ser_logger.info("Reattaching DStat udc")
-    ser.write("!R") # Send restart command
-    ser.close()
+    connected = False
+    
+    try:
+        ser = delayedSerial(ser_port, baudrate=1000000, timeout=1)
+        ser_logger.info("Reattaching DStat udc")
+        # ser.write("!R") # Send restart command
+        ser.close()
+    except serial.SerialException:
+        return 1
     
     for i in range(5):
         time.sleep(1) # Give OS time to enumerate
@@ -47,9 +70,15 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe):
         try:
             ser = delayedSerial(ser_port, baudrate=1000000, timeout=1)
             ser_logger.info("Connecting")
-            break
+            connected = True
         except serial.SerialException:
             pass
+        
+        if connected is True:
+            break
+            
+    if ser.isOpen() is False:
+        return 1
     
     ser.write("ck") # Keep this to support old firmwares
     
@@ -78,7 +107,7 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe):
                     ser_logger.info("DISCONNECT")
                     ser.close()
                     proc_pipe.send("DISCONNECT")
-                    return False
+                    return 0
             
         elif proc_pipe.poll():
             while ctrl_pipe.poll():
@@ -94,19 +123,91 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe):
             
 
 
-class SerialConnection(object):
-    def __init__(self, ser_port):
-        self.proc_pipe_p, self.proc_pipe_c = mp.Pipe(duplex=True)
-        self.ctrl_pipe_p, self.ctrl_pipe_c = mp.Pipe(duplex=True)
-        self.data_pipe_p, self.data_pipe_c = mp.Pipe(duplex=True)
-    
-        self.proc = mp.Process(target=_serial_process, args=(ser_port,
-                                self.proc_pipe_c, self.ctrl_pipe_c,
-                                self.data_pipe_c))
-        self.proc.start()
+class SerialConnection(GObject.GObject):
+    __gsignals__ = {
+        'connected': (GObject.SIGNAL_RUN_FIRST, None, ()),
+        'disconnected': (GObject.SIGNAL_RUN_FIRST, None, ())
+    }
+    
+    def __init__(self):
+        super(SerialConnection, self).__init__()
+        self.connected = False
+    
+    def connect(self, ser_port):
+        if self.connected is False:
+            self.proc_pipe_p, self.proc_pipe_c = mp.Pipe(duplex=True)
+            self.ctrl_pipe_p, self.ctrl_pipe_c = mp.Pipe(duplex=True)
+            self.data_pipe_p, self.data_pipe_c = mp.Pipe(duplex=True)
+    
+            self.proc = mp.Process(target=_serial_process, args=(ser_port,
+                                    self.proc_pipe_c, self.ctrl_pipe_c,
+                                    self.data_pipe_c))
+            self.proc.start()
+            time.sleep(.5)
+            if self.proc.is_alive() is False:
+                raise ConnectionError()
+                return False
+            self.connected = True
+            self.emit('connected')
+            return True
+        else:
+            raise AlreadyConnectedError()
+            return False
+    
+    def assert_connected(self):
+        if self.connected is False:
+            raise NotConnectedError()
+    
+    def start_exp(self, exp):
+        self.assert_connected()
+        
+        self.proc_pipe_p.send(exp)
+    
+    def stop_exp(self):
+        self.assert_connected()
+        self.send_ctrl('a')
         
+    def get_proc(self, block=False):
+        self.assert_connected()
+            
+        if block is True:
+            return self.proc_pipe_p.recv()
+        else:
+            if self.proc_pipe_p.poll() is True:
+                return self.proc_pipe_p.recv()
+            else:
+                return None
+                
+    def get_data(self, block=False):
+        self.assert_connected()
+        
+        if block is True:
+            return self.data_pipe_p.recv()
+        else:
+            if self.data_pipe_p.poll() is True:
+                return self.data_pipe_p.recv()
+            else:
+                return None
+    
+    def flush_data(self):
+        self.assert_connected()
+        
+        while self.proc_pipe_p.poll() is True:
+            self.proc_pipe_p.recv()
+    
+    def send_ctrl(self, ctrl):
+        self.assert_connected()
+        
+        self.ctrl_pipe_p.send(ctrl)
+    
+    def disconnect(self):
+        self.send_ctrl('a')
+        time.sleep(.2)
+        self.proc.terminate()
+        self.emit('disconnected')
+        self.connected = False
 
-class VersionCheck:
+class VersionCheck(object):
     def __init__(self):
         pass
         
@@ -160,24 +261,24 @@ def version_check(ser_port):
     Arguments:
     ser_port -- address of serial port to use
     """
-    try:        
-        global serial_instance
-        serial_instance = SerialConnection(ser_port)
-        
-        serial_instance.proc_pipe_p.send(VersionCheck())
-        result = serial_instance.proc_pipe_p.recv()
-        if result == "SERIAL_ERROR":
-            buffer = 1
-        else:
-            buffer = serial_instance.data_pipe_p.recv()
-        logger.debug("version_check done")
-        
-        return buffer
+    # try:     
+    state.ser = SerialConnection()
+    
+    state.ser.connect(ser_port)
+    state.ser.start_exp(VersionCheck())
+    result = state.ser.get_proc(block=True)
+    if result == "SERIAL_ERROR":
+        buffer = 1
+    else:
+        buffer = state.ser.get_data(block=True)
+    logger.debug("version_check done")
+    
+    return buffer
         
-    except:
-        pass
+    # except:
+    #     pass
 
-class Settings:
+class Settings(object):
     def __init__(self, task, settings=None):
         self.task = task
         self.settings = settings
@@ -200,7 +301,7 @@ class Settings:
         return status
         
     def read(self):
-        settings = {}
+        settings = OrderedDict()
         
         self.ser.flushInput()
         self.ser.write('!')
@@ -252,16 +353,11 @@ def read_settings():
     settings.
     """
     
-    global settings
-    settings = {}
-    
-    while serial_instance.data_pipe_p.poll():
-        serial_instance.data_pipe_p.recv()
-    
-    serial_instance.proc_pipe_p.send(Settings(task='r'))
-    settings = serial_instance.data_pipe_p.recv()
+    state.ser.flush_data()
+    state.ser.start_exp(Settings(task='r'))
+    state.settings = state.ser.get_data(block=True)
     
-    logger.debug("read_settings: %s", serial_instance.proc_pipe_p.recv())
+    logger.debug("read_settings: %s", state.ser.get_proc(block=True))
     
     return
     
@@ -269,12 +365,10 @@ def write_settings():
     """Tries to write settings to DStat from global settings var.
     """
     
-    while serial_instance.data_pipe_p.poll():
-        serial_instance.data_pipe_p.recv()
+    state.ser.flush_data()
+    state.ser.start_exp(Settings(task='w', settings=state.settings))
     
-    serial_instance.proc_pipe_p.send(Settings(task='w', settings=settings))
-    
-    logger.debug("write_settings: %s", serial_instance.proc_pipe_p.recv())
+    logger.debug("write_settings: %s", state.ser.get_proc(block=True))
     
     return
     
@@ -317,14 +411,12 @@ def read_light_sensor():
     light sensor clear channel.
     """
     
-    while serial_instance.data_pipe_p.poll():
-        serial_instance.data_pipe_p.recv()
-        
-    serial_instance.proc_pipe_p.send(LightSensor())
+    state.ser.flush_data()
+    state.ser.start_exp(LightSensor())
     
-    logger.info("read_light_sensor: %s", serial_instance.proc_pipe_p.recv())
+    logger.debug("read_light_sensor: %s", state.ser.get_proc(block=True))
     
-    return serial_instance.data_pipe_p.recv()
+    return state.ser.get_data(block=True)
     
 
 class delayedSerial(serial.Serial): 
diff --git a/dstat_interface/experiments/cal.py b/dstat_interface/experiments/cal.py
index f57c26f..9f965b7 100755
--- a/dstat_interface/experiments/cal.py
+++ b/dstat_interface/experiments/cal.py
@@ -28,7 +28,7 @@ import serial
 
 logger = logging.getLogger("dstat.experiments.cal")
 
-from dstat_comm import serial_instance
+import state
 from experiments.experiment_template import Experiment
 
 def measure_offset(time):
@@ -42,9 +42,9 @@ def measure_offset(time):
     
     for i in range(1,8):
         parameters['gain'] = i
-        serial_instance.proc_pipe_p.send(CALExp(parameters))
-        logger.info("measure_offset: %s", serial_instance.proc_pipe_p.recv())
-        gain_offset[gain_trim_table[i]] = serial_instance.data_pipe_p.recv()
+        state.ser.start_exp(CALExp(parameters))
+        logger.info("measure_offset: %s", state.ser.get_proc(block=True))
+        gain_offset[gain_trim_table[i]] = state.ser.get_data(block=True)
         
     return gain_offset
 
diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py
index 96888d4..47a49a1 100755
--- a/dstat_interface/experiments/experiment_template.py
+++ b/dstat_interface/experiments/experiment_template.py
@@ -53,7 +53,7 @@ dstat_logger = logging.getLogger("dstat.comm.DSTAT")
 exp_logger = logging.getLogger("dstat.comm.Experiment")
 
 from errors import InputError, VarError
-import dstat_comm
+import state
 
 class Experiment(object):
     """Store and acquire a potentiostat experiment. Meant to be subclassed
@@ -87,7 +87,7 @@ class Experiment(object):
             
         self.gain = self.__gaintable[int(self.parameters['gain'])]
         self.gain_trim = int(
-            dstat_comm.settings[
+            state.settings[
                 self.__gain_trim_table[int(self.parameters['gain'])]
             ][1]
         )
diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py
index caa4a58..3e1cb67 100755
--- a/dstat_interface/interface/exp_int.py
+++ b/dstat_interface/interface/exp_int.py
@@ -31,6 +31,7 @@ except ImportError:
     sys.exit(1)
 
 import dstat_comm
+import state
 import experiments as exp
 import experiments.cal as cal
 import __main__
@@ -406,11 +407,11 @@ class PD(ExpInterface):
             self.builder.get_object('light_label').set_text(str(
                 dstat_comm.read_light_sensor()))
             dstat_comm.read_settings()
-            dstat_comm.settings['tcs_enabled'][1] = '1' # Make sure TCS enabled
+            state.settings['tcs_enabled'][1] = '1' # Make sure TCS enabled
             dstat_comm.write_settings()
             
             self.builder.get_object('threshold_entry').set_text(str(
-                                    dstat_comm.settings['tcs_clear_threshold'][1]))   
+                                    state.settings['tcs_clear_threshold'][1]))   
             __main__.MAIN.start_ocp()
             
         finally:
@@ -424,12 +425,12 @@ class PD(ExpInterface):
             i.set_sensitive(False)
             
         try:
-            dstat_comm.settings['tcs_clear_threshold'][1] = self.builder.get_object(
+            state.settings['tcs_clear_threshold'][1] = self.builder.get_object(
                                                     'threshold_entry').get_text()
             dstat_comm.write_settings()
             dstat_comm.read_settings()
             self.builder.get_object('threshold_entry').set_text(
-                                str(dstat_comm.settings['tcs_clear_threshold'][1]))   
+                                str(state.settings['tcs_clear_threshold'][1]))   
             __main__.MAIN.start_ocp()
         
         finally:
@@ -487,19 +488,19 @@ class CAL(ExpInterface):
             dstat_comm.read_settings()
     
             self.entry['R100'].set_text(str(
-                dstat_comm.settings['r100_trim'][1]))
+                state.settings['r100_trim'][1]))
             self.entry['R3k'].set_text(str(
-                dstat_comm.settings['r3k_trim'][1]))
+                state.settings['r3k_trim'][1]))
             self.entry['R30k'].set_text(str(
-                dstat_comm.settings['r30k_trim'][1]))
+                state.settings['r30k_trim'][1]))
             self.entry['R300k'].set_text(str(
-                dstat_comm.settings['r300k_trim'][1]))
+                state.settings['r300k_trim'][1]))
             self.entry['R3M'].set_text(str(
-                dstat_comm.settings['r3M_trim'][1]))
+                state.settings['r3M_trim'][1]))
             self.entry['R30M'].set_text(str(
-                dstat_comm.settings['r30M_trim'][1]))
+                state.settings['r30M_trim'][1]))
             self.entry['R100M'].set_text(str(
-                dstat_comm.settings['r100M_trim'][1]))
+                state.settings['r100M_trim'][1]))
     
             __main__.MAIN.start_ocp()
             
@@ -514,13 +515,13 @@ class CAL(ExpInterface):
             __main__.MAIN.on_pot_stop_clicked()
             __main__.MAIN.stop_ocp()
             
-            dstat_comm.settings['r100_trim'][1] = self.entry['R100'].get_text()
-            dstat_comm.settings['r3k_trim'][1] = self.entry['R3k'].get_text()
-            dstat_comm.settings['r30k_trim'][1] = self.entry['R30k'].get_text()
-            dstat_comm.settings['r300k_trim'][1] = self.entry['R300k'].get_text()
-            dstat_comm.settings['r3M_trim'][1] = self.entry['R3M'].get_text()
-            dstat_comm.settings['r30M_trim'][1] = self.entry['R30M'].get_text()
-            dstat_comm.settings['r100M_trim'][1] = self.entry['R100M'].get_text()
+            state.settings['r100_trim'][1] = self.entry['R100'].get_text()
+            state.settings['r3k_trim'][1] = self.entry['R3k'].get_text()
+            state.settings['r30k_trim'][1] = self.entry['R30k'].get_text()
+            state.settings['r300k_trim'][1] = self.entry['R300k'].get_text()
+            state.settings['r3M_trim'][1] = self.entry['R3M'].get_text()
+            state.settings['r30M_trim'][1] = self.entry['R30M'].get_text()
+            state.settings['r100M_trim'][1] = self.entry['R100M'].get_text()
             dstat_comm.write_settings()        
                                 
             __main__.MAIN.start_ocp()
@@ -544,22 +545,22 @@ class CAL(ExpInterface):
             
             for i in offset:
                 logger.info("{} {}".format(i, str(-offset[i])))
-                dstat_comm.settings[i][1] = str(-offset[i])
+                state.settings[i][1] = str(-offset[i])
             
             self.entry['R100'].set_text(str(
-                dstat_comm.settings['r100_trim'][1]))
+                state.settings['r100_trim'][1]))
             self.entry['R3k'].set_text(str(
-                dstat_comm.settings['r3k_trim'][1]))
+                state.settings['r3k_trim'][1]))
             self.entry['R30k'].set_text(str(
-                dstat_comm.settings['r30k_trim'][1]))
+                state.settings['r30k_trim'][1]))
             self.entry['R300k'].set_text(str(
-                dstat_comm.settings['r300k_trim'][1]))
+                state.settings['r300k_trim'][1]))
             self.entry['R3M'].set_text(str(
-                dstat_comm.settings['r3M_trim'][1]))
+                state.settings['r3M_trim'][1]))
             self.entry['R30M'].set_text(str(
-                dstat_comm.settings['r30M_trim'][1]))
+                state.settings['r30M_trim'][1]))
             self.entry['R100M'].set_text(str(
-                dstat_comm.settings['r100M_trim'][1]))
+                state.settings['r100M_trim'][1]))
             __main__.MAIN.start_ocp()
         
         finally:
diff --git a/dstat_interface/main.py b/dstat_interface/main.py
index f30fbff..5621e2e 100755
--- a/dstat_interface/main.py
+++ b/dstat_interface/main.py
@@ -59,6 +59,7 @@ import params
 import parameter_test
 import analysis
 import zmq
+import state
 from errors import InputError
 
 from plugin import DstatPlugin, get_hub_uri
@@ -258,8 +259,8 @@ class Main(object):
                 self.stop_ocp()
             else:
                 self.on_pot_stop_clicked()
-            comm.serial_instance.ctrl_pipe_p.send("DISCONNECT")
-            comm.serial_instance.proc.terminate()
+            state.ser.send_ctrl("DISCONNECT")
+            state.ser.disconnect()
 
         except AttributeError as err:
             logger.warning("AttributeError: %s", err)
@@ -284,16 +285,15 @@ class Main(object):
 
         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()
+            state.ser.flush_data()
 
             if self.pmt_mode == True:
                 logger.info("Start PMT idle mode")
-                comm.serial_instance.proc_pipe_p.send(exp.PMTIdle())
+                state.ser.start_exp(exp.PMTIdle())
 
             else:
                 logger.info("Start OCP")
-                comm.serial_instance.proc_pipe_p.send(exp.OCPExp())
+                state.ser.start_exp(exp.OCPExp())
 
             self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data),
                              GObject.timeout_add(250, self.ocp_running_proc)
@@ -312,7 +312,7 @@ class Main(object):
                 logger.info("Stop PMT idle mode")
             else:
                 logger.info("Stop OCP")
-            comm.serial_instance.ctrl_pipe_p.send('a')
+            state.ser.send_ctrl('a')
 
             for i in self.ocp_proc:
                 GObject.source_remove(i)
@@ -334,9 +334,8 @@ class Main(object):
         """
 
         try:
-            if comm.serial_instance.data_pipe_p.poll():
-                incoming = comm.serial_instance.data_pipe_p.recv()
-
+            incoming = state.ser.get_data()
+            while incoming is not None:
                 if isinstance(incoming, basestring): # test if incoming is str
                     self.on_serial_disconnect_clicked()
                     return False
@@ -346,9 +345,7 @@ class Main(object):
                                 " V"])
                 self.ocp_disp.set_text(data)
 
-                if comm.serial_instance.data_pipe_p.poll():
-                    self.ocp_running_data()
-                return True
+                incoming = state.ser.get_data()
 
             return True
 
@@ -367,19 +364,16 @@ class Main(object):
         """
 
         try:
-            if comm.serial_instance.proc_pipe_p.poll():
-                proc_buffer = comm.serial_instance.proc_pipe_p.recv()
+            proc_buffer = state.ser.get_proc()
+            while proc_buffer is not None:
                 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()
+                    state.ser.flush_data()
                     return False
-
-                return True
-
+                proc_buffer = state.ser.get_proc()
             return True
 
         except EOFError:
@@ -417,8 +411,7 @@ class Main(object):
         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()
+        state.ser.flush_data()
 
         parameters = {}
         parameters['version'] = self.version
@@ -454,11 +447,10 @@ class Main(object):
             self.data_view.clear_exps()
             self.info_page.clear()
             
-            comm.serial_instance.proc_pipe_p.send(self.current_exp)
+            state.ser.start_exp(self.current_exp)
 
             # Flush data pipe
-            while comm.serial_instance.data_pipe_p.poll():
-                comm.serial_instance.data_pipe_p.recv()
+            state.ser.flush_data()
 
             self.plot_proc = GObject.timeout_add(200,
                                                 self.experiment_running_plot)
@@ -515,22 +507,19 @@ class Main(object):
             function from GTK's queue.
         """
         try:
-            if comm.serial_instance.data_pipe_p.poll():
-                incoming = comm.serial_instance.data_pipe_p.recv()
-
-                self.line, data = incoming
-                if self.line > self.lastdataline:
-                    newline = True
-                    self.lastdataline = self.line
-                else:
-                    newline = False
-                
-                self.current_exp.store_data(incoming, newline)
-                
-                if comm.serial_instance.data_pipe_p.poll():
-                    self.experiment_running_data()
-                return True
-
+            incoming = state.ser.get_data()
+            while incoming is not None:
+                try:
+                    self.line, data = incoming
+                    if self.line > self.lastdataline:
+                        newline = True
+                        self.lastdataline = self.line
+                    else:
+                        newline = False
+                    self.current_exp.store_data(incoming, newline)
+                except TypeError:
+                    pass
+                incoming = state.ser.get_data()
             return True
 
         except EOFError as err:
@@ -552,9 +541,8 @@ class Main(object):
             function from GTK's queue.
         """
         try:
-            if comm.serial_instance.proc_pipe_p.poll():
-                proc_buffer = comm.serial_instance.proc_pipe_p.recv()
-
+            proc_buffer = state.ser.get_proc()
+            if proc_buffer is not None:
                 if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]:
                     self.experiment_done()
                     if proc_buffer == "SERIAL_ERROR":
@@ -563,9 +551,7 @@ class Main(object):
                 else:
                     logger.warning("Unrecognized experiment return code: %s",
                                    proc_buffer)
-
                 return False
-
             return True
 
         except EOFError as err:
@@ -687,7 +673,7 @@ class Main(object):
     def on_pot_stop_clicked(self, data=None):
         """Stop current experiment. Signals experiment process to stop."""
         try:
-            comm.serial_instance.ctrl_pipe_p.send('a')
+            state.ser.stop_exp()
 
         except AttributeError:
             pass
diff --git a/dstat_interface/state.py b/dstat_interface/state.py
new file mode 100644
index 0000000..74295bd
--- /dev/null
+++ b/dstat_interface/state.py
@@ -0,0 +1,4 @@
+from collections import OrderedDict
+
+settings = OrderedDict()
+ser = None
\ No newline at end of file
-- 
GitLab