diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/CHANGELOG b/CHANGELOG index c0d2d5eb13149c3018fc3bec054e154865007e60..0c5a35d10f4a465a4937118f19600b5760e7de8c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,34 +1,35 @@ -Version 1.0.2 - -Improved logging system: Log messages now print showing where they came from. - -Implemented gobject IO callbacks for experiments: - Process will not continuously poll for new data from serial process anymore. - -Stop button works again - -Buttons in Photodiode and Calibration modules remain insensitive until ready. +Version 1.3.3 + -Bugfix #24: Remove ZODB support until it can be fixed for latest ZODB -Version 1.0.2a - -Hotfix #12: Restored measurement ability on Windows - -Minor logging changes - -Automatically enable TCS on DStat when measure light sensor button clicked. +Version 1.3.2 + -Improves initial connection reliability + +Version 1.3.1 + -Fixed electrochem modes broken when database added + -Make metadata keys optional. -Version 1.0.3 - -Fixed #14: Added support for PMT idle mode - -Reduced CPU usage when running OCP by reducing polling frequency +Version 1.3 + -Fixed a bug related to calibration mode + -Added ZODB data storage + -Integrated with zmq_plugin + +Version 1.2 + -Old Microdrop interface depreciated + -New zmq_plugin based interface + -Internal changes to save functionality and plot storage. -Version 1.0.4 - -Adds support for synchronous electromechanical shutter detection (added in dstat/dstat-firmware@29d4c86) - -New version string system +Version 1.1.3 + -Changed internal storage of experiment data + -Added Analysis options: + -FFT integral moved there + -Basic statistics -Version 1.0.5 - -Bugfix for saving error introduced by new logging system - -Version 1.0.6 - -Automatically integrates shutter FFT peak and saves to data file - -Adds option to offset start of FFT to avoid PMT startup delay - -Version 1.0.7 - -Fixed a few bugs for systems without git - -Implements mean crossing detection instead of windowing for shutter FFT +Version 1.1.2 + -Fixed more critical bugs from refactoring +Version 1.1.1 + -Fixed critical bug that made PGA setting change with Gain resistor + Version 1.1 -Plot will be prettier if seaborn is installed -Fixed bug in shutter FFT display @@ -36,15 +37,35 @@ Version 1.1 -Adds requirement for yaml -Parameters automatically saved and loaded from last session -Can manually save and load parameter files + +Version 1.0.7 + -Fixed a few bugs for systems without git + -Implements mean crossing detection instead of windowing for shutter FFT -Version 1.1.1 - -Fixed critical bug that made PGA setting change with Gain resistor - -Version 1.1.2 - -Fixed more critical bugs from refactoring +Version 1.0.6 + -Automatically integrates shutter FFT peak and saves to data file + -Adds option to offset start of FFT to avoid PMT startup delay -Version 1.1.3 - -Changed internal storage of experiment data - -Added Analysis options: - -FFT integral moved there - -Basic statistics \ No newline at end of file + +Version 1.0.5 + -Bugfix for saving error introduced by new logging system + +Version 1.0.4 + -Adds support for synchronous electromechanical shutter detection (added in dstat/dstat-firmware@29d4c86) + -New version string system + +Version 1.0.3 + -Fixed #14: Added support for PMT idle mode + -Reduced CPU usage when running OCP by reducing polling frequency + +Version 1.0.2a + -Hotfix #12: Restored measurement ability on Windows + -Minor logging changes + -Automatically enable TCS on DStat when measure light sensor button clicked. + +Version 1.0.2 + -Improved logging system: Log messages now print showing where they came from. + -Implemented gobject IO callbacks for experiments: + Process will not continuously poll for new data from serial process anymore. + -Stop button works again + -Buttons in Photodiode and Calibration modules remain insensitive until ready. diff --git a/README.markdown b/README.markdown index 9e15e55a84c8117823b07918efbb169e9cbf7884..c4283474369e404adc878a738d2a9ff272ba8b4b 100644 --- a/README.markdown +++ b/README.markdown @@ -9,17 +9,22 @@ It currently has no abilities for analyzing recorded data or opening previously ## Table of Contents: 1. [Installation](#Installation) - 1. [Prerequisites](#Prerequisites) + 1. [Manual Install](#manual-install) + 1. [pip Install](#pip-install) 2. [Getting Started](#Getting-Started) # Introduction The DStat interface is written primarily in Python and runs on Linux, Mac, and Windows. It is the main method for running experiments on the DStat, controlling experimental parameters and collecting and plotting data. It currently has no abilities for analyzing recorded data or opening previously saved data files, but data is saved in a simple text format or numpy-compatible binary format and plots can be saved as images. + +*New in version 1.3:* dstat-interface can now save all data files to a ZODB database for later analysis. +The old autosave functionality has still been retained. + # Installation Unfortunately, due to the python packages used, dstat-interface is difficult to make into a single self-contained package, so for the time being, the simplest way to run it is to install a python distribution. dstat-interface itself, therefore, requires no installation and can be run from any directory by executing `/dstat-interface/main.py` with python. -## Prerequisites +## Manual Install Python and related packages needed: (versions listed are tested, older versions may still work) @@ -64,13 +69,40 @@ The final requirements, can be installed using python's pip system: pip install pyserial pyzmq pyyaml seaborn ### Windows +**These instructions are tricky on Windows, see the [pip install](#pip-install) below for an easier alternative.** + While it is possible to install a bare python distribution and install the required prerequisites separately, [Python(x,y)](https://code.google.com/p/pythonxy/wiki/Downloads) has a python 2.7 distribution that already contains most of the necessary packages. However, pyserial is not installed in the recommended install so it should be manually selected or the full install done instead (tested with 2.7.9.0). The newest versions of Python(x,y) are also missing PyGTK, so it should be installed from [here](http://ftp.gnome.org/pub/GNOME/binaries/win32/pygtk/2.24/pygtk-all-in-one-2.24.2.win32-py2.7.msi) once Python(x,y) is installed. Matplotlib should then be reinstalled to get gtk support from [here](https://downloads.sourceforge.net/project/matplotlib/matplotlib/matplotlib-1.4.3/windows/matplotlib-1.4.3.win32-py2.7.exe). -## Experimental pip install +## pip Install Tagged git versions are uploaded to [PiPy](https://pypi.python.org/pypi/dstat-interface) regularly, and thus dstat-interface can be installed using the command `pip install dstat-interface`, which will attempt to automatically install matplotlib, numpy, pyserial, and pyzmq. (N.B. matplotlib does not install well with pip on Mac and should be manually installed with Homebrew as described above) -This is still experimental as dstat-interface cannot be launched as a module for compatibility with multiprocessing on Windows and pygtk and pygobject must be installed manually as described above. +This is still a bit experimental as pygtk and pygobject must be installed separately. + +To launch a pip-installed dstat-interface, simply type: + + python -m dstat_interface.main + +into a terminal. + +### Windows +The following terminal commands will result in a full installation of dstat-interface and its requirements, assuming [32-bit Miniconda][1] is installed: + + conda create -n dstat python pywin32 + activate dstat + pip install --find-links http://192.99.4.95/wheels --trusted-host 192.99.4.95 scipy==0.17.0 pygtk2-win pycairo-gtk2-win dstat-interface + +This makes use of pre-built binary wheels for many of the Windows packages, stored on our server. +We are installing in a separate environment to keep a clean system. +`activate dstat` will enter the environment (must be done whenever a new terminal is opened), +and `deactivate` will return to the root environment. + +Therefore, to run dstat-interface from our environment, we must first activate it (if not already done) before launching it: + + activate dstat + python -m dstat_interface.main + +[1]: https://repo.continuum.io/miniconda/Miniconda2-latest-Windows-x86.exe # Getting started ## Interface overview @@ -133,4 +165,4 @@ If the connection failed, unplug the DStat and try again. 3. Set an appropriate potentiostat gain. 4. Click Execute. -![experiment](images/3.png) \ No newline at end of file +![experiment](images/3.png) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index 33144c068e58bb9184991549bc0b0d5865e96e22..3190989961710fef302ce07853761fe666b4d331 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -22,8 +22,6 @@ Functions for analyzing data. """ import logging -import pygtk -import gtk from numpy import mean, trapz logger = logging.getLogger('dstat.analysis') @@ -85,11 +83,9 @@ class AnalysisOptions(object): @params.setter def params(self, params): - try: - for key in self._params: + for key in self._params: + if key in params: self._params[key] = params[key] - except KeyError as e: - logger.warning("Missing parameter - %s" % e) self.stats_button.set_active(self._params['stats_true']) self.stats_start_button.set_active(self._params['stats_start_true']) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index ffa43f2217b119b21acd8e2bce98f4edb03cb13d..b50aa6434b3b3070d37bfacaebe3a89807c4b920 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -22,32 +22,86 @@ 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") +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.") + +class ConnectionError(Exception): + def __init__(self): + super(ConnectionError, self).__init__(self, + "Could not connect.") + + 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) + connected = False - ser_logger.info("Connecting") + try: + ser = serial.Serial(ser_port, timeout=1) + time.sleep(2) + ser.reset_input_buffer() + ser_logger.info("Reattaching DStat udc") + # ser.write("!R") # Send restart command + ser.close() + except serial.SerialException: + return 1 - ser.write("ck") + for i in range(5): + time.sleep(1) # Give OS time to enumerate + + try: + ser = serial.Serial(ser_port, timeout=1) + ser_logger.info("Connecting") + time.sleep(2) + connected = True + except serial.SerialException: + pass + + if connected is True: + break + + if ser.isOpen() is False: + ser_logger.info("Connection Error") + return 1 - ser.flushInput() - ser.write('!') + ser.write('!0 ') for i in range(10): - if not ser.read()=="C": - time.sleep(.5) - ser.write('!') + if ser.readline().rstrip()=="@ACK 0": + if ser.readline().rstrip()=="@RCV 0": + break else: - break + time.sleep(.5) + ser.reset_input_buffer() + ser.write('!0 ') + time.sleep(.1) + while True: # These can only be called when no experiment is running @@ -63,13 +117,16 @@ 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(): ctrl_pipe.recv() - - return_code = proc_pipe.recv().run(ser, ctrl_pipe, data_pipe) + try: + return_code = proc_pipe.recv().run(ser, ctrl_pipe, data_pipe) + except serial.SerialException: + proc_pipe.send("DISCONNECT") + return 0 ser_logger.info('Return code: %s', str(return_code)) proc_pipe.send(return_code) @@ -79,19 +136,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) +class SerialConnection(GObject.Object): + __gsignals__ = { + 'connected': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'disconnected': (GObject.SIGNAL_RUN_FIRST, None, ()) + } - 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() + 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(2) + 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.data_pipe_p.poll() is True: + self.data_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 @@ -103,15 +232,29 @@ class VersionCheck: ser_port -- address of serial port to use """ try: - ser.write('V') + ser.reset_input_buffer() + ser.write('!1\n') + + for i in range(10): + if ser.readline().rstrip()=="@ACK 1": + ser.write('V\n') + if ser.readline().rstrip()=="@RCV 1": + break + else: + time.sleep(.5) + ser.reset_input_buffer() + ser.write('!1\n') + time.sleep(.1) + for line in ser: + dstat_logger.info(line) if line.startswith('V'): input = line.lstrip('V') elif line.startswith("#"): dstat_logger.info(line.lstrip().rstrip()) - elif line.lstrip().startswith("no"): + elif line.lstrip().startswith("@DONE"): dstat_logger.debug(line.lstrip().rstrip()) - ser.flushInput() + ser.reset_input_buffer() break parted = input.rstrip().split('.') @@ -137,25 +280,23 @@ 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 - - except: - pass + """ + 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") + time.sleep(.1) + + return buffer + -class Settings: +class Settings(object): def __init__(self, task, settings=None): self.task = task self.settings = settings @@ -178,26 +319,31 @@ class Settings: return status def read(self): - settings = {} - - self.ser.flushInput() - self.ser.write('!') + settings = OrderedDict() + self.ser.reset_input_buffer() + self.ser.write('!2\n') + + for i in range(10): + if self.ser.readline().rstrip()=="@ACK 2": + self.ser.write('SR\n') + if self.ser.readline().rstrip()=="@RCV 2": + break + else: + time.sleep(.5) + self.ser.reset_input_buffer() + self.ser.write('!2\n') + time.sleep(.1) - while not self.ser.read()=="C": - time.sleep(.5) - self.ser.write('!') - - self.ser.write('SR') for line in self.ser: if line.lstrip().startswith('S'): input = line.lstrip().lstrip('S') elif line.lstrip().startswith("#"): dstat_logger.info(line.lstrip().rstrip()) - elif line.lstrip().startswith("no"): + elif line.lstrip().startswith("@DONE"): dstat_logger.debug(line.lstrip().rstrip()) - self.ser.flushInput() + self.ser.reset_input_buffer() break - + parted = input.rstrip().split(':') for i in range(len(parted)): @@ -206,40 +352,46 @@ class Settings: return settings def write(self): - self.ser.flushInput() - self.ser.write('!') - - while not self.ser.read()=="C": - time.sleep(.5) - self.ser.write('!') - write_buffer = range(len(self.settings)) - + for i in self.settings: # make sure settings are in right order write_buffer[self.settings[i][0]] = self.settings[i][1] - self.ser.write('SW') - for i in write_buffer: - self.ser.write(i) - self.ser.write(' ') + to_write = " ".join(write_buffer) + " " + n = len(to_write) - return + self.ser.reset_input_buffer() + self.ser.write('!{}\n'.format(n)) + + for i in range(10): + if self.ser.readline().rstrip()=="@ACK {}".format(n): + self.ser.write('SW\n') + if self.ser.readline().rstrip()=="@RCV {}".format(n): + break + else: + time.sleep(.5) + self.ser.reset_input_buffer() + self.ser.write('!{}\n'.format(n)) + time.sleep(.1) + + for line in self.ser: + if line.lstrip().startswith("#"): + dstat_logger.info(line.lstrip().rstrip()) + elif line.lstrip().startswith("@DONE"): + dstat_logger.debug(line.lstrip().rstrip()) + self.ser.reset_input_buffer() + break def read_settings(): """Tries to contact DStat and get settings. Returns dict of 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 @@ -247,12 +399,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 @@ -265,13 +415,12 @@ class LightSensor: light sensor clear channel. """ - ser.flushInput() + ser.reset_input_buffer() ser.write('!') - while not ser.read()=="C": - time.sleep(.5) + while not ser.read()=="@": + self.ser.reset_input_buffer() ser.write('!') - ser.write('T') for line in ser: @@ -279,9 +428,9 @@ class LightSensor: input = line.lstrip().lstrip('T') elif line.lstrip().startswith("#"): dstat_logger.info(line.lstrip().rstrip()) - elif line.lstrip().startswith("no"): + elif line.lstrip().startswith("@DONE"): dstat_logger.debug(line.lstrip().rstrip()) - ser.flushInput() + ser.reset_input_buffer() break parted = input.rstrip().split('.') @@ -296,24 +445,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()) - - logger.info("read_light_sensor: %s", serial_instance.proc_pipe_p.recv()) + state.ser.flush_data() + state.ser.start_exp(LightSensor()) - return serial_instance.data_pipe_p.recv() + logger.debug("read_light_sensor: %s", state.ser.get_proc(block=True)) - -class delayedSerial(serial.Serial): - """Extends Serial.write so that characters are output individually - with a slight delay - """ - def write(self, data): - for i in data: - serial.Serial.write(self, i) - time.sleep(.001) + return state.ser.get_data(block=True) class SerialDevices(object): """Retrieves and stores list of serial devices in self.ports""" @@ -326,562 +463,4 @@ class SerialDevices(object): def refresh(self): """Refreshes list of ports.""" - self.ports, _, _ = zip(*list_ports.comports()) - -class Experiment(object): - """Store and acquire a potentiostat experiment. Meant to be subclassed - to by different experiment types and not used instanced directly. - """ - - def __init__(self, parameters): - """Adds commands for gain and ADC.""" - self.parameters = parameters - self.databytes = 8 - self.scan = 0 - self.time = 0 - self.plots = {} - self.data = {} - - # list of scans, tuple of dimensions, list of data - self.data['data'] = [([], [])] - self.line_data = ([], []) - - major, minor = self.parameters['version'] - - if major >= 1: - if minor == 1: - self.__gaintable = [1e2, 3e2, 3e3, 3e4, 3e5, 3e6, 3e7, 5e8] - elif minor >= 2: - self.__gaintable = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] - self.__gain_trim_table = ['r100_trim', 'r100_trim', 'r3k_trim', - 'r30k_trim', 'r300k_trim', 'r3M_trim', - 'r30M_trim', 'r100M_trim'] - else: - raise VarError(parameters['version'], "Invalid version parameter.") - - self.gain = self.__gaintable[int(self.parameters['gain'])] - self.gain_trim = int( - settings[self.__gain_trim_table[int(self.parameters['gain'])]][1]) - - self.commands = ["EA", "EG"] - - if self.parameters['buffer_true']: - self.commands[0] += "2" - else: - self.commands[0] += "0" - self.commands[0] += " " - self.commands[0] += (self.parameters['adc_rate']) - self.commands[0] += " " - self.commands[0] += (self.parameters['adc_pga']) - self.commands[0] += " " - self.commands[1] += (self.parameters['gain']) - self.commands[1] += " " - self.commands[1] += (str(int(self.parameters['short_true']))) - self.commands[1] += " " - - def run(self, ser, ctrl_pipe, data_pipe): - """Execute experiment. Connects and sends handshake signal to DStat - then sends self.commands. Don't call directly as a process in Windows, - use run_wrapper instead. - """ - self.serial = ser - self.ctrl_pipe = ctrl_pipe - self.data_pipe = data_pipe - - exp_logger.info("Experiment running") - - try: - self.serial.flushInput() - status = "DONE" - - for i in self.commands: - logger.info("Command: %s", i) - self.serial.write('!') - - while not self.serial.read().startswith("C"): - pass - - self.serial.write(i) - if not self.serial_handler(): - status = "ABORT" - - self.data_postprocessing() - except serial.SerialException: - status = "SERIAL_ERROR" - finally: - while self.ctrl_pipe.poll(): - self.ctrl_pipe.recv() - return status - - def serial_handler(self): - """Handles incoming serial transmissions from DStat. Returns False - if stop button pressed and sends abort signal to instrument. Sends - data to self.data_pipe as result of self.data_handler). - """ - scan = 0 - try: - while True: - if self.ctrl_pipe.poll(): - input = self.ctrl_pipe.recv() - logger.debug("serial_handler: %s", input) - if input == ('a' or "DISCONNECT"): - self.serial.write('a') - 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.info("serial_handler: ABORT pressed!") - return False - - if line.startswith('B'): - data = self.data_handler( - (scan, self.serial.read(size=self.databytes))) - self.data_pipe.send(data) - - elif line.lstrip().startswith('S'): - scan += 1 - - elif line.lstrip().startswith("#"): - dstat_logger.info(line.lstrip().rstrip()) - - elif line.lstrip().startswith("no"): - dstat_logger.debug(line.lstrip().rstrip()) - self.serial.flushInput() - return True - - except serial.SerialException: - return False - - - def data_handler(self, data_input): - """Takes data_input as tuple -- (scan, data). - Returns: - (scan number, (voltage, current)) -- voltage in mV, current in A - """ - scan, data = data_input - voltage, current = struct.unpack(' 32767): - sum = 32767 - elif (sum < -32768): - sum = -32768 - - self.data_pipe.send(sum) - -class Chronoamp(Experiment): - """Chronoamperometry experiment""" - def __init__(self, parameters): - super(Chronoamp, self).__init__(parameters) - - self.datatype = "linearData" - self.xlabel = "Time (s)" - self.ylabel = "Current (A)" - self.datalength = 2 - self.databytes = 8 - self.xmin = 0 - self.xmax = 0 - - for i in self.parameters['time']: - self.xmax += int(i) - - self.commands += "E" - self.commands[2] += "R" - self.commands[2] += str(len(self.parameters['potential'])) - self.commands[2] += " " - for i in self.parameters['potential']: - self.commands[2] += str(int(i*(65536./3000)+32768)) - self.commands[2] += " " - for i in self.parameters['time']: - self.commands[2] += str(i) - self.commands[2] += " " - self.commands[2] += "0 " # disable photodiode interlock - - 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, current = struct.unpack(' +# +# +# 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 . + +import time +import struct +import logging + +from errors import InputError, VarError + +import serial + +logger = logging.getLogger("dstat.experiments.cal") + +import state +from experiments.experiment_template import Experiment, dstat_logger + +def measure_offset(time): + gain_trim_table = [None, 'r100_trim', 'r3k_trim', 'r30k_trim', 'r300k_trim', + 'r3M_trim', 'r30M_trim', 'r100M_trim'] + + parameters = {} + parameters['time'] = time + + gain_offset = {} + + for i in range(1,8): + parameters['gain'] = i + 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 + +class CALExp(Experiment): + id = 'cal' + """Offset calibration experiment""" + def __init__(self, parameters): + self.parameters = parameters + self.databytes = 8 + self.scan = 0 + self.data = [] + + self.commands = ["EA2 3 1 ", "EG", "ER"] + + self.commands[1] += str(self.parameters['gain']) + self.commands[1] += " " + self.commands[1] += "0 " + self.commands[2] += "1 32768 " + self.commands[2] += str(self.parameters['time']) + self.commands[2] += " " + self.commands[2] += "0 " # disable photodiode interlock + + def serial_handler(self): + """Handles incoming serial transmissions from DStat. Returns False + if stop button pressed and sends abort signal to instrument. Sends + data to self.data_pipe as result of self.data_handler). + """ + + try: + while True: + if self.ctrl_pipe.poll(): + input = self.ctrl_pipe.recv() + logger.debug("serial_handler: %s", input) + if input == ('a' or "DISCONNECT"): + self.serial.write('a') + 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.info("serial_handler: ABORT pressed!") + return False + + if line.startswith('B'): + self.data.append(self.data_handler( + self.serial.read(size=self.databytes))) + + elif line.lstrip().startswith("#"): + dstat_logger.info(line.lstrip().rstrip()) + + elif line.lstrip().startswith("@DONE"): + dstat_logger.debug(line.lstrip().rstrip()) + self.serial.flushInput() + return True + + except serial.SerialException: + return False + + def data_handler(self, data): + """Takes data_input as tuple -- (scan, data). + Returns: + current + """ + + seconds, milliseconds, current = struct.unpack(' 32767): + sum = 32767 + elif (sum < -32768): + sum = -32768 + + self.data_pipe.send(sum) \ No newline at end of file diff --git a/dstat_interface/experiments/chronoamp.py b/dstat_interface/experiments/chronoamp.py new file mode 100644 index 0000000000000000000000000000000000000000..f2fefa1581a37cd5e3a8e6e249eb3a3934db6080 --- /dev/null +++ b/dstat_interface/experiments/chronoamp.py @@ -0,0 +1,148 @@ +import time +import struct +import numpy as np +import serial + +from experiments.experiment_template import PlotBox, Experiment, exp_logger + +class ChronoampBox(PlotBox): + def format_plots(self): + """ + Creates and formats subplots needed. Overrides superclass. + """ + self.subplots = {'current_time' : self.figure.add_subplot(111)} + + for key, subplot in self.subplots.items(): + subplot.ticklabel_format(style='sci', scilimits=(0, 3), + useOffset=False, axis='y') + subplot.plot([],[]) + +class Chronoamp(Experiment): + id = 'cae' + """Chronoamperometry experiment""" + def setup(self): + self.plots.append(ChronoampBox('current_time')) + + self.datatype = "linearData" + self.datalength = 2 + self.databytes = 8 + self.data = {'current_time' : [([],[])]} + self.columns = ['Time (s)', 'Current (A)'] + self.plot_format = { + 'current_time' : { + 'labels' : self.columns, + 'xlims' : (0, sum(self.parameters['time'])) + } + } + + self.commands.append( + ("ER"+ str(len(self.parameters['potential'])) + " 0 ", []) + ) + + for i in self.parameters['potential']: + self.commands[-1][1].append(str(int(i*(65536./3000)+32768))) + for i in self.parameters['time']: + self.commands[-1][1].append(str(i)) + + 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, current = struct.unpack(' target: + return i + + 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:] + x1 = x[i:] + avg = mean(y1) + min_index, max_index = findBounds(y1) + y1[min_index] = avg + y1[max_index] = avg + 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.data['ft'] = [(f, Y)] + + def changetype(self, Experiment): + """Change plot type. Set axis labels and x bounds to those stored + 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() \ No newline at end of file diff --git a/dstat_interface/experiments/cv.py b/dstat_interface/experiments/cv.py new file mode 100644 index 0000000000000000000000000000000000000000..fbeda5653354d01592b20cf5ca4bba310a81d527 --- /dev/null +++ b/dstat_interface/experiments/cv.py @@ -0,0 +1,42 @@ +import time +import struct + +from experiments.experiment_template import PlotBox, Experiment + +class CVExp(Experiment): + id = 'cve' + """Cyclic Voltammetry experiment""" + def setup(self): + super(CVExp, self).setup() + self.datatype = "CVData" + self.xlabel = "Voltage (mV)" + self.ylabel = "Current (A)" + self.datalength = 2 * self.parameters['scans'] # x and y for each scan + self.databytes = 6 # uint16 + int32 + self.plot_format['current_voltage']['xlims'] = ( + int(self.parameters['v1']), + int(self.parameters['v2']) + ) + + self.commands += "E" + self.commands[2] += "C" + self.commands[2] += str(self.parameters['clean_s']) + self.commands[2] += " " + self.commands[2] += str(self.parameters['dep_s']) + self.commands[2] += " " + self.commands[2] += str(int(int(self.parameters['clean_mV'])* + (65536./3000)+32768)) + self.commands[2] += " " + self.commands[2] += str(int(int(self.parameters['dep_mV'])* + (65536./3000)+32768)) + self.commands[2] += " " + self.commands[2] += str(self.parameters['v1']) + self.commands[2] += " " + self.commands[2] += str(self.parameters['v2']) + self.commands[2] += " " + self.commands[2] += str(self.parameters['start']) + self.commands[2] += " " + self.commands[2] += str(self.parameters['scans']) + self.commands[2] += " " + self.commands[2] += str(self.parameters['slope']) + self.commands[2] += " " \ No newline at end of file diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py new file mode 100755 index 0000000000000000000000000000000000000000..29aa8393aa81d89eb7bb2dc7a6763881bb45f342 --- /dev/null +++ b/dstat_interface/experiments/experiment_template.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# DStat Interface - An interface for the open hardware DStat potentiostat +# Copyright (C) 2017 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 . +import logging +import struct +from datetime import datetime +from collections import OrderedDict +from copy import deepcopy +from math import ceil +import time + +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 matplotlib.figure import Figure +import matplotlib.gridspec as gridspec +import matplotlib.pyplot as plt + +from matplotlib.backends.backend_gtk3agg \ + import FigureCanvasGTK3Agg as FigureCanvas + +from pandas import DataFrame + +try: + import seaborn as sns + sns.set(context='paper', style='darkgrid') +except ImportError: + pass +import serial + +logger = logging.getLogger("dstat.comm") +dstat_logger = logging.getLogger("dstat.comm.DSTAT") +exp_logger = logging.getLogger("dstat.comm.Experiment") + +from errors import InputError, VarError +import state + +class Experiment(object): + """Store and acquire a potentiostat experiment. Meant to be subclassed + to by different experiment types and not used instanced directly. Subclass + must instantiate self.plotbox as the PlotBox class to use and define id as + a class attribute. + """ + id = None + + def __init__(self, parameters): + """Adds commands for gain and ADC.""" + self.parameters = parameters + self.databytes = 8 + self.datapoint = 0 + self.scan = 0 + self.time = 0 + self.plots = [] + + major, minor = state.dstat_version + + if major >= 1: + if minor == 1: + self.__gaintable = [1e2, 3e2, 3e3, 3e4, 3e5, 3e6, 3e7, 5e8] + elif minor >= 2: + self.__gaintable = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] + self.__gain_trim_table = ['r100_trim', 'r100_trim', 'r3k_trim', + 'r30k_trim', 'r300k_trim', 'r3M_trim', + 'r30M_trim', 'r100M_trim'] + else: + raise VarError(parameters['version'], "Invalid version parameter.") + + self.gain = self.__gaintable[int(self.parameters['gain'])] + self.gain_trim = int( + state.settings[ + self.__gain_trim_table[int(self.parameters['gain'])] + ][1] + ) + + self.commands = ["EA", "EG"] + + if self.parameters['buffer_true']: + self.commands[0] += "2" + else: + self.commands[0] += "0" + self.commands[0] += " {p[adc_rate]} {p[adc_pga]} ".format( + p=self.parameters) + self.commands[1] += "{p[gain]} {p[short_true]:d} ".format( + p=self.parameters) + + self.setup() + self.time = [datetime.utcnow()] + + def setup(self): + self.data = OrderedDict(current_voltage=[([],[])]) + + self.columns = ['Voltage (mV)', 'Current (A)'] + self.plot_format = { + 'current_voltage' : {'labels' : self.columns, + 'xlims' : (0, 1) + } + } + # list of scans, tuple of dimensions, list of data + self.line_data = ([], []) + + self.plots.append(PlotBox(['current_voltage'])) + + def write_command(self, cmd, params=None, retry=10): + """Write command to serial with optional number of retries.""" + def get_reply(): + reply = self.serial.readline().rstrip() + if reply.startswith('#'): + dstat_logger.info(reply) + return get_reply() + return reply + + n = len(cmd) + if params is not None: + n_params = len(params) + + for _ in range(retry): + time.sleep(0.2) + self.serial.reset_input_buffer() + self.serial.write('!{}\n'.format(n)) + time.sleep(.1) + + reply = get_reply() + + if reply != "@ACK {}".format(n): + logger.warning("Invalid response: {}".format(reply)) + continue + + self.serial.write('{}\n'.format(cmd)) + + reply = get_reply() + + if reply != "@RCV {}".format(n): + logger.warning("Invalid response: {}".format(reply)) + continue + + if params is None: + return True + + reply = get_reply() + + if reply != "@RQP {}".format(n_params): + logger.warning("Invalid response: {}".format(reply)) + continue + + self.serial.write(" ".join(params) + " ") + + reply = get_reply() + + if reply != "@RCP {}".format(n_params): + logger.warning("Invalid response: {}".format(reply)) + continue + + return True + + return False + + def run(self, ser, ctrl_pipe, data_pipe): + """Execute experiment. Connects and sends handshake signal to DStat + then sends self.commands. Don't call directly as a process in Windows, + use run_wrapper instead. + """ + self.serial = ser + self.ctrl_pipe = ctrl_pipe + self.data_pipe = data_pipe + + exp_logger.info("Experiment running") + + try: + for i in self.commands: + status = "DONE" + if isinstance(i, (str, unicode)): + logger.info("Command: %s", i) + + if not self.write_command(i): + status = "ABORT" + break + + else: + cmd = i[0] + data = i[1] + + logger.info("Command: {}".format(cmd)) + + if not self.write_command(cmd, params=data): + status = "ABORT" + break + + if not self.serial_handler(): + status = "ABORT" + break + + time.sleep(0.5) + + except serial.SerialException: + status = "SERIAL_ERROR" + finally: + while self.ctrl_pipe.poll(): + self.ctrl_pipe.recv() + return status + + def serial_handler(self): + """Handles incoming serial transmissions from DStat. Returns False + if stop button pressed and sends abort signal to instrument. Sends + data to self.data_pipe as result of self.data_handler). + """ + scan = 0 + try: + while True: + if self.ctrl_pipe.poll(): + input = self.ctrl_pipe.recv() + logger.debug("serial_handler: %s", input) + if input == "DISCONNECT": + self.serial.write('a') + self.serial.reset_input_buffer() + logger.info("serial_handler: ABORT pressed!") + time.sleep(.3) + return False + elif input == 'a': + self.serial.write('a') + + for line in self.serial: + if self.ctrl_pipe.poll(): + if self.ctrl_pipe.recv() == 'a': + self.serial.write('a') + + if line.startswith('B'): + data = self.data_handler( + (scan, self.serial.read(size=self.databytes))) + data = self.data_postprocessing(data) + if data is not None: + self.data_pipe.send(data) + try: + self.datapoint += 1 + except AttributeError: #Datapoint counting is optional + pass + + elif line.lstrip().startswith('S'): + scan += 1 + + elif line.lstrip().startswith("#"): + dstat_logger.info(line.lstrip().rstrip()) + + elif line.lstrip().startswith("@DONE"): + dstat_logger.debug(line.lstrip().rstrip()) + time.sleep(.3) + return True + + except serial.SerialException: + return False + + + def data_handler(self, data_input): + """Takes data_input as tuple -- (scan, data). + Returns: + (scan number, (voltage, current)) -- voltage in mV, current in A + """ + scan, data = data_input + voltage, current = struct.unpack(' 1: + gs = gridspec.GridSpec(int(ceil(len(self.plotnames)/2.)),2) + else: + gs = gridspec.GridSpec(1,1) + for n, i in enumerate(self.plotnames): + self.subplots[i] = self.figure.add_subplot(gs[n]) + + for subplot in self.subplots.values(): + subplot.ticklabel_format(style='sci', scilimits=(0, 3), + useOffset=False, axis='y') + subplot.plot([],[]) + + def clearall(self): + """Remove all lines on plot. """ + for name, plot in self.subplots.items(): + for line in reversed(plot.lines): + line.remove() + self.addline() + + def clearline(self, subplot, line_number): + """Remove a single line. + + Arguments: + subplot -- key in self.subplots + line_number -- line number in subplot + """ + self.subplots[subplot].lines[line_number].remove() + + def addline(self): + """Add a new line to plot. (initialized with dummy data)))""" + for subplot in self.subplots.values(): + subplot.plot([], []) + + def updateline(self, Experiment, line_number): + """Update a line specified with new data. + + Arguments: + Experiment -- Experiment instance + line_number -- line number to update + """ + for subplot in Experiment.data: + self.subplots[subplot].lines[line_number].set_xdata( + Experiment.data[subplot][line_number][0]) + self.subplots[subplot].lines[line_number].set_ydata( + Experiment.data[subplot][line_number][1]) + + def changetype(self, Experiment): + """Change plot type. Set axis labels and x bounds to those stored + in the Experiment instance. Stores class instance in Experiment. + """ + + for name, subplot in self.subplots.items(): + subplot.set_xlabel(Experiment.plot_format[name]['labels'][0]) + subplot.set_ylabel(Experiment.plot_format[name]['labels'][1]) + subplot.set_xlim(Experiment.plot_format[name]['xlims']) + for name, subplot in Experiment.plot_format.items(): + self.subplots[name].set_xlabel(subplot['labels'][0]) + self.subplots[name].set_ylabel(subplot['labels'][1]) + self.subplots[name].set_xlim(subplot['xlims']) + + + self.figure.canvas.draw() + + def redraw(self): + """Autoscale and refresh the plot.""" + for name, plot in self.subplots.items(): + plot.relim() + plot.autoscale(True, axis = 'y') + self.figure.canvas.draw() + + return True \ No newline at end of file diff --git a/dstat_interface/experiments/idle.py b/dstat_interface/experiments/idle.py new file mode 100644 index 0000000000000000000000000000000000000000..23e871b9a09a8aad4cdee432c7de0b90d8a198fb --- /dev/null +++ b/dstat_interface/experiments/idle.py @@ -0,0 +1,36 @@ +import time +import struct + +from experiments.experiment_template import Experiment + +class OCPExp(Experiment): + """Open circuit potential measumement in statusbar.""" + id = 'ocp' + def __init__(self): + self.databytes = 8 + + self.commands = ["EA", "EP0 0 "] + + self.commands[0] += "2 " # input buffer + self.commands[0] += "3 " # 2.5 Hz sample rate + self.commands[0] += "1 " # 2x PGA + + + 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(' + - - + False True True - automatic - automatic + True + True True False none - + True False + vertical - + True False 0 out - + True False 5 5 5 - + True False - 3 - 3 - - - True - False - Potential (mV) - - - 1 - 2 - GTK_FILL - - + True - + True False - Time (s) + True + Start (mV) - 2 - 3 - GTK_FILL + 0 + 0 - + True False + True + Stop (mV) - GTK_FILL + 0 + 1 - + True False - Cleaning + True + Slope (mV/s) - 1 - 2 - GTK_FILL + 0 + 2 - + True - False - Deposition + True + + 8 + 0 + 1 + False + False - 2 - 3 - GTK_FILL + 1 + 0 - + True True 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL - + True True 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL - + + True + False + True + Amplitude (mV) + + + 0 + 3 + + + + + True + False + True + Frequency (Hz) + + + 0 + 4 + + + + True True 8 0 1 - True False False - True - True - 2 - 3 - 2 - 3 - - GTK_FILL + 1 + 3 - + True True 8 0 1 - True False False - True - True - 2 - 3 - 1 - 2 - - GTK_FILL + 1 + 4 @@ -192,10 +183,10 @@ - + True False - Preconditioning + Experiment True @@ -208,211 +199,215 @@ - + True False 0 out - + True False 5 5 5 - + True False - 5 - 2 - 10 - True + True - + True False - Start (mV) + Potential (mV) - GTK_FILL + 1 + 0 - + True False - Stop (mV) + Time (s) + + + 2 + 0 + + + + + True + False + True + Cleaning + 0 1 - 2 - GTK_FILL - + True False - Slope (mV/s) + True + Deposition + 0 2 - 3 - GTK_FILL - + True True 8 0 1 - True False False - True - True 1 - 2 - - GTK_FILL - 3 + 1 - + True True 8 0 1 - True False False - True - True 1 - 2 - 1 - 2 - - GTK_FILL - 3 + 2 - + True True 8 0 1 - True False False - True - True - 1 - 2 + 2 2 - 3 - - GTK_FILL - 3 - - - - - True - False - Amplitude (mV) - - - 3 - 4 - GTK_FILL - - True - False - Frequency (Hz) - - - 4 - 5 - GTK_FILL - - - - + True True 8 0 1 - True False False - True - True - 1 - 2 - 3 - 4 - - GTK_FILL - 3 + 2 + 1 - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False - 1 - 2 - 4 - 5 - - GTK_FILL - 3 + 9 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + True False - Experiment + Preconditioning True @@ -428,16 +423,15 @@ True False - 20 - 20 Experiment not yet implemented - False - False + True + True + 6 2 diff --git a/dstat_interface/interface/adc_pot.glade b/dstat_interface/interface/adc_pot.glade index 6cb4371f8439ff8abab2a961c56f74e3a5ada646..fcb0d6eeebee057e80d4060cbb377ef90d59fefb 100644 --- a/dstat_interface/interface/adc_pot.glade +++ b/dstat_interface/interface/adc_pot.glade @@ -1,7 +1,7 @@ + - - + @@ -196,13 +196,15 @@ False - + True False + vertical - + True False + vertical True @@ -217,12 +219,11 @@ 5 5 - + True False - 4 - 2 - 2 + True + True True @@ -230,6 +231,10 @@ Gain of the ADC's programmable gain amplifier. Default is 2 - gives full scale input. PGA Setting + + 0 + 0 + @@ -238,8 +243,8 @@ Sample Rate + 0 1 - 2 @@ -252,10 +257,7 @@ 1 - 2 2 - 3 - GTK_EXPAND @@ -266,8 +268,8 @@ Input Buffer + 0 2 - 3 @@ -278,7 +280,7 @@ 1 - 2 + 0 @@ -289,9 +291,7 @@ 1 - 2 1 - 2 @@ -302,8 +302,8 @@ 2 Electrode Mode + 0 3 - 4 @@ -315,10 +315,7 @@ 1 - 2 3 - 4 - GTK_EXPAND @@ -336,8 +333,7 @@ False - False - 2 + True 0 @@ -355,9 +351,10 @@ 5 5 - + True False + True True @@ -366,9 +363,8 @@ Gain - True - True - 0 + 0 + 0 @@ -378,9 +374,8 @@ gain_liststore - True - True - 1 + 1 + 0 @@ -405,8 +400,9 @@ - True - True + False + False + 2 0 diff --git a/dstat_interface/interface/adc_pot.py b/dstat_interface/interface/adc_pot.py index b223cd2e3a1ad02955611e4d25f40dad49220fb8..8e731629f4091076d045d58c7031346b24407ae7 100755 --- a/dstat_interface/interface/adc_pot.py +++ b/dstat_interface/interface/adc_pot.py @@ -18,36 +18,41 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import gtk +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk +except ImportError: + print "ERR: GTK not available" + sys.exit(1) -from errors import InputError, VarError, ErrorLogger -_logger = ErrorLogger(sender="dstat_adc_pot") +from errors import InputError, VarError -v1_1_gain = [(0, "100 Ω (15 mA FS)", 0), - (1, "300 Ω (5 mA FS)", 1), - (2, "3 kΩ (500 µA FS)", 2), - (3, "30 kΩ (50 µA FS)", 3), - (4, "300 kΩ (5 µA FS)", 4), - (5, "3 MΩ (500 nA FS)", 5), - (6, "30 MΩ (50 nA FS)", 6), - (7, "500 MΩ (3 nA FS)", 7)] +v1_1_gain = [(0, "100 Ω (15 mA FS)", "0"), + (1, "300 Ω (5 mA FS)", "1"), + (2, "3 kΩ (500 µA FS)", "2"), + (3, "30 kΩ (50 µA FS)", "3"), + (4, "300 kΩ (5 µA FS)", "4"), + (5, "3 MΩ (500 nA FS)", "5"), + (6, "30 MΩ (50 nA FS)", "6"), + (7, "500 MΩ (3 nA FS)", "7")] -v1_2_gain = [(0, "Bypass", 0), - (1, "100 Ω (15 mA FS)", 1), - (2, "3 kΩ (500 µA FS)", 2), - (3, "30 kΩ (50 µA FS)", 3), - (4, "300 kΩ (5 µA FS)", 4), - (5, "3 MΩ (500 nA FS)", 5), - (6, "30 MΩ (50 nA FS)", 6), - (7, "100 MΩ (15 nA FS)", 7)] +v1_2_gain = [(0, "Bypass", "0"), + (1, "100 Ω (15 mA FS)", "1"), + (2, "3 kΩ (500 µA FS)", "2"), + (3, "30 kΩ (50 µA FS)", "3"), + (4, "300 kΩ (5 µA FS)", "4"), + (5, "3 MΩ (500 nA FS)", "5"), + (6, "30 MΩ (50 nA FS)", "6"), + (7, "100 MΩ (15 nA FS)", "7")] class adc_pot(object): def __init__(self): - self.builder = gtk.Builder() + self.builder = Gtk.Builder() self.builder.add_from_file('interface/adc_pot.glade') self.builder.connect_signals(self) - self.cell = gtk.CellRendererText() + self.cell = Gtk.CellRendererText() ui_keys = ['buffer_true', 'short_true', @@ -121,10 +126,8 @@ class adc_pot(object): self._params = dict.fromkeys(self.ui.keys()) for i in self._params: - try: + if i in params: self._params[i] = params[i] - except KeyError as e: - _logger.error("Invalid parameter key: %s" % e, "WAR") self._set_params() def _set_params(self): @@ -138,7 +141,7 @@ class adc_pot(object): if version[0] == 1: if version[1] == 1: for i in v1_1_gain: - self.gain_liststore.append(i) + self.gain_liststore.append(str(i)) elif version[1] >= 2: for i in v1_2_gain: self.gain_liststore.append(i) diff --git a/dstat_interface/interface/analysis_options.glade b/dstat_interface/interface/analysis_options.glade index be8262f428826a8bf2485c3676c8a3e295820bf0..dda2e6cd80f0a14ca581056a7ec6d52dccd9d005 100644 --- a/dstat_interface/interface/analysis_options.glade +++ b/dstat_interface/interface/analysis_options.glade @@ -1,31 +1,20 @@ + - - - - -3000 - 9999 - 0.5 - 10 - - - -3000 - 9999 - 0.5 - 10 - + False 5 Analysis Options… dialog - + True False + vertical 2 - + True False end @@ -54,27 +43,18 @@ - + True False - 4 - 2 - - - - - - True False True + 0.00 False False - True - True stats_start_adj 0.050000000000000003 2 @@ -82,9 +62,7 @@ 1 - 2 1 - 2 @@ -93,10 +71,9 @@ False True + 0.00 False False - True - True stats_stop_adj 0.050000000000000003 2 @@ -105,9 +82,7 @@ 1 - 2 2 - 3 @@ -116,11 +91,14 @@ True True False + center bottom True - 2 + 0 + 0 + 2 @@ -130,11 +108,12 @@ False True False + True True + 0 1 - 2 @@ -144,16 +123,17 @@ False True False + True True + 0 2 - 3 - True + False True 1 @@ -164,4 +144,16 @@ ok_button + + -3000 + 9999 + 0.5 + 10 + + + -3000 + 9999 + 0.5 + 10 + diff --git a/dstat_interface/interface/calib.glade b/dstat_interface/interface/calib.glade index 2c72437a2aecb5ed935bd60f6206f56932605cae..ff4b40f616e53cc2b649d835b99767541f7daa54 100644 --- a/dstat_interface/interface/calib.glade +++ b/dstat_interface/interface/calib.glade @@ -1,40 +1,28 @@ + - - + False True True - automatic - automatic True False none - + True False + vertical - + True False - 13 - 2 - True - - - - - - - - - + True True @@ -44,18 +32,12 @@ 8 0 1 - True False False - True - True 1 - 2 - - GTK_FILL - 3 + 0 @@ -65,19 +47,8 @@ Measurement Time (s) - - GTK_FILL - - - - - True - False - - - 2 - 1 - 2 + 0 + 0 @@ -87,10 +58,8 @@ Resistor + 0 2 - 3 - - 10 @@ -101,9 +70,7 @@ 1 - 2 2 - 3 @@ -113,9 +80,8 @@ 100 Ω + 0 3 - 4 - @@ -125,9 +91,8 @@ 3 kΩ + 0 4 - 5 - GTK_EXPAND @@ -137,9 +102,8 @@ 30 kΩ + 0 5 - 6 - GTK_EXPAND @@ -149,9 +113,8 @@ 300 kΩ + 0 6 - 7 - GTK_EXPAND @@ -161,9 +124,8 @@ 3 MΩ + 0 7 - 8 - GTK_EXPAND @@ -173,9 +135,8 @@ 30 MΩ + 0 8 - 9 - GTK_EXPAND @@ -185,9 +146,8 @@ 100 MΩ + 0 9 - 10 - GTK_EXPAND @@ -199,20 +159,12 @@ 8 0 1 - True False False - True - True 1 - 2 5 - 6 - - GTK_FILL - 3 @@ -224,20 +176,12 @@ 8 0 1 - True False False - True - True 1 - 2 4 - 5 - - GTK_FILL - 3 @@ -249,20 +193,12 @@ 8 0 1 - True False False - True - True 1 - 2 3 - 4 - - GTK_FILL - 3 @@ -274,20 +210,12 @@ 8 0 1 - True False False - True - True 1 - 2 6 - 7 - - GTK_FILL - 3 @@ -299,20 +227,12 @@ 8 0 1 - True False False - True - True 1 - 2 7 - 8 - - GTK_FILL - 3 @@ -324,20 +244,12 @@ 8 0 1 - True False False - True - True 1 - 2 8 - 9 - - GTK_FILL - 3 @@ -349,20 +261,12 @@ 8 0 1 - True False False - True - True 1 - 2 9 - 10 - - GTK_FILL - 3 @@ -374,12 +278,8 @@ - 1 - 2 - 10 - 11 - GTK_EXPAND | GTK_SHRINK - GTK_EXPAND | GTK_SHRINK | GTK_FILL + 0 + 11 @@ -391,12 +291,8 @@ - 1 - 2 - 11 - 12 - GTK_EXPAND | GTK_SHRINK - GTK_EXPAND | GTK_SHRINK | GTK_FILL + 0 + 12 @@ -408,18 +304,34 @@ - 1 - 2 - 12 - 13 - GTK_EXPAND | GTK_SHRINK - GTK_EXPAND | GTK_SHRINK | GTK_FILL + 0 + 10 + + + + + True + False + + + 0 + 1 + 2 + + + + + + + + + - False - False + True + True 0 @@ -427,8 +339,8 @@ True False - 5 - 20 + True + True Measure with WE open. Offsets cannot exceed 16 bit unsigned int. diff --git a/dstat_interface/interface/chronoamp.glade b/dstat_interface/interface/chronoamp.glade index b3ceb0688d1d58a0a4264468ebba78bff90dbefd..e0a22de627b23d20c986f5d0664ba7ebe6e66e8a 100644 --- a/dstat_interface/interface/chronoamp.glade +++ b/dstat_interface/interface/chronoamp.glade @@ -1,7 +1,7 @@ + - - + @@ -18,29 +18,34 @@ True True - automatic - automatic + True + True True False none - + True False + vertical - + True False - 3 - 2 + True + True True False Potential (mV) + + 0 + 0 + @@ -51,18 +56,13 @@ 0 1 True - True False False False - True - True 1 - 2 - - GTK_SHRINK + 0 @@ -72,8 +72,8 @@ Time (s) + 0 1 - 2 @@ -85,20 +85,13 @@ 0 1 True - True False False False - True - True 1 - 2 1 - 2 - - GTK_SHRINK @@ -111,8 +104,8 @@ + 0 2 - 3 @@ -126,29 +119,26 @@ 1 - 2 2 - 3 False - True - 5 + False 0 - + True False + vertical True True ca_list - False True True False @@ -157,6 +147,9 @@ False True both + + + True @@ -169,7 +162,6 @@ True False 2 - False False diff --git a/dstat_interface/interface/cv.glade b/dstat_interface/interface/cv.glade index ab547acf2c4d4b62f0b55598d79a20f356a89e91..9cc5d7f996b439348935afe4507371642ce30570 100644 --- a/dstat_interface/interface/cv.glade +++ b/dstat_interface/interface/cv.glade @@ -1,24 +1,25 @@ + - - + False True True - automatic - automatic + True + True True False none - + True False + vertical True @@ -33,21 +34,21 @@ 5 5 - + True False - 3 - 3 + True + True True False + False Potential (mV) 1 - 2 - GTK_FILL + 0 @@ -58,8 +59,7 @@ 2 - 3 - GTK_FILL + 0 @@ -68,7 +68,8 @@ False - GTK_FILL + 0 + 0 @@ -78,9 +79,8 @@ Cleaning + 0 1 - 2 - GTK_FILL @@ -90,9 +90,8 @@ Deposition + 0 2 - 3 - GTK_FILL @@ -103,19 +102,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL @@ -126,19 +118,12 @@ 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL @@ -149,19 +134,12 @@ 8 0 1 - True False False - True - True 2 - 3 2 - 3 - - GTK_FILL @@ -172,19 +150,12 @@ 8 0 1 - True False False - True - True 2 - 3 1 - 2 - - GTK_FILL @@ -221,13 +192,11 @@ 5 5 - + True False - 5 - 2 - 10 - True + True + True True @@ -235,7 +204,8 @@ Start (mV) - GTK_FILL + 0 + 0 @@ -245,9 +215,8 @@ Vertex 1 (mV) + 0 1 - 2 - GTK_FILL @@ -257,9 +226,8 @@ Vertex 2 (mV) + 0 2 - 3 - GTK_FILL @@ -270,18 +238,12 @@ 8 0 1 - True False False - True - True 1 - 2 - - GTK_FILL - 3 + 0 @@ -292,20 +254,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL - 3 @@ -316,20 +270,12 @@ 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL - 3 @@ -339,9 +285,8 @@ Slope (mV/s) + 0 3 - 4 - GTK_FILL @@ -351,9 +296,8 @@ Scans + 0 4 - 5 - GTK_FILL @@ -364,20 +308,12 @@ 8 0 1 - True False False - True - True 1 - 2 3 - 4 - - GTK_FILL - 3 @@ -388,20 +324,12 @@ 8 0 1 - True False False - True - True 1 - 2 4 - 5 - - GTK_FILL - 3 @@ -420,7 +348,7 @@ False False - 2 + 1 1 diff --git a/dstat_interface/interface/data_view.py b/dstat_interface/interface/data_view.py new file mode 100644 index 0000000000000000000000000000000000000000..e4bd711f0d2af0f95f9bbda50a591419019644f2 --- /dev/null +++ b/dstat_interface/interface/data_view.py @@ -0,0 +1,81 @@ +from __future__ import division, absolute_import, print_function, unicode_literals + +import logging +logger = logging.getLogger("dstat.interface.data_view") + +from collections import OrderedDict + +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk +except ImportError: + print("ERR: GTK not available") + sys.exit(1) + +class DataPage(object): + def __init__(self, notebook, name="Data"): + """Make new notebook page and adds to notebook.""" + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.combobox = Gtk.ComboBoxText() + self.scroll = Gtk.ScrolledWindow(vexpand=True) + self.textview = Gtk.TextView(cursor_visible=False, monospace=True, + editable=False) + self.scroll.add(self.textview) + self.box.add(self.combobox) + self.box.add(self.scroll) + self.name = name + self.buffers = {} + + self.combobox.connect('changed', self.combobox_changed) + + notebook.append_page(self.box, Gtk.Label(label=name)) + + def add_exp(self, exp): + """Add all data from exp to page.""" + for name, df in exp.df.items(): + self.combobox.append(id=name, text=name) + self.buffers[name] = Gtk.TextBuffer() + self.buffers[name].set_text(df.to_string()) + self.box.show_all() + + self.combobox.set_active(0) + + def clear_exps(self): + self.combobox.remove_all() + self.buffers = {} + + def combobox_changed(self, object): + """Switch displayed data buffer.""" + try: + self.textview.set_buffer( + self.buffers[self.combobox.get_active_id()] + ) + except KeyError: + pass + +class InfoPage(object): + def __init__(self, notebook, name="Info"): + """Make new notebook page and adds to notebook.""" + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.buffer = Gtk.TextBuffer() + self.scroll = Gtk.ScrolledWindow(vexpand=True) + self.textview = Gtk.TextView(cursor_visible=False, monospace=True, + editable=False, buffer=self.buffer) + self.scroll.add(self.textview) + self.box.add(self.scroll) + + self.name = name + + notebook.append_page(self.box, Gtk.Label(label=name)) + self.box.show_all() + + def clear(self): + """Clear buffer""" + self.buffer.set_text('') + + def set_text(self, text): + self.buffer.set_text(text) + + def add_line(self, line): + self.buffer.insert_at_cursor('{}\n'.format(line)) \ No newline at end of file diff --git a/dstat_interface/interface/db.glade b/dstat_interface/interface/db.glade new file mode 100644 index 0000000000000000000000000000000000000000..e65d520de3bdfe9d25b127d5e4168264caa11fc0 --- /dev/null +++ b/dstat_interface/interface/db.glade @@ -0,0 +1,204 @@ + + + + + + False + Database + center-on-parent + True + + + True + False + vertical + + + Enable Database saving + True + True + False + True + True + + + + False + False + 0 + + + + + True + False + True + True + + + True + False + Measurement ID + + + 0 + 0 + + + + + True + False + Experiment ID + + + 0 + 1 + + + + + True + True + + False + False + + + 1 + 1 + + + + + True + True + False + + False + False + + + 1 + 0 + + + + + True + True + False + + False + False + + + 1 + 3 + + + + + True + False + Patient ID + + + 0 + 3 + + + + + Generate New + True + True + True + + + + 2 + 1 + + + + + True + False + DB Path + + + 0 + 4 + + + + + True + True + + False + False + + + 1 + 4 + + + + + Apply + True + True + True + + + + 2 + 4 + + + + + True + False + Measurement Name + + + 0 + 2 + + + + + True + True + + False + False + + + 1 + 2 + + + + + + + + + + + + + + False + True + 1 + + + + + + diff --git a/dstat_interface/interface/db.py b/dstat_interface/interface/db.py new file mode 100755 index 0000000000000000000000000000000000000000..cc31db72e427d88d504ab7100992404621cdb5b6 --- /dev/null +++ b/dstat_interface/interface/db.py @@ -0,0 +1,143 @@ +#!/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 . + +import os +import sys +import logging +from uuid import uuid4 + +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) + +logger = logging.getLogger('dstat.interface.db') + +class DB_Window(GObject.GObject): + __gsignals__ = { + 'db-reset' : (GObject.SIGNAL_RUN_LAST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,) + ) + } + def __init__(self): + GObject.GObject.__init__(self) + + self.builder = Gtk.Builder() + self.builder.add_from_file('interface/db.glade') + self.builder.connect_signals(self) + + self.window = self.builder.get_object('db_window') + # Make sure window isn't destroyed if checkbox hit. + self.window.connect('delete-event', self.on_delete_event) + self.db_control_table = self.builder.get_object('db_control_table') + + ui_keys = ['exp_id_entry', + 'measure_id_entry', + 'patient_id_entry', + 'measure_name_entry', + 'db_path_entry', + 'db_apply_button', + 'exp_id_autogen_button', + 'db_enable_checkbutton' + ] + self.persistent_keys = ['db_path_entry','db_enable_checkbutton'] + self.ui = {} + for i in ui_keys: + self.ui[i] = self.builder.get_object(i) + + self.metadata_map = [('experiment_uuid', 'exp_id_entry'), + ('patient_id', 'patient_id_entry'), + ('name', 'measure_name_entry')] + + self._params = {} + + @property + def persistent_params(self): + """Dict of parameters that should be saved.""" + self._get_params() + return {k:self._params[k] for k in self.persistent_keys} + + @property + def params(self): + """Dict of parameters.""" + self._get_params() + return self._params + + def _get_params(self): + """Updates self._params from UI.""" + for i in self.ui: + if i.endswith('checkbutton'): + self._params[i] = self.ui[i].get_active() + elif i.endswith('entry'): + text = self.ui[i].get_text() + if text == "": + text = None + self._params[i] = text + + @params.setter + def params(self, params): + if self._params is {}: + self._params = dict.fromkeys(self.ui.keys()) + + for i in self._params: + if i in params: + self._params[i] = params[i] + self._set_params() + + def _set_params(self): + """Updates UI with new parameters.""" + for i in self.ui: + if i.endswith('checkbutton'): + self.ui[i].set_active(self._params[i]) + elif i.endswith('entry'): + if self._params[i] is not None: + self.ui[i].set_text(self._params[i]) + else: + self.ui[i].set_text("") + + def update_from_metadata(self, metadata): + params = {k : metadata[j] for j, k in self.metadata_map + if j in metadata} + self.params = params + + def show(self): + self.window.show_all() + self.on_db_enable_checkbutton_toggled() + + def on_db_enable_checkbutton_toggled(self, *args): + if self.ui['db_enable_checkbutton'].get_active(): + self.db_control_table.show() + else: + self.db_control_table.hide() + + def on_exp_id_autogen_button_clicked(self, *args): + self.ui['exp_id_entry'].set_text(uuid4().hex) + + def on_db_apply_button_clicked(self, widget=None, *data): + self.emit('db-reset', self.params['db_path_entry']) + + def on_delete_event(self, widget=None, *data): + widget.hide() + return True + +GObject.type_register(DB_Window) \ No newline at end of file diff --git a/dstat_interface/interface/dpv.glade b/dstat_interface/interface/dpv.glade index e13b41d2659b6db07b81fd40db8ea725f40ee45f..9143f880abc27d6a76bcb281c3c1057220b02596 100644 --- a/dstat_interface/interface/dpv.glade +++ b/dstat_interface/interface/dpv.glade @@ -1,24 +1,25 @@ + - - + False True True - automatic - automatic + True + True True False none - + True False + vertical True @@ -33,54 +34,73 @@ 5 5 - + True False - 3 - 3 + True + True - + True - False - Potential (mV) + True + + 8 + 0 + 1 + False + False - 1 - 2 - GTK_FILL + 2 + 1 - + True - False - Time (s) + True + + 8 + 0 + 1 + False + False 2 - 3 - GTK_FILL + 2 - + True - False + True + + 8 + 0 + 1 + False + False - GTK_FILL + 1 + 2 - + True - False - Cleaning + True + + 8 + 0 + 1 + False + False + 1 1 - 2 - GTK_FILL @@ -90,101 +110,51 @@ Deposition + 0 2 - 3 - GTK_FILL - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False + Cleaning - 1 - 2 + 0 1 - 2 - - GTK_FILL - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False - 1 - 2 - 2 - 3 - - GTK_FILL + 0 + 0 - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False + Time (s) 2 - 3 - 2 - 3 - - GTK_FILL + 0 - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False + Potential (mV) - 2 - 3 - 1 - 2 - - GTK_FILL + 1 + 0 @@ -221,13 +191,11 @@ 5 5 - + True False - 6 - 2 - 10 - True + True + True True @@ -235,7 +203,8 @@ Start (mV) - GTK_FILL + 0 + 0 @@ -245,9 +214,8 @@ Stop (mV) + 0 1 - 2 - GTK_FILL @@ -257,9 +225,8 @@ Step Size (mV) + 0 2 - 3 - GTK_FILL @@ -270,18 +237,12 @@ 8 0 1 - True False False - True - True 1 - 2 - - GTK_FILL - 3 + 0 @@ -292,20 +253,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL - 3 @@ -316,20 +269,12 @@ 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL - 3 @@ -339,9 +284,8 @@ Pulse Height (mV) + 0 3 - 4 - GTK_FILL @@ -351,9 +295,8 @@ Pulse Width (ms) - 4 - 5 - GTK_FILL + 0 + 5 @@ -364,20 +307,12 @@ 8 0 1 - True False False - True - True 1 - 2 3 - 4 - - GTK_FILL - 3 @@ -388,20 +323,12 @@ 8 0 1 - True False False - True - True 1 - 2 - 4 - 5 - - GTK_FILL - 3 + 5 @@ -412,8 +339,8 @@ Pulse Period (ms) - 5 - 6 + 0 + 4 @@ -424,20 +351,12 @@ 8 0 1 - True False False - True - True 1 - 2 - 5 - 6 - - GTK_FILL - 3 + 4 diff --git a/dstat_interface/interface/dstatinterface.glade b/dstat_interface/interface/dstatinterface.glade index 8ae7a008ba3cbb275889c99a19e533c5bbed2aa8..09dfb6cbcf6ad8740b4256a982dec50d3328ecce 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -1,64 +1,7 @@ + - - - - - - - - - - - - - - 0 - cae - Chronoamperometry - - - 1 - lsv - Linear Sweep Voltammetry - - - 2 - cve - Cyclic Voltammetry - - - 3 - swv - Square Wave Voltammetry - - - 4 - dpv - Differential Pulse Voltammetry - - - 5 - acv - AC Voltammetry - - - 6 - pde - Photodiode - - - 7 - pot - Potentiometry - - - 8 - cal - Offset Calibration - - - + False 5 @@ -693,13 +636,15 @@ copy of the Program in return for a fee. Michael Dryden Thanks to Christian Fobel for help with Dropbot Plugin + image-missing - + True False + vertical 2 - + True False end @@ -721,23 +666,18 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False - 0.47999998927116394 + center gtk-missing-image - - True - False - gtk-save-as - True False - gtk-save + gtk-save-as True False - gtk-save + gtk-save-as True @@ -749,6 +689,11 @@ Thanks to Christian Fobel for help with Dropbot Plugin False gtk-preferences + + True + False + gtk-save-as + @@ -763,9 +708,10 @@ Thanks to Christian Fobel for help with Dropbot Plugin 800 - + True False + vertical True @@ -774,25 +720,24 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False - _File - True + File True False - Save current data... + Save Experiment Data… True False - image2 + image8 False - Save Plots… + Save Plot… True False image3 @@ -802,7 +747,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - Save Parameters… + Save Parameters… True False image4 @@ -812,7 +757,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - Load Parameters… + Load Parameters… True False image5 @@ -839,7 +784,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False Dropbot - True True @@ -880,7 +824,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin False - Analysis options… + Analysis Options… True False image6 @@ -896,8 +840,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False - _Help - True + About True @@ -924,31 +867,34 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True True 300 True + True - + True False + vertical - + True False + vertical - + True False - 2 + vertical - True - True + False + False 0 @@ -962,21 +908,19 @@ Thanks to Christian Fobel for help with Dropbot Plugin - True - True + False + False 10 1 - + True False - ExpComboListStore - - True + False True 2 @@ -984,14 +928,15 @@ Thanks to Christian Fobel for help with Dropbot Plugin False - True + False 0 - + True False + vertical @@ -1003,7 +948,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False @@ -1052,7 +997,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin False False - 5 2 @@ -1063,18 +1007,50 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True True True - + True False + vertical + + + True + True + bottom + + + + + + + + + + + + + + + + + + + + + True + True + -1 + + - + True False + start Autosave @@ -1093,6 +1069,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False + start select-folder False Select a Save Folder @@ -1108,6 +1085,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin True True True + start 32 filename @@ -1115,9 +1093,8 @@ Thanks to Christian Fobel for help with Dropbot Plugin gtk-file False False - True - True File name + File name False @@ -1128,94 +1105,10 @@ Thanks to Christian Fobel for help with Dropbot Plugin False - True + False 0 - - - True - True - bottom - - - True - False - - - - - - - - True - False - 0.40999999642372131 - Main - - - False - - - - - True - False - - - - - - - - - 1 - - - - - True - False - FT - - - 1 - False - - - - - True - False - - - - - - - - - 2 - - - - - True - False - Periodogram - - - 2 - False - - - - - True - True - 1 - - @@ -1229,68 +1122,16 @@ Thanks to Christian Fobel for help with Dropbot Plugin - - True - True - automatic - automatic - - - True - True - False - 10 - 10 - databuffer1 - - - - - 1 - + - - True - False - Raw Data - - - 1 - False - + - - True - True - automatic - automatic - - - True - True - False - 10 - 10 - databuffer2 - - - - - 2 - + - - True - False - Extra Data - - - 2 - False - + @@ -1306,42 +1147,42 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False - + True False - Serial Port: + 2 - False + True True - 5 + end 0 - + True False - serial_liststore + Serial Port: False - False + True + 5 1 - - gtk-refresh + + 128 True - True - True - True - + False + start + serial_liststore False @@ -1394,13 +1235,13 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + + gtk-refresh True - False - 0 - 5 - OCP: - True + True + True + True + False @@ -1409,21 +1250,24 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False - 2 + start + OCP: + True - True - True + False + False + 3 7 False - False + True 2 diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index 1cb9a345d2fc8befd21fb33f289f055c12556e77..397fe91bcc1c2dfb393d54cd3168604c2c31d79e 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -16,24 +16,44 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, absolute_import, print_function, unicode_literals import os import sys +import logging -import gtk -import gobject +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) import dstat_comm +import state +import experiments as exp +import experiments.cal as cal import __main__ -from errors import InputError, VarError, ErrorLogger -_logger = ErrorLogger(sender="dstat-interface-exp_int") +from errors import InputError, VarError +logger = logging.getLogger("dstat.interface.exp_int") -class ExpInterface(object): +class ExpInterface(GObject.Object): """Generic experiment interface class. Should be subclassed to implement - experiment interfaces by populating self.entry. + experiment interfaces by populating self.entry. Override class attributes + to set id and experiment class to run. """ + __gsignals__ = { + b'run_utility': (GObject.SIGNAL_RUN_FIRST, None, ()), + b'done_utility': (GObject.SIGNAL_RUN_FIRST, None, ()) + } + + id = None + experiment = None + def __init__(self, glade_path): - self.builder = gtk.Builder() + super(ExpInterface, self).__init__() + self.builder = Gtk.Builder() self.builder.add_from_file(glade_path) self.builder.connect_signals(self) self.entry = {} # to be used only for str parameters @@ -63,14 +83,21 @@ class ExpInterface(object): try: self._params[i] = params[i] except KeyError as e: - _logger.error("Invalid parameter key: %s" % e, "WAR") + logger.warning("Invalid parameter key: %s" % e) self._set_params() def _set_params(self): """Updates UI with new parameters.""" for i in self.entry: self.entry[i].set_text(self._params[i]) - + + def get_window(self): + return self.builder.get_object('scrolledwindow1') + def on_run_utility(self, data=None): + self.emit('run_utility') + def on_done_utility(self, data=None): + self.emit('done_utility') + class Chronoamp(ExpInterface): """Experiment class for chronoamperometry. Extends ExpInterface class to support treeview neeeded for CA. @@ -80,22 +107,27 @@ class Chronoamp(ExpInterface): on_remove_button_clicked(self, widget) get_params(self) """ + id = 'cae' + experiment = exp.Chronoamp def __init__(self): """Extends superclass method to support treeview.""" super(Chronoamp, self).__init__('interface/chronoamp.glade') + self.name = "Chronoamperometry" + self.statusbar = self.builder.get_object('statusbar') self.model = self.builder.get_object('ca_list') self.treeview = self.builder.get_object('treeview') - self.cell_renderer = gtk.CellRendererText() + self.treeview.set_fixed_height_mode(False) - self.treeview.insert_column_with_attributes(-1, "Time", - self.cell_renderer, text=1).set_expand(True) - self.treeview.insert_column_with_attributes(-1, "Potential", - self.cell_renderer, text=0).set_expand(True) + for i, column_title in enumerate(["Potential", "Time"]): + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(column_title, renderer, text=i) + column.set_expand(True) + self.treeview.append_column(column) self.selection = self.treeview.get_selection() - self.selection.set_mode(gtk.SELECTION_MULTIPLE) + self.selection.set_mode(Gtk.SelectionMode.MULTIPLE) def _fill_params(self): super(Chronoamp, self)._fill_params() @@ -131,7 +163,7 @@ class Chronoamp(ExpInterface): referencelist = [] for i in selected_rows: - referencelist.append(gtk.TreeRowReference(self.model, i)) + referencelist.append(Gtk.TreeRowReference(self.model, i)) for i in referencelist: self.model.remove(self.model.get_iter(i.get_path())) @@ -151,13 +183,16 @@ class Chronoamp(ExpInterface): for i in table: self.model.append(i) - + class LSV(ExpInterface): """Experiment class for LSV.""" + id = 'lsv' + experiment = exp.LSVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(LSV, self).__init__('interface/lsv.glade') - + self.name = "Linear Sweep Voltammetry" + self.entry['clean_mV'] = self.builder.get_object('clean_mV') self.entry['clean_s'] = self.builder.get_object('clean_s') self.entry['dep_mV'] = self.builder.get_object('dep_mV') @@ -168,10 +203,13 @@ class LSV(ExpInterface): class CV(ExpInterface): """Experiment class for CV.""" + id = 'cve' + experiment = exp.CVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(CV, self).__init__('interface/cv.glade') - + self.name = "Cyclic Voltammetry" + self.entry['clean_mV'] = self.builder.get_object('clean_mV') self.entry['clean_s'] = self.builder.get_object('clean_s') self.entry['dep_mV'] = self.builder.get_object('dep_mV') @@ -184,10 +222,13 @@ class CV(ExpInterface): class SWV(ExpInterface): """Experiment class for SWV.""" + id = 'swv' + experiment = exp.SWVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(SWV, self).__init__('interface/swv.glade') - + self.name = "Square Wave Voltammetry" + self.entry['clean_mV'] = self.builder.get_object('clean_mV') self.entry['clean_s'] = self.builder.get_object('clean_s') self.entry['dep_mV'] = self.builder.get_object('dep_mV') @@ -220,10 +261,14 @@ class SWV(ExpInterface): class DPV(ExpInterface): """Experiment class for DPV.""" + id = 'dpv' + experiment = exp.DPVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(DPV, self).__init__('interface/dpv.glade') - + + self.name = "Differential Pulse Voltammetry" + self.entry['clean_mV'] = self.builder.get_object('clean_mV') self.entry['clean_s'] = self.builder.get_object('clean_s') self.entry['dep_mV'] = self.builder.get_object('dep_mV') @@ -235,23 +280,29 @@ class DPV(ExpInterface): self.entry['period'] = self.builder.get_object('period_entry') self.entry['width'] = self.builder.get_object('width_entry') -class ACV(ExpInterface): - """Experiment class for ACV.""" - def __init__(self): - """Adds entry listings to superclass's self.entry dict""" - super(ACV, self).__init__('interface/acv.glade') - - self.entry['start'] = self.builder.get_object('start_entry') - self.entry['stop'] = self.builder.get_object('stop_entry') - self.entry['slope'] = self.builder.get_object('slope_entry') - self.entry['amplitude'] = self.builder.get_object('amplitude_entry') - self.entry['freq'] = self.builder.get_object('freq_entry') +# class ACV(ExpInterface): +# """Experiment class for ACV.""" +# id = 'acv' +# def __init__(self): +# """Adds entry listings to superclass's self.entry dict""" +# super(ACV, self).__init__('interface/acv.glade') +# self.name = "AC Voltammetry" +# +# self.entry['start'] = self.builder.get_object('start_entry') +# self.entry['stop'] = self.builder.get_object('stop_entry') +# self.entry['slope'] = self.builder.get_object('slope_entry') +# self.entry['amplitude'] = self.builder.get_object('amplitude_entry') +# self.entry['freq'] = self.builder.get_object('freq_entry') class PD(ExpInterface): """Experiment class for PD.""" + id = 'pde' + experiment = exp.PDExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(PD, self).__init__('interface/pd.glade') + self.name = "Photodiode/PMT" + self.entry['time'] = self.builder.get_object('time_entry') self.entry['sync_freq'] = self.builder.get_object('sync_freq') @@ -313,15 +364,15 @@ 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: - gobject.timeout_add(700, restore_buttons, self.buttons) + GObject.timeout_add(700, restore_buttons, self.buttons) def on_threshold_button_clicked(self, data=None): __main__.MAIN.on_pot_stop_clicked() @@ -331,16 +382,16 @@ 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: - gobject.timeout_add(700, restore_buttons, self.buttons) + GObject.timeout_add(700, restore_buttons, self.buttons) def on_shutter_button_toggled(self, widget): if self.bool['shutter_true'].get_active(): @@ -353,17 +404,23 @@ class PD(ExpInterface): class POT(ExpInterface): """Experiment class for Potentiometry.""" + id = 'pot' + experiment = exp.PotExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(POT, self).__init__('interface/potexp.glade') + self.name = "Potentiometry" self.entry['time'] = self.builder.get_object('time_entry') class CAL(ExpInterface): """Experiment class for Calibrating gain.""" + id = 'cal' + experiment = exp.CALExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" super(CAL, self).__init__('interface/calib.glade') + self.name = "Calilbration" self.entry['time'] = self.builder.get_object('time_entry') self.entry['R100'] = self.builder.get_object('100_entry') @@ -388,24 +445,24 @@ 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() finally: - gobject.timeout_add(700, restore_buttons, self.buttons) + GObject.timeout_add(700, restore_buttons, self.buttons) def on_write_button_clicked(self, data=None): for i in self.buttons: @@ -415,23 +472,23 @@ 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() finally: - gobject.timeout_add(700, restore_buttons, self.buttons) + GObject.timeout_add(700, restore_buttons, self.buttons) def on_measure_button_clicked(self, data=None): if (int(self.entry['time'].get_text()) <= 0 or int(self.entry['time'].get_text()) > 65535): - print "ERR: Time out of range" + logger.error("ERR: Time out of range") return for i in self.buttons: @@ -441,34 +498,34 @@ class CAL(ExpInterface): __main__.MAIN.stop_ocp() __main__.MAIN.spinner.start() - offset = dstat_comm.measure_offset(self.get_params()['time']) + offset = cal.measure_offset(self.params['time']) for i in offset: - _logger.error(" ".join((i, str(-offset[i]))), "INFO") - dstat_comm.settings[i][1] = str(-offset[i]) + logger.info("{} {}".format(i, 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: - gobject.timeout_add(700, restore_buttons, self.buttons) + GObject.timeout_add(700, restore_buttons, self.buttons) __main__.MAIN.spinner.stop() def restore_buttons(buttons): - """ Should be called with gobject callback """ + """ Should be called with GObject callback """ for i in buttons: i.set_sensitive(True) diff --git a/dstat_interface/interface/exp_window.py b/dstat_interface/interface/exp_window.py index 10fa69c5c9ba08ad73ea33022c597265bf3674cb..413d43a2e8d00a8004ffc7bf0ac432448c3dad68 100755 --- a/dstat_interface/interface/exp_window.py +++ b/dstat_interface/interface/exp_window.py @@ -17,40 +17,89 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import inspect +import logging +from collections import OrderedDict + +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) + import interface.exp_int as exp -class Experiments: +logger = logging.getLogger("dstat.interface.exp_window") + +class Experiments(GObject.Object): + __gsignals__ = { + 'run_utility': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'done_utility': (GObject.SIGNAL_RUN_FIRST, None, ()) + } def __init__(self, builder): + super(Experiments,self).__init__() self.builder = builder + self.builder.connect_signals(self) # (experiment index in UI, experiment) - self.classes = {} - self.classes['cae'] = (0, exp.Chronoamp()) - self.classes['lsv'] = (1, exp.LSV()) - self.classes['cve'] = (2, exp.CV()) - self.classes['swv'] = (3, exp.SWV()) - self.classes['dpv'] = (4, exp.DPV()) - self.classes['acv'] = (5, exp.ACV()) - self.classes['pde'] = (6, exp.PD()) - self.classes['pot'] = (7, exp.POT()) - self.classes['cal'] = (8, exp.CAL()) + + classes = {c.id : c() # Make class instances + for _, c in inspect.getmembers(exp, inspect.isclass) + if issubclass(c, exp.ExpInterface) + and c is not exp.ExpInterface + } - # Create reverse lookup - self.select_to_key = {} - for key, value in self.classes.iteritems(): - self.select_to_key[value[0]] = key - + self.classes = OrderedDict(sorted(classes.items())) #fill exp_section exp_section = self.builder.get_object('exp_section_box') self.containers = {} - for key, cls in self.classes.iteritems(): - self.containers[key] = cls[1].builder.get_object('scrolledwindow1') + for key, c in self.classes.items(): + c.connect('run_utility', self.on_run_utility) + c.connect('done_utility', self.on_done_utility) + self.containers[key] = c.get_window() for key in self.containers: - self.containers[key].reparent(exp_section) - self.containers[key].hide() + try: + self.containers[key].get_parent().remove(self.containers[key]) + except AttributeError: + pass + + exp_section.add(self.containers[key]) + self.expcombobox = self.builder.get_object('expcombobox') + self.expcombobox.connect('changed', self.on_expcombobox_changed) + + for c in self.classes.values(): + self.expcombobox.append(id=c.id, text=c.name) + + def on_run_utility(self, data=None): + self.emit('run_utility') + + def on_done_utility(self, data=None): + self.emit('done_utility') + + def on_expcombobox_changed(self, data=None): + """Change the experiment window when experiment box changed.""" + self.set_exp(self.expcombobox.get_active_id()) + + def setup_exp(self, parameters): + exp = self.classes[self.expcombobox.get_active_id()] + try: + exp.param_test(parameters) + except AttributeError: + logger.warning( + "Experiment {} has no defined parameter test.".format( + exp.name) + ) + return exp.experiment(parameters) + + def hide_exps(self): + for key in self.containers: + self.containers[key].hide() + def set_exp(self, selection): """Changes parameter tab to selected experiment. Returns True if successful, False if invalid selection received. @@ -58,16 +107,14 @@ class Experiments: Arguments: selection -- id string of experiment type """ - for key in self.containers: - self.containers[key].hide() - + self.hide_exps() self.containers[selection].show() self.selected_exp = selection return True def get_params(self, experiment): - return self.classes[experiment][1].params + return self.classes[experiment].params def set_params(self, experiment, parameters): - self.classes[experiment][1].params = parameters \ No newline at end of file + self.classes[experiment].params = parameters \ No newline at end of file diff --git a/dstat_interface/interface/lsv.glade b/dstat_interface/interface/lsv.glade index 63c4468a3060496c14a8b3e092be3285578222c6..404e1e4b31d4a7fe3c8b3103e9d99aeaf419c247 100644 --- a/dstat_interface/interface/lsv.glade +++ b/dstat_interface/interface/lsv.glade @@ -1,24 +1,25 @@ + - - + False True True - automatic - automatic + True + True True False none - + True False + vertical True @@ -33,11 +34,11 @@ 5 5 - + True False - 3 - 3 + True + True True @@ -46,8 +47,7 @@ 1 - 2 - GTK_FILL + 0 @@ -58,8 +58,7 @@ 2 - 3 - GTK_FILL + 0 @@ -68,7 +67,8 @@ False - GTK_FILL + 0 + 0 @@ -78,9 +78,8 @@ Cleaning + 0 1 - 2 - GTK_FILL @@ -90,9 +89,8 @@ Deposition + 0 2 - 3 - GTK_FILL @@ -103,19 +101,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL @@ -126,19 +117,12 @@ 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL @@ -149,19 +133,12 @@ 8 0 1 - True False False - True - True 2 - 3 2 - 3 - - GTK_FILL @@ -172,19 +149,12 @@ 8 0 1 - True False False - True - True 2 - 3 1 - 2 - - GTK_FILL @@ -221,13 +191,11 @@ 5 5 - + True False - 3 - 2 - 10 - True + True + True True @@ -235,7 +203,8 @@ Start (mV) - GTK_FILL + 0 + 0 @@ -245,9 +214,8 @@ Stop (mV) + 0 1 - 2 - GTK_FILL @@ -257,9 +225,8 @@ Slope (mV/s) + 0 2 - 3 - GTK_FILL @@ -270,18 +237,12 @@ 8 0 1 - True False False - True - True 1 - 2 - - GTK_FILL - 3 + 0 @@ -292,20 +253,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL - 3 @@ -316,20 +269,12 @@ 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL - 3 diff --git a/dstat_interface/interface/pd.glade b/dstat_interface/interface/pd.glade index bb72ff8153173d2da83cec1bfab0d494fb22372b..c39930e8bbb319dc29ab30eb348c1c1b96b6d516 100644 --- a/dstat_interface/interface/pd.glade +++ b/dstat_interface/interface/pd.glade @@ -1,7 +1,7 @@ + - - + 3000 1 @@ -13,24 +13,23 @@ True True - automatic - automatic + True + True True False none - + True False + vertical - + True False - 11 - 2 - True + True True @@ -39,20 +38,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL - 3 @@ -62,7 +53,8 @@ Bias Voltage (mV) - GTK_FILL + 0 + 0 @@ -72,9 +64,8 @@ Measurement Time (s) + 0 1 - 2 - GTK_FILL @@ -83,10 +74,9 @@ True 4 + 0 False False - True - True voltage_adjustment 1 True @@ -95,7 +85,7 @@ 1 - 2 + 0 @@ -103,10 +93,13 @@ True False Ambient Light Interlock + + + + 0 3 - 4 @@ -120,10 +113,7 @@ 1 - 2 3 - 4 - GTK_EXPAND @@ -132,13 +122,12 @@ True True True + center + 0 4 - 5 - - GTK_FILL @@ -149,20 +138,12 @@ 8 0 1 - True False False - True - True 1 - 2 4 - 5 - - GTK_FILL - 3 @@ -171,24 +152,12 @@ True True True + center + 0 5 - 6 - - GTK_FILL - - - - - True - False - - - 2 - 2 - 3 @@ -204,20 +173,7 @@ 1 - 2 5 - 6 - - - - - True - False - - - 2 - 6 - 7 @@ -225,10 +181,13 @@ True False Electromechanical Shutter + + + + 0 7 - 8 @@ -237,13 +196,12 @@ True True False - 0.54000002145767212 + center True + 0 8 - 9 - @@ -254,20 +212,12 @@ 8 0 1 - True False False - True - True 1 - 2 8 - 9 - - GTK_FILL - 3 @@ -282,10 +232,7 @@ 1 - 2 7 - 8 - GTK_EXPAND @@ -295,8 +242,8 @@ Start FFT after (s) + 0 9 - 10 @@ -310,16 +257,10 @@ 1 False False - True - True 1 - 2 9 - 10 - - GTK_FILL @@ -329,8 +270,8 @@ FFT Integral bandwidth (Hz) + 0 10 - 11 @@ -342,25 +283,44 @@ 8 1 1 - True False False - True - True 1 - 2 10 - 11 - - GTK_FILL + + + + + True + False + 5 + 5 + + + 0 + 2 + 2 + + + + + True + False + 5 + 5 + + + 0 + 6 + 2 - False - False + True + True 0 @@ -368,8 +328,6 @@ True False - 20 - 20 diff --git a/dstat_interface/interface/plot_ui.py b/dstat_interface/interface/plot_ui.py new file mode 100644 index 0000000000000000000000000000000000000000..b0ff570fb85c4875576e73cdcb6a663c3d7a7c77 --- /dev/null +++ b/dstat_interface/interface/plot_ui.py @@ -0,0 +1,33 @@ +import logging +logger = logging.getLogger("dstat.interface.plot_ui") + +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk +except ImportError: + print "ERR: GTK not available" + sys.exit(1) + +from matplotlib.backends.backend_gtk3 \ + import NavigationToolbar2GTK3 as NavigationToolbar + +def clear_notebook(notebook): + for pages in range(notebook.get_n_pages()): + notebook.remove_page(0) + +def add_exp_to_notebook(notebook, exp): + for plot in exp.plots: + label = Gtk.Label.new(plot.name) + notebook.append_page(plot.box, label) + plot.box.show_all() + +def replace_notebook_exp(notebook, exp, window): + clear_notebook(notebook) + add_exp_to_notebook(notebook, exp) + add_navigation_toolbars(exp, window) + +def add_navigation_toolbars(exp, window): + for plot in exp.plots: + toolbar = NavigationToolbar(plot.canvas, window) + plot.box.pack_start(toolbar, expand=False, fill=False, padding=0) \ No newline at end of file diff --git a/dstat_interface/interface/potexp.glade b/dstat_interface/interface/potexp.glade index 4d8d915234c08570bb877ed69bea97111c6f746d..d4ec1e6a3f7b06c58807ac3c42e55e620752f3b2 100644 --- a/dstat_interface/interface/potexp.glade +++ b/dstat_interface/interface/potexp.glade @@ -1,7 +1,7 @@ + - - + @@ -18,23 +18,24 @@ True True - automatic - automatic + True + True True False none - + True False + vertical - + True False - 2 - 2 + True + True True @@ -42,7 +43,8 @@ Time (s) - 2 + 0 + 0 @@ -54,26 +56,19 @@ 0 1 True - True False False False - True - True 1 - 2 - 2 - GTK_EXPAND - GTK_SHRINK + 0 False - True - 5 + False 0 diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 675bcd1c2f29ba738c63a20dc8f0a43eb0748858..76a714e49b41d0fb54347b224bf2aec2984aa573 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -18,10 +18,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import division, absolute_import, print_function, unicode_literals + import io import os -import gtk +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk +except ImportError: + print("ERR: GTK not available") + sys.exit(1) import numpy as np import logging @@ -31,12 +39,12 @@ from errors import InputError, VarError from params import save_params, load_params 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)") + fcd = Gtk.FileChooserDialog("Save…", None, Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) + + filters = [Gtk.FileFilter()] + filters[0].set_name("Tab-separated Text (.txt)") filters[0].add_pattern("*.txt") fcd.set_do_overwrite_confirmation(True) @@ -45,29 +53,29 @@ def manSave(current_exp): response = fcd.run() - if response == gtk.RESPONSE_OK: - path = fcd.get_filename() + if response == Gtk.ResponseType.OK: + path = fcd.get_filename().decode("utf-8") logger.info("Selected filepath: %s", path) - filter_selection = fcd.get_filter().get_name() + filter_selection = fcd.get_filter().get_name().decode("utf-8") if filter_selection.endswith("(.txt)"): save_text(current_exp, path) fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.ResponseType.CANCEL: fcd.destroy() def plot_save_dialog(plots): - fcd = gtk.FileChooserDialog("Save Plot…", None, - gtk.FILE_CHOOSER_ACTION_SAVE, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + fcd = Gtk.FileChooserDialog("Save Plot…", None, + Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) - filters = [gtk.FileFilter()] + filters = [Gtk.FileFilter()] filters[0].set_name("Portable Document Format (.pdf)") filters[0].add_pattern("*.pdf") - filters.append(gtk.FileFilter()) + filters.append(Gtk.FileFilter()) filters[1].set_name("Portable Network Graphics (.png)") filters[1].add_pattern("*.png") @@ -77,10 +85,10 @@ def plot_save_dialog(plots): response = fcd.run() - if response == gtk.RESPONSE_OK: - path = fcd.get_filename() - logger.info("Selected filepath: %s", path) - filter_selection = fcd.get_filter().get_name() + if response == Gtk.ResponseType.OK: + path = fcd.get_filename().decode("utf-8") + logger.info("Selected filepath: %r", path) + filter_selection = fcd.get_filter().get_name().decode("utf-8") if filter_selection.endswith("(.pdf)"): if not path.endswith(".pdf"): @@ -94,19 +102,18 @@ def plot_save_dialog(plots): fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.ResponseType.CANCEL: fcd.destroy() def man_param_save(window): - fcd = gtk.FileChooserDialog("Save Parameters…", + fcd = Gtk.FileChooserDialog("Save Parameters…", None, - gtk.FILE_CHOOSER_ACTION_SAVE, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_SAVE, gtk.RESPONSE_OK) - ) + Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) - filters = [gtk.FileFilter()] + filters = [Gtk.FileFilter()] filters[0].set_name("Parameter File (.yml)") filters[0].add_pattern("*.yml") @@ -116,8 +123,8 @@ def man_param_save(window): response = fcd.run() - if response == gtk.RESPONSE_OK: - path = fcd.get_filename() + if response == Gtk.ResponseType.OK: + path = fcd.get_filename().decode("utf-8") logger.info("Selected filepath: %s", path) if not path.endswith(".yml"): @@ -127,18 +134,17 @@ def man_param_save(window): fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.ResponseType.CANCEL: fcd.destroy() def man_param_load(window): - fcd = gtk.FileChooserDialog("Load Parameters…", + fcd = Gtk.FileChooserDialog("Load Parameters…", None, - gtk.FILE_CHOOSER_ACTION_OPEN, - (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, - gtk.STOCK_OPEN, gtk.RESPONSE_OK) - ) + Gtk.FileChooserAction.OPEN, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OPEN, Gtk.ResponseType.OK)) - filters = [gtk.FileFilter()] + filters = [Gtk.FileFilter()] filters[0].set_name("Parameter File (.yml)") filters[0].add_pattern("*.yml") @@ -147,15 +153,15 @@ def man_param_load(window): response = fcd.run() - if response == gtk.RESPONSE_OK: - path = fcd.get_filename() + if response == Gtk.ResponseType.OK: + path = fcd.get_filename().decode("utf-8") logger.info("Selected filepath: %s", path) load_params(window, path) fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.ResponseType.CANCEL: fcd.destroy() def autoSave(exp, path, name): @@ -180,62 +186,22 @@ def autoPlot(exp, path, name): save_plot(exp, path) def save_text(exp, path): - name, _sep, ext = path.rpartition('.') # ('','',string) if no match - if _sep == '': - name = ext - ext = 'txt' + savestrings = exp.get_save_strings() + path = path.rstrip('.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)): + for key, text in savestrings.items(): # Test for existing files of any kind + while os.path.exists("{}{}-{}.txt".format(path, num, key)): j += 1 num = j - - for dname in exp.data: # save data - file = open("%s%s-%s.%s" % (name, num, dname, ext), 'w') - - time = exp.time - header = "".join(['# TIME ', time.isoformat(), "\n"]) - - header += "# DSTAT COMMANDS\n# " - for i in exp.commands: - header += i - - file.write("".join([header, '\n'])) - - 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) - ) - - for i in analysis_buffer: - file.write("%s\n" % i) - - # Write out actual data - line_buffer = [] - - 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) - - file.close() + save_path = "{}{}".format(path, num) + + for key, text in savestrings.items(): + with open('{}-{}.txt'.format(save_path, key), 'w') as f: + f.write(text) def save_plot(exp, path): """Saves everything in exp.plots to path. Appends a number for duplicates. @@ -250,9 +216,11 @@ def save_plot(exp, path): j = 0 for i in exp.plots: # Test for any existing files - while os.path.exists("%s%s-%s.%s" % (name, num, i, ext)): + plot_type = '_'.join(i.name.lower().split()) + while os.path.exists("{}{}-{}.{}".format(name, num, plot_type, 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 + plot_type = '_'.join(i.name.lower().split()) + i.figure.savefig("{}{}-{}.{}".format(name, num, plot_type, ext)) \ No newline at end of file diff --git a/dstat_interface/interface/swv.glade b/dstat_interface/interface/swv.glade index 62659c118e5f0fb1dd1d1e55d335cec7ba6946c8..0a1e3268f8a7d07613918646ed1f330d5874868e 100644 --- a/dstat_interface/interface/swv.glade +++ b/dstat_interface/interface/swv.glade @@ -1,24 +1,25 @@ + - - + False True True - automatic - automatic + True + True True False none - + True False + vertical True @@ -33,11 +34,11 @@ 5 5 - + True False - 3 - 3 + True + True True @@ -46,8 +47,7 @@ 1 - 2 - GTK_FILL + 0 @@ -58,8 +58,7 @@ 2 - 3 - GTK_FILL + 0 @@ -68,7 +67,8 @@ False - GTK_FILL + 0 + 1 @@ -78,9 +78,8 @@ Cleaning + 0 1 - 2 - GTK_FILL @@ -90,9 +89,8 @@ Deposition + 0 2 - 3 - GTK_FILL @@ -103,19 +101,12 @@ 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL @@ -126,19 +117,12 @@ 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL @@ -149,19 +133,12 @@ 8 0 1 - True False False - True - True 2 - 3 2 - 3 - - GTK_FILL @@ -172,21 +149,17 @@ 8 0 1 - True False False - True - True 2 - 3 1 - 2 - - GTK_FILL + + + @@ -221,251 +194,198 @@ 5 5 - + True False - 7 - 2 - 10 - True + True + True - + True - False - Start (mV) + True + + 8 + 0 + 1 + False + False - GTK_FILL + 1 + 6 - + True - False - Stop (mV) + True + False + center + center + True + True - 1 - 2 - GTK_FILL + 1 + 5 - + True False - Step Size (mV) + Scans - 2 - 3 - GTK_FILL + 0 + 6 - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False + Scan both forwards and backwards. + Cyclic Mode - 1 - 2 - - GTK_FILL - 3 + 0 + 5 - + True True 8 0 1 - True False False - True - True 1 - 2 - 1 - 2 - - GTK_FILL - 3 + 4 - + True True 8 0 1 - True False False - True - True 1 - 2 - 2 - 3 - - GTK_FILL - 3 + 3 - + True False - Pulse Height (mV) + Frequency (Hz) - 3 - 4 - GTK_FILL + 0 + 4 - + True False - Frequency (Hz) + Pulse Height (mV) - 4 - 5 - GTK_FILL + 0 + 3 - + True True 8 0 1 - True False False - True - True 1 - 2 - 3 - 4 - - GTK_FILL - 3 + 2 - + True True 8 0 1 - True False False - True - True 1 - 2 - 4 - 5 - - GTK_FILL - 3 + 1 - + True - False - Scan both forwards and backwards. - Cyclic Mode + True + + 8 + 0 + 1 + False + False - 5 - 6 + 1 + 0 - + True False - Scans + Step Size (mV) - 6 - 7 + 0 + 2 - + True - True - False - True - True + False + Stop (mV) - 1 - 2 - 5 - 6 - - + 0 + 1 - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False + Start (mV) - 1 - 2 - 6 - 7 - - GTK_FILL - 3 + 0 + 0 diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 146c83b41c9eabf884a9c133d6f8a2f544630a5e..32c58f027b2662765dcebdd8e843e047f95eaac5 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -28,22 +28,20 @@ from copy import deepcopy from collections import OrderedDict from datetime import datetime +# try: +# import pygtk +# pygtk.require('2.0') +# except ImportError: +# print "ERR: PyGTK 2.0 not available" +# sys.exit(1) try: - import pygtk - pygtk.require('2.0') -except ImportError: - print "ERR: PyGTK 2.0 not available" - sys.exit(1) -try: - import gtk + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk, GObject except ImportError: print "ERR: GTK not available" sys.exit(1) -try: - import gobject -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]))) @@ -51,13 +49,17 @@ os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) from version import getVersion import interface.save as save import dstat_comm as comm +import experiments as exp import interface.exp_window as exp_window import interface.adc_pot as adc_pot +import interface.plot_ui +import interface.data_view import plot import params import parameter_test import analysis import zmq +import state from errors import InputError from plugin import DstatPlugin, get_hub_uri @@ -67,7 +69,7 @@ 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', + fmt='%(asctime)s %(levelname)s: [%(name)s] %(message)s', datefmt='%H:%M:%S' ) log_handler.setFormatter(log_formatter) @@ -78,18 +80,16 @@ logger = logging.getLogger("dstat.main") class Main(object): """Main program """ def __init__(self): - self.builder = gtk.Builder() + self.builder = Gtk.Builder() self.builder.add_from_file('interface/dstatinterface.glade') self.builder.connect_signals(self) - self.cell = gtk.CellRendererText() + self.cell = Gtk.CellRendererText() # 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') - self.databuffer = self.builder.get_object('databuffer2') self.stopbutton = self.builder.get_object('pot_stop') self.startbutton = self.builder.get_object('pot_start') self.adc_pot = adc_pot.adc_pot() @@ -97,11 +97,10 @@ class Main(object): 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) + self.exp_window.connect('run_utility', self.stop_ocp) + self.exp_window.connect('done_utility', self.start_ocp) + self.analysis_opt_window = analysis.AnalysisOptions(self.builder) # Setup Autosave @@ -111,9 +110,9 @@ 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.main_notebook = self.builder.get_object('main_notebook') + self.data_view = interface.data_view.DataPage(self.main_notebook) + self.info_page = interface.data_view.InfoPage(self.main_notebook) #fill adc_pot_box self.adc_pot_box = self.builder.get_object('gain_adc_box') @@ -137,12 +136,6 @@ class Main(object): 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') @@ -157,9 +150,9 @@ class Main(object): self.aboutdialog.set_version(ver) self.mainwindow.show_all() - - self.on_expcombobox_changed() - + self.exp_window.hide_exps() + + self.expnumber = 0 self.connected = False @@ -171,11 +164,8 @@ class Main(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.metadata = None # Should only be added to by plugin interface self.params_loaded = False # Disable 0MQ plugin API by default. @@ -199,7 +189,7 @@ class Main(object): params.save_params(self, 'last_params.yml') self.on_serial_disconnect_clicked() - gtk.main_quit() + mainloop.quit() def on_gtk_about_activate(self, menuitem, data=None): """Display the about window.""" @@ -209,14 +199,6 @@ class Main(object): def on_menu_analysis_options_activate(self, menuitem, data=None): self.analysis_opt_window.show() - def on_expcombobox_changed(self, data=None): - """Change the experiment window when experiment box changed.""" - model = self.expcombobox.get_model() - _, id, _ = model[self.expcombobox.get_active()] # id is in 2nd col - self.statusbar.remove_all(self.error_context_id) - if not self.exp_window.set_exp(id): - self.statusbar.push( - self.error_context_id, "Experiment not yet implemented") def on_serial_refresh_clicked(self, data=None): """Refresh list of serial devices.""" @@ -231,22 +213,22 @@ class Main(object): try: self.serial_connect.set_sensitive(False) - self.version = comm.version_check(self.serial_liststore.get_value( + state.dstat_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: + if not len(state.dstat_version) == 2: self.statusbar.push(self.error_context_id, "Communication Error") return else: - self.adc_pot.set_version(self.version) + self.adc_pot.set_version(state.dstat_version) self.statusbar.push(self.error_context_id, "".join(["DStat version: ", - str(self.version[0]), - ".", str(self.version[1])]) + str(state.dstat_version[0]), + ".", str(state.dstat_version[1])]) ) comm.read_settings() @@ -280,8 +262,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) @@ -301,24 +283,23 @@ class Main(object): self.adc_pot.ui['short_true'].set_sensitive(False) self.on_serial_connect_clicked() - def start_ocp(self): + def start_ocp(self, data=None): """Start OCP measurements.""" - if self.version[0] >= 1 and self.version[1] >= 2: + if state.dstat_version[0] >= 1 and state.dstat_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(comm.PMTIdle()) + state.ser.start_exp(exp.PMTIdle()) else: logger.info("Start OCP") - comm.serial_instance.proc_pipe_p.send(comm.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) + self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data), + GObject.timeout_add(250, self.ocp_running_proc) ) self.ocp_is_running = True @@ -326,18 +307,18 @@ class Main(object): logger.info("OCP measurements not supported on v1.1 boards.") return - def stop_ocp(self): + def stop_ocp(self, data=None): """Stop OCP measurements.""" - if self.version[0] >= 1 and self.version[1] >= 2: + if state.dstat_version[0] >= 1 and state.dstat_version[1] >= 2: if self.pmt_mode == True: 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) + GObject.source_remove(i) while self.ocp_running_proc(): pass self.ocp_is_running = False @@ -356,21 +337,21 @@ 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 + + try: + data = "".join(["OCP: ", + "{0:.3f}".format(incoming), + " V"]) + self.ocp_disp.set_text(data) + except ValueError: + pass - 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 + incoming = state.ser.get_data() return True @@ -389,19 +370,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,11 +395,17 @@ class Main(object): # Ignore expected exceptions when triggering experiment from UI. pass - def run_active_experiment(self): + def run_active_experiment(self, param_override=None, metadata=None): """Run currently visible experiment.""" # Assign current experiment a unique identifier. experiment_id = uuid.uuid4() self.active_experiment_id = experiment_id + logger.info("Current measurement id: %s", experiment_id.hex) + + self.metadata = metadata + + if self.metadata is not None: + logger.info("Loading external metadata") def exceptions(): """ Cleans up after errors """ @@ -430,54 +414,23 @@ class Main(object): 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() - # nb.get_nth_page( - # nb.page_num(self.period_window)).show() - self.ft_plot.clearall() - self.ft_plot.changetype(self.current_exp) - else: - nb.get_nth_page(nb.page_num(self.ft_window)).hide() - # 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() - - self.plot_proc = gobject.timeout_add(200, - self.experiment_running_plot) - self.experiment_proc = ( - 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() + state.ser.flush_data() - selection = self.expcombobox.get_active() parameters = {} - parameters['version'] = self.version + parameters['metadata'] = self.metadata # Make sure these are defined parameters['sync_true'] = False parameters['shutter_true'] = False try: - parameters.update(self.adc_pot.params) - parameters.update(self.analysis_opt_window.params) - + if param_override is not None: + params.set_params(self, param_override) + + parameters.update(params.get_params(self)) + self.line = 0 self.lastline = 0 self.lastdataline = 0 @@ -486,90 +439,32 @@ class Main(object): self.startbutton.set_sensitive(False) self.stopbutton.set_sensitive(True) self.statusbar.remove_all(self.error_context_id) + + self.current_exp = self.exp_window.setup_exp(parameters) + + interface.plot_ui.replace_notebook_exp( + self.plot_notebook, self.current_exp, self.window + ) - if selection == 0: # CA - # Add experiment parameters to existing - parameters.update(self.exp_window.get_params('cae')) - 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 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 experiment_id - - elif selection == 2: # CV - parameters.update(self.exp_window.get_params('cve')) - parameter_test.cv_test(parameters) - - self.current_exp = comm.CVExp(parameters) - run_experiment() - - 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 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 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 experiment_id - - 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')) - parameter_test.pot_test(parameters) - - self.current_exp = comm.PotExp(parameters) - run_experiment() + for plot in self.current_exp.plots: + plot.changetype(self.current_exp) + + self.data_view.clear_exps() + self.info_page.clear() + + state.ser.start_exp(self.current_exp) - return experiment_id + # Flush data pipe + state.ser.flush_data() - else: - self.statusbar.push(self.error_context_id, - "Experiment not yet implemented.") - exceptions() + self.plot_proc = GObject.timeout_add(200, + self.experiment_running_plot) + self.experiment_proc = ( + GObject.idle_add(self.experiment_running_data), + GObject.idle_add(self.experiment_running_proc) + ) + + return experiment_id except ValueError as i: logger.info("ValueError: %s",i) @@ -579,6 +474,8 @@ class Main(object): raise except KeyError as i: + import traceback + traceback.print_exc() logger.info("KeyError: %s", i) self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.") @@ -615,22 +512,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: - 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['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 - + 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: @@ -652,9 +546,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": @@ -663,9 +556,7 @@ class Main(object): else: logger.warning("Unrecognized experiment return code: %s", proc_buffer) - return False - return True except EOFError as err: @@ -677,128 +568,117 @@ class Main(object): self.experiment_done() return False - def experiment_running_plot(self): + def experiment_running_plot(self, force_refresh=False): """Plot all data in current_exp.data. Run in GTK main loop. Always returns True so must be manually removed from GTK's queue. """ - 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.lastline = self.line - self.plot.updateline(self.current_exp, self.line) - self.plot.redraw() + for plot in self.current_exp.plots: + if self.line > self.lastline: + plot.addline() + # make sure all of last line is added + plot.updateline(self.current_exp, self.lastline) + self.lastline = self.line + plot.updateline(self.current_exp, self.line) + + if plot.continuous_refresh is True or force_refresh is True: + plot.redraw() return True def experiment_done(self): """Clean up after data acquisition is complete. Update plot and copy data to raw data tab. Saves data if autosave enabled. """ - self.current_exp.time = datetime.now() - 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.redraw() - - 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) - - # 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, - "Integral: %s A" % self.current_exp.analysis['FT Integral'][0][1] - ) - except KeyError: - pass + self.current_exp.experiment_done() + GObject.source_remove(self.experiment_proc[0]) + GObject.source_remove(self.plot_proc) # stop automatic plot update + self.experiment_running_plot(force_refresh=True) + + + # # 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.redraw() + # + # 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) + + self.current_exp.experiment_done() + + # Write DStat commands + self.info_page.set_text(self.current_exp.get_info_text()) + + try: + self.statusbar.push( + self.message_context_id, + "Integral: %s A" % self.current_exp.analysis['FT Integral'][0][1] + ) + except KeyError: + pass + + # Data Output + analysis_buffer = [] + + if self.current_exp.analysis != {}: + analysis_buffer.append("# ANALYSIS") + for key, value in self.current_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: + self.info_page.add_line(i) + + self.data_view.add_exp(self.current_exp) - # Data Output - analysis_buffer = [] - - if self.current_exp.analysis != {}: - analysis_buffer.append("# ANALYSIS") - for key, value in self.current_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: - 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)): - try: - line_buffer[i] += "%s " % dimension[i] - 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, - self.autosavedir_button.get_filename(), - self.autosavename.get_text() - ) - - save.autoPlot(self.current_exp, - self.autosavedir_button.get_filename(), - self.autosavename.get_text() - ) + # Autosaving + if self.autosave_checkbox.get_active(): + save.autoSave(self.current_exp, + self.autosavedir_button.get_filename().decode('utf-8'), + self.autosavename.get_text() + ) + save.autoPlot(self.current_exp, + self.autosavedir_button.get_filename().decode('utf-8'), + self.autosavename.get_text() + ) + # uDrop # UI stuff - self.spinner.stop() - self.startbutton.set_sensitive(True) - self.stopbutton.set_sensitive(False) + finally: + self.metadata = None # Reset metadata + + 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() + 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.""" try: - comm.serial_instance.ctrl_pipe_p.send('a') + state.ser.stop_exp() except AttributeError: pass @@ -865,18 +745,23 @@ class Main(object): self.plugin.reset() # Periodically process outstanding message received on plugin sockets. - self.plugin_timeout_id = gtk.timeout_add(500, + 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) + 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() + GObject.threads_init() MAIN = Main() - gtk.main() + mainloop = GObject.MainLoop() + try: + mainloop.run() + except KeyboardInterrupt: + logger.info('Ctrl+C hit, quitting') + MAIN.quit() diff --git a/dstat_interface/mr_db b/dstat_interface/mr_db new file mode 160000 index 0000000000000000000000000000000000000000..72481e585d92d0adab6537718a4d18164df1b445 --- /dev/null +++ b/dstat_interface/mr_db @@ -0,0 +1 @@ +Subproject commit 72481e585d92d0adab6537718a4d18164df1b445 diff --git a/dstat_interface/parameter_test.py b/dstat_interface/parameter_test.py index 5600a8dd1a0d619c65baf72e83e28f6de9c1656f..ec629439eac8b2f53edaa63ea6590fa0c9ef0319 100755 --- a/dstat_interface/parameter_test.py +++ b/dstat_interface/parameter_test.py @@ -26,12 +26,12 @@ logger = logging.getLogger("dstat.parameter_test") def lsv_test(params): """Test LSV parameters for sanity""" + test_parameters = ['clean_mV', 'dep_mV', 'clean_s', 'dep_s', 'start', + 'stop', 'slope'] parameters = {} for i in params: - try: + if i in test_parameters: parameters[i] = int(params[i]) - except TypeError: - pass if (parameters['clean_mV'] > 1499 or parameters['clean_mV'] < -1500): @@ -62,12 +62,12 @@ def lsv_test(params): def cv_test(params): """Test CV parameters for sanity""" + test_parameters = ['clean_mV', 'dep_mV', 'clean_s', 'dep_s', 'start', + 'stop', 'slope', 'v1', 'v2', 'scans'] parameters = {} for i in params: - try: + if i in test_parameters: parameters[i] = int(params[i]) - except TypeError: - pass if (parameters['clean_mV'] > 1499 or parameters['clean_mV'] < -1500): @@ -104,12 +104,12 @@ def cv_test(params): def swv_test(params): """Test SWV parameters for sanity""" + test_parameters = ['clean_mV', 'dep_mV', 'clean_s', 'dep_s', 'start', + 'stop', 'step', 'pulse', 'freq'] parameters = {} for i in params: - try: + if i in test_parameters: parameters[i] = int(params[i]) - except TypeError: - pass if params['cyclic_true'] : if int(params['scans']) < 1: @@ -156,12 +156,12 @@ def swv_test(params): def dpv_test(params): """Test DPV parameters for sanity""" + test_parameters = ['clean_mV', 'dep_mV', 'clean_s', 'dep_s', 'start', + 'stop', 'step', 'pulse', 'period', 'width'] parameters = {} for i in params: - try: + if i in test_parameters: parameters[i] = int(params[i]) - except TypeError: - pass if (parameters['clean_mV'] > 1499 or parameters['clean_mV'] < -1500): @@ -228,12 +228,11 @@ def pd_test(parameters): def pot_test(params): """Test POT parameters for sanity""" + test_parameters = ['time'] parameters = {} for i in params: - try: + if i in test_parameters: parameters[i] = int(params[i]) - except TypeError: - pass if (int(parameters['time']) <= 0): raise InputError(parameters['time'], diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 3ad6181c450daadf97263b06ce2439ee33eb465b..0aa85802c1dd47a248743b01beb1c8dc84b55f74 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -17,18 +17,19 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import logging import yaml -from errors import ErrorLogger, InputError -_logger = ErrorLogger(sender="dstat-interface-params") +from errors import InputError + +logger = logging.getLogger('dstat.params') def get_params(window): """Fetches and returns dict of all parameters for saving.""" + selection = window.exp_window.expcombobox.get_active_id() parameters = {} - - selection = window.exp_window.select_to_key[window.expcombobox.get_active()] parameters['experiment_index'] = selection try: @@ -47,7 +48,7 @@ def get_params(window): def save_params(window, path): """Fetches current params and saves to path.""" - + logger.info("Save to: %s", path) params = get_params(window) with open(path, 'w') as f: @@ -55,11 +56,13 @@ def save_params(window, path): 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 + except KeyError: # Will be thrown because no experiment will be selected + pass with open(path, 'r') as f: params = yaml.load(f) @@ -67,13 +70,10 @@ def load_params(window, path): def set_params(window, params): window.adc_pot.params = params - - if not 'experiment_index' in params: - logger.warning("Missing experiment parameters.") - return - window.expcombobox.set_active( - window.exp_window.classes[params['experiment_index']][0]) - window.exp_window.set_params(params['experiment_index'], params) + if 'experiment_index' in params: + window.exp_window.expcombobox.set_active_id(params['experiment_index']) + window.exp_window.set_params(params['experiment_index'], params) + window.analysis_opt_window.params = params window.params_loaded = True diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 7c5c017f371e2ea7a00a1a02b2a347bb02d6c9fd..7e87c41b05021601d353953f9ddc9ce2bdbbefa8 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -20,17 +20,20 @@ """ Creates data plot. """ -import gtk +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk +except ImportError: + print "ERR: GTK not available" + sys.exit(1) + from matplotlib.figure import Figure -#from matplotlib.backends.backend_gtkcairo\ -# import FigureCanvasGTKCairo as FigureCanvas -#from matplotlib.backends.backend_gtkcairo\ -# import NavigationToolbar2Cairo as NavigationToolbar -from matplotlib.backends.backend_gtkagg \ - import FigureCanvasGTKAgg as FigureCanvas -from matplotlib.backends.backend_gtkagg \ - import NavigationToolbar2GTKAgg as NavigationToolbar +from matplotlib.backends.backend_gtk3agg \ + import FigureCanvasGTK3Agg as FigureCanvas +from matplotlib.backends.backend_gtk3 \ + import NavigationToolbar2GTK3 as NavigationToolbar try: import seaborn as sns except ImportError: @@ -88,115 +91,4 @@ def findBounds(y): break return (start_index, stop_index) - - -class PlotBox(object): - """Contains main data plot and associated methods.""" - def __init__(self, plotwindow_instance): - """Creates plot and moves it to a gtk container. - - Arguments: - plotwindow_instance -- gtk container to hold plot. - """ - - self.figure = Figure() - self.figure.subplots_adjust(left=0.07, bottom=0.07, - right=0.96, top=0.96) - self.axe1 = self.figure.add_subplot(111) - - self.axe1.plot([0, 1], [0, 1]) - - self.axe1.ticklabel_format(style='sci', scilimits=(0, 3), - useOffset=False, axis='y') - self.canvas = FigureCanvas(self.figure) - self.win = gtk.Window() - self.vbox = gtk.VBox() - self.win.add(self.vbox) - self.vbox.pack_start(self.canvas) - self.toolbar = NavigationToolbar(self.canvas, self.win) - self.vbox.pack_start(self.toolbar, False, False) - self.vbox.reparent(plotwindow_instance) - - def clearall(self): - """Remove all lines on plot. """ - for i in range(len(self.axe1.lines)): - self.axe1.lines.pop(0) - self.addline() - - def clearline(self, line_number): - """Remove a line specified by line_number.""" - self.lines[line_number].remove() - self.lines.pop(line_number) - - def addline(self): - """Add a new line to plot. (initialized with dummy data)))""" - self.axe1.plot([0, 1], [0, 1]) - - def updateline(self, Experiment, line_number): - """Update a line specified by line_number with data stored in - the Experiment instance. - """ - # limits display to 2000 data points per line - divisor = len(Experiment.data['data'][line_number][0]) // 2000 + 1 - - self.axe1.lines[line_number].set_ydata( - Experiment.data['data'][line_number][1][1::divisor]) - self.axe1.lines[line_number].set_xdata( - Experiment.data['data'][line_number][0][1::divisor]) - - def changetype(self, Experiment): - """Change plot type. Set axis labels and x bounds to those stored - 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() - - def redraw(self): - """Autoscale and refresh the plot.""" - self.axe1.relim() - self.axe1.autoscale(True, axis = 'y') - self.figure.canvas.draw() - - return True - -class FT_Box(PlotBox): - def updateline(self, Experiment, line_number): - def search_value(data, target): - for i in range(len(data)): - if data[i] > target: - return i - - 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:] - x1 = x[i:] - avg = mean(y1) - min_index, max_index = findBounds(y1) - y1[min_index] = avg - y1[max_index] = avg - 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.data['ft'] = [(f, Y)] - - def changetype(self, Experiment): - """Change plot type. Set axis labels and x bounds to those stored - 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() - - diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index ebe2493324f4a2dc449128aace59ef83c6f0d506..59ec93d013d7c1a6e3410f6722fc4b6ed5f55edb 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -98,9 +98,26 @@ class DstatPlugin(ZmqPlugin): return get_params(self.parent) def on_execute__run_active_experiment(self, request): + data = decode_content_data(request) self.parent.statusbar.push(self.parent.message_context_id, "µDrop " "acquisition requested.") - return self.parent.run_active_experiment() + return self.parent.run_active_experiment( + param_override=data.get('params'), + metadata=data.get('metadata') + ) + + def on_execute__set_metadata(self, request=None): + ''' + Args + ---- + + (dict) : Dictionary of metadata to be used in subsequent + experiments. Should include `device_id`, `patient_id`, and + `experiment_id`. Leave blank to reset all metadata fields or set + individual keys to `None` to reset individual values. + ''' + data = decode_content_data(request) + self.parent.metadata = request def on_execute__save_text(self, request): ''' diff --git a/dstat_interface/state.py b/dstat_interface/state.py new file mode 100644 index 0000000000000000000000000000000000000000..7e3f1958894aab4a5482c65cca03cb5f3cdd1d93 --- /dev/null +++ b/dstat_interface/state.py @@ -0,0 +1,5 @@ +from collections import OrderedDict + +settings = OrderedDict() +ser = None +dstat_version = None \ No newline at end of file