From a03c5882811298add064970a46c0c422b46da8a1 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Fri, 8 Apr 2016 16:09:39 -0400 Subject: [PATCH 01/66] Updated changelog --- CHANGELOG | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index c0d2d5e..67c086b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -47,4 +47,9 @@ 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 + -Basic statistics + +Version 1.2 + -Old Microdrop interface depreciated + -New zmq_plugin based interface + -Internal changes to save functionality and plot storage. \ No newline at end of file -- GitLab From 41ac11d532853728725f47a354557fba2c13efbf Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 12 Apr 2016 23:25:03 -0400 Subject: [PATCH 02/66] Fix calibration mode (broken with changes to parameter storage) --- dstat_interface/interface/exp_int.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index 1cb9a34..4c477a7 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -441,7 +441,7 @@ class CAL(ExpInterface): __main__.MAIN.stop_ocp() __main__.MAIN.spinner.start() - offset = dstat_comm.measure_offset(self.get_params()['time']) + offset = dstat_comm.measure_offset(self.params['time']) for i in offset: _logger.error(" ".join((i, str(-offset[i]))), "INFO") -- GitLab From 0967f0e72097b4098b8170ac7de1e0bcfc16b478 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Mon, 2 May 2016 18:37:17 -0400 Subject: [PATCH 03/66] Added attribute for storing metadata and setting it from the zmq plugin. --- dstat_interface/main.py | 5 ++++- dstat_interface/plugin.py | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 146c83b..49d403d 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -171,7 +171,9 @@ class Main(object): 'menu_dropbot_disconnect') self.dropbot_enabled = False self.dropbot_triggered = False - + + self.metadata = None + self.plot_notebook.get_nth_page( self.plot_notebook.page_num(self.ft_window)).hide() self.plot_notebook.get_nth_page( @@ -470,6 +472,7 @@ class Main(object): selection = self.expcombobox.get_active() parameters = {} parameters['version'] = self.version + parameters['metadata'] = self.metadata # Make sure these are defined parameters['sync_true'] = False diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index ebe2493..c4ce9c1 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -101,6 +101,18 @@ class DstatPlugin(ZmqPlugin): self.parent.statusbar.push(self.parent.message_context_id, "µDrop " "acquisition requested.") return self.parent.run_active_experiment() + + 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. + ''' + self.parent.metadata = request def on_execute__save_text(self, request): ''' -- GitLab From 83bd302f4bfbd63d5062475896932aa87e3f4c47 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 03:34:04 -0400 Subject: [PATCH 04/66] Added database window. --- .../interface/dstatinterface.glade | 27 +++++++++++++++++++ dstat_interface/main.py | 8 ++++++ dstat_interface/params.py | 4 ++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/dstat_interface/interface/dstatinterface.glade b/dstat_interface/interface/dstatinterface.glade index 8ae7a00..6503660 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -749,6 +749,11 @@ Thanks to Christian Fobel for help with Dropbot Plugin False gtk-preferences + + True + False + gtk-info + @@ -892,6 +897,28 @@ Thanks to Christian Fobel for help with Dropbot Plugin + + + True + False + Database + + + True + False + + + View… + True + False + image7 + False + + + + + + True diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 49d403d..b3afdc6 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -50,6 +50,7 @@ os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) from version import getVersion import interface.save as save +from interface.db import DB_Window import dstat_comm as comm import interface.exp_window as exp_window import interface.adc_pot as adc_pot @@ -58,6 +59,7 @@ import params import parameter_test import analysis import zmq +import db from errors import InputError from plugin import DstatPlugin, get_hub_uri @@ -103,6 +105,12 @@ class Main(object): self.exp_window = exp_window.Experiments(self.builder) self.analysis_opt_window = analysis.AnalysisOptions(self.builder) + + self.db_window = DB_Window() + self.builder.get_object('menu_database_options').connect_object( + 'activate', DB_Window.show, self.db_window + ) + self.db_window.connect('db_reset', db.restart_db) # Setup Autosave self.autosave_checkbox = self.builder.get_object('autosave_checkbutton') diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 3ad6181..9e2ae40 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -42,7 +42,8 @@ def get_params(window): logger.info("No gain selected.") parameters.update(window.exp_window.get_params(selection)) parameters.update(window.analysis_opt_window.params) - + parameters.update(window.db_window.params) + return parameters def save_params(window, path): @@ -75,5 +76,6 @@ def set_params(window, params): window.exp_window.classes[params['experiment_index']][0]) window.exp_window.set_params(params['experiment_index'], params) window.analysis_opt_window.params = params + window.db_window.params = params window.params_loaded = True -- GitLab From eb8866f513f26460ef321ad309a212a6ffe613bd Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:43:17 -0400 Subject: [PATCH 05/66] Add export method to experiment classes --- dstat_interface/dstat_comm.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index ffa43f2..8cadd00 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -474,6 +474,21 @@ class Experiment(object): in subclass. """ pass + + def export(self): + """Return a dict containing data for saving.""" + output = { + "datatype" : self.datatype, + "xlabel" : self.xlabel, + "ylabel" : self.ylabel, + "xmin" : self.xmin, + "xmax" : self.xmax, + "parameters" : self.parameters, + "data" : self.data, + "commands" : self.commands + } + + return output class CALExp(Experiment): """Offset calibration experiment""" -- GitLab From 09bcfb5df46bf86ab391feafc81e7665817ed6c6 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:44:14 -0400 Subject: [PATCH 06/66] Add database code --- dstat_interface/db.py | 194 +++++++++++++++++++++++++ dstat_interface/interface/db.glade | 219 +++++++++++++++++++++++++++++ dstat_interface/interface/db.py | 143 +++++++++++++++++++ 3 files changed, 556 insertions(+) create mode 100644 dstat_interface/db.py create mode 100644 dstat_interface/interface/db.glade create mode 100755 dstat_interface/interface/db.py diff --git a/dstat_interface/db.py b/dstat_interface/db.py new file mode 100644 index 0000000..3e1fda8 --- /dev/null +++ b/dstat_interface/db.py @@ -0,0 +1,194 @@ +import logging +from time import sleep, time +from os.path import expanduser +from uuid import uuid4 + +from BTrees.OOBTree import OOBTree + +import mr_db.mr_db as db + +logger = logging.getLogger("dstat.db") + +current_db = None + +def start_db(path=None): + global current_db + current_db = Database(data_dir=path) + +def restart_db(object, path): + logger.info("Restarting database") + global current_db + if current_db is None: + logger.info("No database running") + start_db(path=path) + else: + stop_db() + start_db(path=path) + +def stop_db(): + global current_db + if not current_db is None: + logger.info("Stopping ZEO") + current_db.disconnect() + current_db = None + db.stop_server() + else: + logger.warning("Tried to disconnect ZEO when not connected") + +class Database(object): + def __init__(self, name='dstat', data_dir=None): + if data_dir is None: + data_dir = expanduser('~/.mr_db') + self.connected = False + self.name = name + + self.db_connect(data_dir) + + self.db = self.connection.databases + + # Make sure database exists + if not self.db.has_key(name): + self.db[name] = OOBTree() + db.transaction.commit() + + def disconnect(self): + if self.connected is True: + self.connection.db.close() + else: + logger.war("Tried to disconnect ZEO when not connected") + + def db_connect(self, root_dir): + """Connects to ZEO process. Starts ZEO if not running. Returns + connection object. + """ + if root_dir == '': + root_dir = None + + while self.connected is False: + try: + self.connection = db.DbConnection(root_dir=root_dir) + self.connected = True + logger.info("Connected to ZEO server") + except db.ClientStorage.ClientDisconnected: + db.stop_server() + logger.info("Starting ZEO server -- root_dir = %s", root_dir) + db_proc = db.start_server(root_dir=root_dir) + sleep(3) + + def add_results(self, measurement_uuid=None, measurement_name=None, + experiment_uuid=None, experiment_metadata=None, + patient_id=None, + timestamp=None, + data=None): + + """Add a measurement""" + + if experiment_metadata is None: + experiment_metadata = {} + + try: + logger.info("Starting DB transaction") + db.transaction.begin() + + logger.info("Creating Experiment with id: %s", experiment_uuid) + exp_db, exp_id = self.add_experiment( + experiment_uuid=experiment_uuid, + timestamp=timestamp, + **experiment_metadata) + + logger.info("Adding Measurement with id: %s", measurement_uuid) + name = self.add_dstat_measurement(experiment=exp_db[exp_id], + measurement_uuid=measurement_uuid, + name=measurement_name, + timestamp=timestamp, + data=data) + + if patient_id is not None: + if not patient_id in self.db['patients']: + logger.info("Creating patient with id: %s", patient_id) + patient = db.Patient(pid=patient_id) + self.db['patients'][patient_id] = patient + + if not exp_id in self.db['patients'][patient_id].experiments: + logger.info("Linking experiment into patient with id: %s", + patient_id) + self.db['patients'][patient_id].link_experiment(exp_db, + exp_id) + + logger.info("Committing DB transaction") + db.transaction.commit() + + return name + + except: + logger.error("Aborting DB transaction") + db.transaction.abort() + raise + + def add_experiment(self, experiment_uuid=None, timestamp=None, **kwargs): + """Add a new experiment. Will raise KeyExistsError if id is already + in db to avoid unintended collisions. + ---- + Arguments: + id: experiment id---UUID will be generated if not supplied + timestamp: current time---Will be generated if not supplied + kwargs: additional keyword arguments that will be saved in experiment. + """ + + if experiment_uuid is None: + experiment_uuid = uuid4().hex + if timestamp is None: + timestamp = time() + + kwargs.update({'id':experiment_uuid, 'timestamp':timestamp}) + + if not experiment_uuid in self.db[self.name]: + self.db[self.name][experiment_uuid] = db.PersistentMapping() + else: + logger.info("Experiment already exists, appending") + + self.db[self.name][experiment_uuid].update(kwargs) + + return (self.db[self.name], experiment_uuid) + + def add_dstat_measurement(self, experiment, measurement_uuid=None, + data=None, timestamp=None, name=None): + + if measurement_uuid is None: + measurement_uuid = uuid4().hex + if timestamp is None: + timestamp = time() + if data is None: + data = {} + + if not 'measurements' in experiment: + experiment['measurements'] = db.PersistentMapping() + if not 'measurements_by_name' in experiment: + experiment['measurements_by_name'] = db.PersistentMapping() + + if measurement_uuid in experiment['measurements']: + raise db.KeyExistsError(measurement_uuid, + "Measurement ID already exists. Access directly to update") + + data['timestamp'] = timestamp + data['id'] = measurement_uuid + + if name is not None: + data['name'] = name + + experiment['measurements'][measurement_uuid] = data + + if name is not None: + while name in experiment['measurements_by_name']: + first, sep, last = name.rpartition('-') + + if last.isdigit(): + index = int(last) + index += 1 + name = sep.join((first,str(index))) + else: + name += '-1' + + experiment['measurements_by_name'][name] = data + + return name \ 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 0000000..20a833a --- /dev/null +++ b/dstat_interface/interface/db.glade @@ -0,0 +1,219 @@ + + + + + + False + Database + center-on-parent + True + + + True + False + + + Enable Database saving + True + True + False + True + True + + + + False + False + 0 + + + + + True + False + 5 + 3 + + + + + + + + + True + False + Measurement ID + + + 1 + 2 + + + + + True + False + Experiment ID + + + + + True + True + + False + False + True + True + + + 1 + 2 + + + + + True + True + False + + False + False + True + True + + + 1 + 2 + 1 + 2 + + + + + True + True + False + + False + False + True + True + + + 1 + 2 + 3 + 4 + + + + + True + False + Patient ID + + + 3 + 4 + + + + + Generate New + True + True + True + + + + 2 + 3 + + + + + True + False + DB Path + + + 4 + 5 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 4 + 5 + + + + + Apply + True + True + True + + + + 2 + 3 + 4 + 5 + + + + + + + + True + False + Measurement Name + + + 2 + 3 + + + + + True + True + + False + False + True + True + + + 1 + 2 + 2 + 3 + + + + + True + True + 1 + + + + + + diff --git a/dstat_interface/interface/db.py b/dstat_interface/interface/db.py new file mode 100755 index 0000000..94125c0 --- /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 + +import gtk +import gobject + +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.""" + logger.info("getter") + self._get_params() + logger.info("params: %s", self._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() + logger.info("%s=%s",i,text) + if text == "": + text = None + self._params[i] = text + + @params.setter + def params(self, params): + logger.info("Setter Params: %s", 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() + logger.info("params: %s", self._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 -- GitLab From 55d72eb717ecb9370369e01cc2dc75c2eebaade5 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:44:50 -0400 Subject: [PATCH 07/66] Switch params to native logging --- dstat_interface/params.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 9e2ae40..80bf816 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -17,11 +17,13 @@ # # 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.""" -- GitLab From 27e5e7d6de497033fe5e5cff4c71307f3574b557 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:45:23 -0400 Subject: [PATCH 08/66] Only make some db parameters persistent. --- dstat_interface/params.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 80bf816..0a33f88 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -44,13 +44,13 @@ def get_params(window): logger.info("No gain selected.") parameters.update(window.exp_window.get_params(selection)) parameters.update(window.analysis_opt_window.params) - parameters.update(window.db_window.params) + parameters.update(window.db_window.persistent_params) return parameters 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: @@ -58,7 +58,7 @@ 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 -- GitLab From 1ecd840da954ed6f185ee64fb1f2838f6cca2b38 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:45:45 -0400 Subject: [PATCH 09/66] Stop db on quit. --- dstat_interface/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index b3afdc6..9886380 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -209,6 +209,7 @@ class Main(object): params.save_params(self, 'last_params.yml') self.on_serial_disconnect_clicked() + db.stop_db() gtk.main_quit() def on_gtk_about_activate(self, menuitem, data=None): -- GitLab From 335a55e85deaca5868c392eb2ef49766d64a6516 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:47:07 -0400 Subject: [PATCH 10/66] Make sure cleanup code executes even if experiment fails. --- dstat_interface/main.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 9886380..346715f 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -799,13 +799,16 @@ class Main(object): # 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.""" -- GitLab From 12731bd0b45fc98d473d072eeceed0cd066e29db Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 4 May 2016 20:47:39 -0400 Subject: [PATCH 11/66] Tie db into main. --- dstat_interface/main.py | 211 ++++++++++++++++++++++++---------------- 1 file changed, 129 insertions(+), 82 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 346715f..e8ea118 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -180,7 +180,7 @@ class Main(object): self.dropbot_enabled = False self.dropbot_triggered = False - self.metadata = None + self.metadata = None # Should only be added to by plugin interface self.plot_notebook.get_nth_page( self.plot_notebook.page_num(self.ft_window)).hide() @@ -428,11 +428,23 @@ class Main(object): # Ignore expected exceptions when triggering experiment from UI. pass - def run_active_experiment(self): + def run_active_experiment(self, 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") + self.db_window.update_from_metadata(self.metadata) + elif self.db_window.params['exp_id_entry'] is None: + logger.info("DB exp_id field blank, autogenerating") + self.db_window.on_exp_id_autogen_button_clicked() + + self.db_window.params = {'measure_id_entry':experiment_id.hex} def exceptions(): """ Cleans up after errors """ @@ -458,7 +470,13 @@ class Main(object): else: nb.get_nth_page(nb.page_num(self.ft_window)).hide() # nb.get_nth_page(nb.page_num(self.period_window)).hide() - + + if parameters['db_enable_checkbutton']: + if db.current_db is None: + db.start_db() + elif not db.current_db.connected: + db.restart_db() + comm.serial_instance.proc_pipe_p.send(self.current_exp) # Flush data pipe @@ -489,7 +507,8 @@ class Main(object): try: parameters.update(self.adc_pot.params) parameters.update(self.analysis_opt_window.params) - + parameters.update(self.db_window.params) + self.line = 0 self.lastline = 0 self.lastdataline = 0 @@ -707,26 +726,74 @@ class Main(object): """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() + try: + 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 + + # 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['ft']: + for scan in self.current_exp.data['data']: for dimension in scan: for i in range(len(dimension)): try: @@ -736,67 +803,47 @@ class Main(object): 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 - - # 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() - ) - + 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() + ) + # Database output + if self.current_exp.parameters['db_enable_checkbutton']: + meta = {} + + if self.current_exp.parameters['metadata'] is not None: + metadata = self.current_exp.parameters['metadata'] + exp_metakeys = ['experiment_uuid', 'patient_id', 'name'] + meta.update( + {k: metadata[k] + for k in metadata + if k not in exp_metakeys + } + ) + + name = self.current_exp.parameters['measure_name_entry'] + + newname = db.current_db.add_results( + measurement_uuid=self.active_experiment_id.hex, + measurement_name=name, + experiment_uuid=self.current_exp.parameters['exp_id_entry'], + experiment_metadata=meta, + patient_id=self.current_exp.parameters['patient_id_entry'], + timestamp=None, + data=self.current_exp.export() + ) + + self.db_window.params = {'measure_name_entry':newname} + # uDrop # UI stuff finally: -- GitLab From 8c9b95079188e58ca3f3d3bd0445b2aebfc331db Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 13:46:41 -0400 Subject: [PATCH 12/66] Add mr_db submodule --- .gitmodules | 9 +++++++++ dstat_interface/mr_db | 1 + 2 files changed, 10 insertions(+) create mode 100644 .gitmodules create mode 160000 dstat_interface/mr_db diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a95ffcb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "mr_db"] + path = mr_db + url = /Users/mdryden/src/mr_db +[submodule "dstat-interface/mr_db"] + path = dstat-interface/mr_db + url = http://microfluidics.utoronto.ca/gitlab/mdryden/mr-db.git +[submodule "dstat_interface/mr_db"] + path = dstat_interface/mr_db + url = http://microfluidics.utoronto.ca/gitlab/mdryden/mr-db.git diff --git a/dstat_interface/mr_db b/dstat_interface/mr_db new file mode 160000 index 0000000..bb4c163 --- /dev/null +++ b/dstat_interface/mr_db @@ -0,0 +1 @@ +Subproject commit bb4c16328b85faf9dfa7d15ab1c39c4bc9fa882a -- GitLab From 4e09a2600fabc22f38ea669b921a35e690eb7dd2 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 13:52:59 -0400 Subject: [PATCH 13/66] update pavement.py --- pavement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pavement.py b/pavement.py index 0aea7f5..bf1f907 100644 --- a/pavement.py +++ b/pavement.py @@ -18,7 +18,8 @@ setup(name='dstat_interface', license='GPLv3', packages=['dstat_interface', ], install_requires=['matplotlib', 'numpy', 'pyserial', 'pyzmq', - 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2'], + 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2', 'zodb', + 'zeo', 'psutil'], # Install data listed in `MANIFEST.in` include_package_data=True) -- GitLab From 2d76ffea05a65ac15969c7922b2fe98e46185485 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 14:10:14 -0400 Subject: [PATCH 14/66] update mr_db --- dstat_interface/mr_db | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/mr_db b/dstat_interface/mr_db index bb4c163..72481e5 160000 --- a/dstat_interface/mr_db +++ b/dstat_interface/mr_db @@ -1 +1 @@ -Subproject commit bb4c16328b85faf9dfa7d15ab1c39c4bc9fa882a +Subproject commit 72481e585d92d0adab6537718a4d18164df1b445 -- GitLab From 2bb944eb8147265c2d484d193d3fc1382465e158 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 15:13:50 -0400 Subject: [PATCH 15/66] Remove debugging statements --- dstat_interface/interface/db.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/dstat_interface/interface/db.py b/dstat_interface/interface/db.py index 94125c0..7cedf24 100755 --- a/dstat_interface/interface/db.py +++ b/dstat_interface/interface/db.py @@ -75,9 +75,7 @@ class DB_Window(gobject.GObject): @property def params(self): """Dict of parameters.""" - logger.info("getter") - self._get_params() - logger.info("params: %s", self._params) + self._get_params() return self._params def _get_params(self): @@ -87,14 +85,12 @@ class DB_Window(gobject.GObject): self._params[i] = self.ui[i].get_active() elif i.endswith('entry'): text = self.ui[i].get_text() - logger.info("%s=%s",i,text) if text == "": text = None self._params[i] = text @params.setter def params(self, params): - logger.info("Setter Params: %s", params) if self._params is {}: self._params = dict.fromkeys(self.ui.keys()) @@ -102,7 +98,6 @@ class DB_Window(gobject.GObject): if i in params: self._params[i] = params[i] self._set_params() - logger.info("params: %s", self._params) def _set_params(self): """Updates UI with new parameters.""" -- GitLab From 1650213944c950c43700611fae2ed08200b3c47d Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 15:14:18 -0400 Subject: [PATCH 16/66] Add metadata input to plugin. --- dstat_interface/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index c4ce9c1..9fb6d2b 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -98,9 +98,10 @@ 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(metadata=data['metadata']) def on_execute__set_metadata(self, request=None): ''' @@ -112,6 +113,7 @@ class DstatPlugin(ZmqPlugin): `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): -- GitLab From b9632f2a409883fff9c4ce31dbd441863c763eeb Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 15:50:48 -0400 Subject: [PATCH 17/66] can load partial params files --- dstat_interface/params.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 0a33f88..73bface 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -70,12 +70,11 @@ 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]) + if 'experiment_index' in params: + window.expcombobox.set_active( + window.exp_window.classes[params['experiment_index']][0] + ) + window.exp_window.set_params(params['experiment_index'], params) window.analysis_opt_window.params = params window.db_window.params = params -- GitLab From b6e5e44a444ad9e4e5493210a916fcd9a7fe0482 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 15:51:52 -0400 Subject: [PATCH 18/66] can load partial params files-fixed --- dstat_interface/params.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 73bface..6dbc720 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -74,8 +74,8 @@ def set_params(window, params): window.expcombobox.set_active( window.exp_window.classes[params['experiment_index']][0] ) - - window.exp_window.set_params(params['experiment_index'], params) + window.exp_window.set_params(params['experiment_index'], params) + window.analysis_opt_window.params = params window.db_window.params = params -- GitLab From 3670cbee8d563c34c68201e02bbd3679e6f42cc4 Mon Sep 17 00:00:00 2001 From: Christian Fobel Date: Thu, 5 May 2016 16:23:33 -0400 Subject: [PATCH 19/66] Make metadata arg optional --- dstat_interface/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index 9fb6d2b..ce8e9f7 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -101,8 +101,9 @@ class DstatPlugin(ZmqPlugin): data = decode_content_data(request) self.parent.statusbar.push(self.parent.message_context_id, "µDrop " "acquisition requested.") - return self.parent.run_active_experiment(metadata=data['metadata']) - + return self.parent.run_active_experiment(metadata=data + .get('metadata')) + def on_execute__set_metadata(self, request=None): ''' Args -- GitLab From 8700c941676b948f75ade40b384582bc5cc928eb Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 16:34:09 -0400 Subject: [PATCH 20/66] Suppress parameter loading missing key errors. --- dstat_interface/analysis.py | 6 ++---- dstat_interface/interface/adc_pot.py | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index 33144c0..9526872 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/analysis.py @@ -85,11 +85,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/interface/adc_pot.py b/dstat_interface/interface/adc_pot.py index b223cd2..aafaacd 100755 --- a/dstat_interface/interface/adc_pot.py +++ b/dstat_interface/interface/adc_pot.py @@ -121,10 +121,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): -- GitLab From 1562aff67c86a8bad04ae23548750b3d135ee00b Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 16:34:47 -0400 Subject: [PATCH 21/66] Add param_override option to run_active_experiment. --- dstat_interface/main.py | 5 ++++- dstat_interface/plugin.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index e8ea118..3e89577 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -428,7 +428,7 @@ class Main(object): # Ignore expected exceptions when triggering experiment from UI. pass - def run_active_experiment(self, metadata=None): + def run_active_experiment(self, param_override=None, metadata=None): """Run currently visible experiment.""" # Assign current experiment a unique identifier. experiment_id = uuid.uuid4() @@ -509,6 +509,9 @@ class Main(object): parameters.update(self.analysis_opt_window.params) parameters.update(self.db_window.params) + if param_override is not None: + params.set_params(self, param_override) + self.line = 0 self.lastline = 0 self.lastdataline = 0 diff --git a/dstat_interface/plugin.py b/dstat_interface/plugin.py index ce8e9f7..59ec93d 100644 --- a/dstat_interface/plugin.py +++ b/dstat_interface/plugin.py @@ -101,8 +101,10 @@ class DstatPlugin(ZmqPlugin): data = decode_content_data(request) self.parent.statusbar.push(self.parent.message_context_id, "µDrop " "acquisition requested.") - return self.parent.run_active_experiment(metadata=data - .get('metadata')) + return self.parent.run_active_experiment( + param_override=data.get('params'), + metadata=data.get('metadata') + ) def on_execute__set_metadata(self, request=None): ''' -- GitLab From 17944559ba4c63e1556c5a3e502a5b1c7da90d9d Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 5 May 2016 17:52:24 -0400 Subject: [PATCH 22/66] Update changelog and readme. --- CHANGELOG | 7 ++++++- README.markdown | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 67c086b..678ec79 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -52,4 +52,9 @@ Version 1.1.3 Version 1.2 -Old Microdrop interface depreciated -New zmq_plugin based interface - -Internal changes to save functionality and plot storage. \ No newline at end of file + -Internal changes to save functionality and plot storage. + +Version 1.3 + -Fixed a bug related to calibration mode + -Added ZODB data storage + -Integrated with zmq_plugin \ No newline at end of file diff --git a/README.markdown b/README.markdown index 9e15e55..66544cc 100644 --- a/README.markdown +++ b/README.markdown @@ -16,6 +16,10 @@ It currently has no abilities for analyzing recorded data or opening previously 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. @@ -33,6 +37,9 @@ Python and related packages needed: (versions listed are tested, older versions * XQuartz (2.7.7) * zeromq (4.0.5) and pyzmq (14.6.0) * pyyaml (3.11) +* zodb +* zeo +* psutil Optional: -- GitLab From d46ac775d19e4accdc2f6d22023a703518904ede Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 10 May 2016 18:29:51 -0400 Subject: [PATCH 23/66] Fix parameter fetching for non PD experiments. --- dstat_interface/dstat_comm.py | 2 +- dstat_interface/main.py | 12 ++---------- dstat_interface/parameter_test.py | 29 ++++++++++++++--------------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 8cadd00..4b7d3bc 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -667,7 +667,7 @@ class PotExp(Experiment): self.datalength = 2 self.databytes = 8 self.xmin = 0 - self.xmax = self.parameters['time'] + self.xmax = int(self.parameters['time']) self.commands += "E" self.commands[2] += "P" diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 3e89577..a7e05d1 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -505,13 +505,11 @@ class Main(object): parameters['sync_true'] = False parameters['shutter_true'] = False try: - parameters.update(self.adc_pot.params) - parameters.update(self.analysis_opt_window.params) - parameters.update(self.db_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 @@ -541,7 +539,6 @@ class Main(object): 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) @@ -550,7 +547,6 @@ class Main(object): 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) @@ -559,7 +555,6 @@ class Main(object): 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) @@ -568,7 +563,6 @@ class Main(object): 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) @@ -577,7 +571,6 @@ class Main(object): 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) @@ -592,7 +585,6 @@ class Main(object): exceptions() return - parameters.update(self.exp_window.get_params('pot')) parameter_test.pot_test(parameters) self.current_exp = comm.PotExp(parameters) diff --git a/dstat_interface/parameter_test.py b/dstat_interface/parameter_test.py index 5600a8d..ec62943 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'], -- GitLab From b97c0d5e2fc27a617e9bd06de4d096c8e6968f13 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 10 May 2016 18:37:52 -0400 Subject: [PATCH 24/66] Make metadata keys optional. --- dstat_interface/interface/db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/interface/db.py b/dstat_interface/interface/db.py index 7cedf24..2306b9e 100755 --- a/dstat_interface/interface/db.py +++ b/dstat_interface/interface/db.py @@ -111,8 +111,8 @@ class DB_Window(gobject.GObject): 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} + params = {k : metadata[j] for j, k in self.metadata_map + if j in metadata} self.params = params def show(self): -- GitLab From aacc2560644fb786838b0627148ff179fbacbbb4 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 10 May 2016 18:38:46 -0400 Subject: [PATCH 25/66] Update changelog. --- CHANGELOG | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 678ec79..bf6aa4b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -57,4 +57,8 @@ Version 1.2 Version 1.3 -Fixed a bug related to calibration mode -Added ZODB data storage - -Integrated with zmq_plugin \ No newline at end of file + -Integrated with zmq_plugin + +Version 1.3.1 + -Fixed electrochem modes broken when database added + -Make metadata keys optional. \ No newline at end of file -- GitLab From cafc97b7ccd6e7a510546e7dfe87dddd32a0ec55 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 13 May 2016 17:16:34 -0400 Subject: [PATCH 26/66] Update README.markdown with better Windows pip instructions. --- README.markdown | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index 66544cc..a0157f9 100644 --- a/README.markdown +++ b/README.markdown @@ -9,7 +9,8 @@ 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 @@ -23,7 +24,7 @@ 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) @@ -71,13 +72,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 -- GitLab From 89ced57a40e839ea23e8ea098f600bdf7d2effb1 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 20 Jul 2016 19:00:05 -0400 Subject: [PATCH 27/66] Improve connection reliability. --- dstat_interface/dstat_comm.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 4b7d3bc..a4e98ff 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -103,6 +103,13 @@ class VersionCheck: ser_port -- address of serial port to use """ try: + ser.flushInput() + ser.write('!') + + while not ser.read()=="C": + ser.flushInput() + ser.write('!') + ser.write('V') for line in ser: if line.startswith('V'): @@ -184,7 +191,7 @@ class Settings: self.ser.write('!') while not self.ser.read()=="C": - time.sleep(.5) + self.ser.flushInput() self.ser.write('!') self.ser.write('SR') @@ -210,7 +217,7 @@ class Settings: self.ser.write('!') while not self.ser.read()=="C": - time.sleep(.5) + self.ser.flushInput() self.ser.write('!') write_buffer = range(len(self.settings)) @@ -269,9 +276,8 @@ class LightSensor: ser.write('!') while not ser.read()=="C": - time.sleep(.5) + self.ser.flushInput() ser.write('!') - ser.write('T') for line in ser: @@ -396,10 +402,12 @@ class Experiment(object): for i in self.commands: logger.info("Command: %s", i) + self.serial.flushInput() self.serial.write('!') - while not self.serial.read().startswith("C"): - pass + while not self.serial.read()=="C": + self.serial.flushInput() + self.serial.write('!') self.serial.write(i) if not self.serial_handler(): -- GitLab From bc324a1d362daa579fbeae158f5028b5a9ec1091 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 20 Jul 2016 19:01:17 -0400 Subject: [PATCH 28/66] Update changelog. --- CHANGELOG | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bf6aa4b..f90415d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -61,4 +61,7 @@ Version 1.3 Version 1.3.1 -Fixed electrochem modes broken when database added - -Make metadata keys optional. \ No newline at end of file + -Make metadata keys optional. + +Version 1.3.2 + -Improves initial connection reliability \ No newline at end of file -- GitLab From 0db91a29a6bcca2fecc138803f377d3aa22401f9 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 19 Oct 2016 19:44:52 -0400 Subject: [PATCH 29/66] Mostly working --- dstat_interface/interface/acv.glade | 360 +++++++++--------- dstat_interface/interface/adc_pot.glade | 58 ++- dstat_interface/interface/adc_pot.py | 12 +- .../interface/analysis_options.glade | 66 ++-- dstat_interface/interface/calib.glade | 182 +++------ dstat_interface/interface/chronoamp.glade | 52 ++- dstat_interface/interface/cv.glade | 290 ++++++-------- dstat_interface/interface/db.glade | 73 ++-- dstat_interface/interface/db.py | 23 +- dstat_interface/interface/dpv.glade | 241 ++++-------- .../interface/dstatinterface.glade | 277 +++++++------- dstat_interface/interface/exp_int.py | 38 +- dstat_interface/interface/lsv.glade | 101 ++--- dstat_interface/interface/pd.glade | 156 +++----- dstat_interface/interface/potexp.glade | 31 +- dstat_interface/interface/save.py | 64 ++-- dstat_interface/interface/swv.glade | 260 +++++-------- dstat_interface/main.py | 54 +-- dstat_interface/plot.py | 30 +- 19 files changed, 968 insertions(+), 1400 deletions(-) diff --git a/dstat_interface/interface/acv.glade b/dstat_interface/interface/acv.glade index 3f7b376..d664e83 100644 --- a/dstat_interface/interface/acv.glade +++ b/dstat_interface/interface/acv.glade @@ -1,190 +1,181 @@ + - - + 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 6cb4371..fcb0d6e 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 aafaacd..28619d1 100755 --- a/dstat_interface/interface/adc_pot.py +++ b/dstat_interface/interface/adc_pot.py @@ -18,7 +18,13 @@ # 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") @@ -44,10 +50,10 @@ v1_2_gain = [(0, "Bypass", 0), 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', diff --git a/dstat_interface/interface/analysis_options.glade b/dstat_interface/interface/analysis_options.glade index be8262f..dda2e6c 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 2c72437..ff4b40f 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 b3ceb06..e0a22de 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 ab547ac..c65e4bc 100644 --- a/dstat_interface/interface/cv.glade +++ b/dstat_interface/interface/cv.glade @@ -1,190 +1,177 @@ + - - + 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 + True - - True - False - Potential (mV) - - - 1 - 2 - GTK_FILL - - - - + True False - Time (s) + Start (mV) - 2 - 3 - GTK_FILL + 0 + 0 - + True False + Vertex 1 (mV) - GTK_FILL + 0 + 1 - + True False - Cleaning + Vertex 2 (mV) - 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 + Slope (mV/s) + + + 0 + 3 + + + + + True + False + Scans + + + 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 +179,10 @@ - + True False - Preconditioning + Experiment True @@ -203,205 +190,146 @@ False False - 2 + 1 0 - + True False 0 out - + True False 5 5 5 - + True False - 5 - 2 - 10 - True + True + True - + True False - Start (mV) + False + Potential (mV) - GTK_FILL + 1 + 0 - + True False - Vertex 1 (mV) + Time (s) - 1 - 2 - GTK_FILL + 2 + 0 - + True False - Vertex 2 (mV) - 2 - 3 - GTK_FILL + 0 + 0 - + True - True - - 8 - 0 - 1 - True - False - False - True - True + False + Cleaning - 1 - 2 - - GTK_FILL - 3 + 0 + 1 - + + True + False + Deposition + + + 0 + 2 + + + + True True 8 0 1 - True False False - True - True 1 - 2 1 - 2 - - GTK_FILL - 3 - + True True 8 0 1 - True False False - True - True 1 - 2 2 - 3 - - GTK_FILL - 3 - - True - False - Slope (mV/s) - - - 3 - 4 - GTK_FILL - - - - - True - False - Scans - - - 4 - 5 - GTK_FILL - - - - + True True 8 0 1 - True False False - True - True - 1 - 2 - 3 - 4 - - GTK_FILL - 3 + 2 + 2 - + True True 8 0 1 - True False False - True - True - 1 - 2 - 4 - 5 - - GTK_FILL - 3 + 2 + 1 @@ -409,10 +337,10 @@ - + True False - Experiment + Preconditioning True diff --git a/dstat_interface/interface/db.glade b/dstat_interface/interface/db.glade index 20a833a..e65d520 100644 --- a/dstat_interface/interface/db.glade +++ b/dstat_interface/interface/db.glade @@ -1,16 +1,17 @@ + - - + False Database center-on-parent True - + True False + vertical Enable Database saving @@ -28,17 +29,11 @@ - + True False - 5 - 3 - - - - - - + True + True True @@ -46,8 +41,8 @@ Measurement ID - 1 - 2 + 0 + 0 @@ -56,6 +51,10 @@ False Experiment ID + + 0 + 1 + @@ -64,12 +63,10 @@ False False - True - True 1 - 2 + 1 @@ -80,14 +77,10 @@ False False - True - True 1 - 2 - 1 - 2 + 0 @@ -98,14 +91,10 @@ False False - True - True 1 - 2 3 - 4 @@ -115,8 +104,8 @@ Patient ID + 0 3 - 4 @@ -129,7 +118,7 @@ 2 - 3 + 1 @@ -139,8 +128,8 @@ DB Path + 0 4 - 5 @@ -150,14 +139,10 @@ False False - True - True 1 - 2 4 - 5 @@ -170,14 +155,9 @@ 2 - 3 4 - 5 - - - True @@ -185,8 +165,8 @@ Measurement Name + 0 2 - 3 @@ -196,19 +176,24 @@ False False - True - True 1 - 2 2 - 3 + + + + + + + + + - True + False True 1 diff --git a/dstat_interface/interface/db.py b/dstat_interface/interface/db.py index 2306b9e..cc31db7 100755 --- a/dstat_interface/interface/db.py +++ b/dstat_interface/interface/db.py @@ -22,22 +22,27 @@ import sys import logging from uuid import uuid4 -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) logger = logging.getLogger('dstat.interface.db') -class DB_Window(gobject.GObject): +class DB_Window(GObject.GObject): __gsignals__ = { - 'db-reset' : (gobject.SIGNAL_RUN_LAST, - gobject.TYPE_NONE, - (gobject.TYPE_STRING,) + 'db-reset' : (GObject.SIGNAL_RUN_LAST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,) ) } def __init__(self): - gobject.GObject.__init__(self) + GObject.GObject.__init__(self) - self.builder = gtk.Builder() + self.builder = Gtk.Builder() self.builder.add_from_file('interface/db.glade') self.builder.connect_signals(self) @@ -135,4 +140,4 @@ class DB_Window(gobject.GObject): widget.hide() return True -gobject.type_register(DB_Window) \ No newline at end of file +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 e13b41d..9143f88 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 6503660..e134224 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -1,7 +1,7 @@ + - - + @@ -693,13 +693,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 +723,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 @@ -754,6 +751,11 @@ Thanks to Christian Fobel for help with Dropbot Plugin False gtk-info + + True + False + gtk-save-as + @@ -768,9 +770,10 @@ Thanks to Christian Fobel for help with Dropbot Plugin 800 - + True False + vertical True @@ -779,25 +782,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 @@ -807,7 +809,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - Save Parameters… + Save Parameters… True False image4 @@ -817,7 +819,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - Load Parameters… + Load Parameters… True False image5 @@ -844,7 +846,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False Dropbot - True True @@ -885,7 +886,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin False - Analysis options… + Analysis Options… True False image6 @@ -908,7 +909,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin False - View… + Database Options True False image7 @@ -923,8 +924,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False - _Help - True + About True @@ -951,31 +951,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 @@ -989,8 +992,8 @@ Thanks to Christian Fobel for help with Dropbot Plugin - True - True + False + False 10 1 @@ -1003,22 +1006,23 @@ Thanks to Christian Fobel for help with Dropbot Plugin - True - True + False + False 2 False - True + False 0 - + True False + vertical @@ -1030,7 +1034,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False @@ -1079,7 +1083,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin False False - 5 2 @@ -1095,79 +1098,20 @@ Thanks to Christian Fobel for help with Dropbot Plugin True True - + True False - - - True - False - - - Autosave - True - True - False - True - - - False - True - 0 - - - - - True - False - select-folder - False - Select a Save Folder - - - True - True - 1 - - - - - True - True - True - 32 - - filename - False - gtk-file - False - False - True - True - File name - - - False - True - 2 - - - - - False - True - 0 - - + vertical True True bottom - + True False + vertical @@ -1177,7 +1121,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin True False - 0.40999999642372131 Main @@ -1185,7 +1128,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False @@ -1211,7 +1154,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False @@ -1240,7 +1183,70 @@ Thanks to Christian Fobel for help with Dropbot Plugin True True - 1 + -1 + + + + + True + False + start + + + Autosave + True + True + False + True + + + False + True + 0 + + + + + True + False + start + select-folder + False + Select a Save Folder + + + True + True + 1 + + + + + True + True + True + start + 32 + + filename + False + gtk-file + False + False + File name + File name + + + False + True + 2 + + + + + False + False + 0 @@ -1259,8 +1265,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin True True - automatic - automatic True @@ -1291,8 +1295,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin True True - automatic - automatic True @@ -1333,42 +1335,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 @@ -1421,13 +1423,13 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + + gtk-refresh True - False - 0 - 5 - OCP: - True + True + True + True + False @@ -1436,21 +1438,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 4c477a7..44ff614 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -20,8 +20,13 @@ import os import sys -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 __main__ @@ -33,7 +38,7 @@ class ExpInterface(object): experiment interfaces by populating self.entry. """ def __init__(self, glade_path): - self.builder = gtk.Builder() + 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 @@ -87,15 +92,16 @@ class Chronoamp(ExpInterface): 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 +137,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())) @@ -321,7 +327,7 @@ class PD(ExpInterface): __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() @@ -340,7 +346,7 @@ class PD(ExpInterface): __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(): @@ -405,7 +411,7 @@ class CAL(ExpInterface): __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: @@ -427,7 +433,7 @@ class CAL(ExpInterface): __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): @@ -464,11 +470,11 @@ class CAL(ExpInterface): __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/lsv.glade b/dstat_interface/interface/lsv.glade index 63c4468..404e1e4 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 bb72ff8..c39930e 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/potexp.glade b/dstat_interface/interface/potexp.glade index 4d8d915..d4ec1e6 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 675bcd1..982dcee 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -21,7 +21,13 @@ 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,11 +37,11 @@ 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)) + 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 = [Gtk.FileFilter()] filters[0].set_name("Space separated text (.txt)") filters[0].add_pattern("*.txt") @@ -45,7 +51,7 @@ def manSave(current_exp): response = fcd.run() - if response == gtk.RESPONSE_OK: + if response == Gtk.RESPONSE_OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() @@ -55,19 +61,19 @@ def manSave(current_exp): fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.RESPONSE_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.FILE_CHOOSER_ACTION_SAVE, + (Gtk.STOCK_CANCEL, Gtk.RESPONSE_CANCEL, + Gtk.STOCK_SAVE, Gtk.RESPONSE_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,7 +83,7 @@ def plot_save_dialog(plots): response = fcd.run() - if response == gtk.RESPONSE_OK: + if response == Gtk.RESPONSE_OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() @@ -94,19 +100,19 @@ def plot_save_dialog(plots): fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.RESPONSE_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.FILE_CHOOSER_ACTION_SAVE, + (Gtk.STOCK_CANCEL, Gtk.RESPONSE_CANCEL, + Gtk.STOCK_SAVE, Gtk.RESPONSE_OK) ) - filters = [gtk.FileFilter()] + filters = [Gtk.FileFilter()] filters[0].set_name("Parameter File (.yml)") filters[0].add_pattern("*.yml") @@ -116,7 +122,7 @@ def man_param_save(window): response = fcd.run() - if response == gtk.RESPONSE_OK: + if response == Gtk.RESPONSE_OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) @@ -127,18 +133,18 @@ def man_param_save(window): fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.RESPONSE_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.FILE_CHOOSER_ACTION_OPEN, + (Gtk.STOCK_CANCEL, Gtk.RESPONSE_CANCEL, + Gtk.STOCK_OPEN, Gtk.RESPONSE_OK) ) - filters = [gtk.FileFilter()] + filters = [Gtk.FileFilter()] filters[0].set_name("Parameter File (.yml)") filters[0].add_pattern("*.yml") @@ -147,7 +153,7 @@ def man_param_load(window): response = fcd.run() - if response == gtk.RESPONSE_OK: + if response == Gtk.RESPONSE_OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) @@ -155,7 +161,7 @@ def man_param_load(window): fcd.destroy() - elif response == gtk.RESPONSE_CANCEL: + elif response == Gtk.RESPONSE_CANCEL: fcd.destroy() def autoSave(exp, path, name): diff --git a/dstat_interface/interface/swv.glade b/dstat_interface/interface/swv.glade index 62659c1..0a1e326 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 a7e05d1..9c0e03b 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]))) @@ -80,10 +78,10 @@ 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') @@ -210,7 +208,7 @@ class Main(object): self.on_serial_disconnect_clicked() db.stop_db() - gtk.main_quit() + Gtk.main_quit() def on_gtk_about_activate(self, menuitem, data=None): """Display the about window.""" @@ -328,8 +326,8 @@ class Main(object): logger.info("Start OCP") comm.serial_instance.proc_pipe_p.send(comm.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 @@ -348,7 +346,7 @@ class Main(object): comm.serial_instance.ctrl_pipe_p.send('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 @@ -483,11 +481,11 @@ class Main(object): while comm.serial_instance.data_pipe_p.poll(): comm.serial_instance.data_pipe_p.recv() - self.plot_proc = gobject.timeout_add(200, + 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) + GObject.idle_add(self.experiment_running_data), + GObject.idle_add(self.experiment_running_proc) ) self.stop_ocp() @@ -605,6 +603,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.") @@ -723,8 +723,8 @@ class Main(object): """ try: self.current_exp.time = datetime.now() - gobject.source_remove(self.experiment_proc[0]) - gobject.source_remove(self.plot_proc) # stop automatic plot update + 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("") @@ -922,18 +922,18 @@ 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() + Gtk.main() diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 7c5c017..1ce5a87 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: @@ -110,12 +113,13 @@ class PlotBox(object): useOffset=False, axis='y') self.canvas = FigureCanvas(self.figure) - self.win = gtk.Window() - self.vbox = gtk.VBox() + self.canvas.set_vexpand(True) + self.win = Gtk.Window() + self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.win.add(self.vbox) - self.vbox.pack_start(self.canvas) + self.vbox.pack_start(self.canvas, expand=True, fill=True, padding=0) self.toolbar = NavigationToolbar(self.canvas, self.win) - self.vbox.pack_start(self.toolbar, False, False) + self.vbox.pack_start(self.toolbar, expand=False, fill=False, padding=0) self.vbox.reparent(plotwindow_instance) def clearall(self): -- GitLab From 1933d3eb1dcc43f0db671e8a59937eb8e2c05d69 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Thu, 20 Oct 2016 13:16:24 -0400 Subject: [PATCH 30/66] Fix gain_liststore type mismatch. --- dstat_interface/interface/adc_pot.py | 34 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/dstat_interface/interface/adc_pot.py b/dstat_interface/interface/adc_pot.py index 28619d1..1b236e1 100755 --- a/dstat_interface/interface/adc_pot.py +++ b/dstat_interface/interface/adc_pot.py @@ -29,23 +29,23 @@ except ImportError: from errors import InputError, VarError, ErrorLogger _logger = ErrorLogger(sender="dstat_adc_pot") -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): @@ -142,7 +142,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) -- GitLab From ae517c81b4c910cdad4ced1f25c44111ea6b4586 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Thu, 20 Oct 2016 13:19:00 -0400 Subject: [PATCH 31/66] Fixed KeyboardInterrupt not working. --- dstat_interface/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 9c0e03b..bca4a1e 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -208,7 +208,7 @@ class Main(object): self.on_serial_disconnect_clicked() db.stop_db() - Gtk.main_quit() + mainloop.quit() def on_gtk_about_activate(self, menuitem, data=None): """Display the about window.""" @@ -936,4 +936,9 @@ if __name__ == "__main__": multiprocessing.freeze_support() GObject.threads_init() MAIN = Main() - Gtk.main() + mainloop = GObject.MainLoop() + try: + mainloop.run() + except KeyboardInterrupt: + logger.info('Ctrl+C hit, quitting') + MAIN.quit() -- GitLab From 3ca3afeb930d913fa850bcac8c35e94c1ec606b1 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 25 Oct 2016 15:45:35 -0400 Subject: [PATCH 32/66] Send udc reattach command before connecting. --- dstat_interface/dstat_comm.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index a4e98ff..0a366f7 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -33,11 +33,17 @@ exp_logger = logging.getLogger("dstat.comm.Experiment") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger = logging.getLogger("dstat.comm._serial_process") - ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) + ser = serial.Serial(ser_port, baudrate=1000000, timeout=1) + ser_logger.info("Reattaching DStat udc") + ser.write("!R") # Send restart command + ser.close() + time.sleep(1) # Give OS time to enumerate + + ser = serial.Serial(ser_port, baudrate=1000000, timeout=1) ser_logger.info("Connecting") - ser.write("ck") + ser.write("ck") # Keep this to support old firmwares ser.flushInput() ser.write('!') -- GitLab From e9330d16cc7532e57bfbeb6554fe694da80f1f60 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 9 Nov 2016 17:23:17 -0500 Subject: [PATCH 33/66] Fix reconnection code. --- dstat_interface/dstat_comm.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 0a366f7..ac58436 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -33,15 +33,20 @@ exp_logger = logging.getLogger("dstat.comm.Experiment") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger = logging.getLogger("dstat.comm._serial_process") - ser = serial.Serial(ser_port, baudrate=1000000, timeout=1) + ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) ser_logger.info("Reattaching DStat udc") ser.write("!R") # Send restart command ser.close() - time.sleep(1) # Give OS time to enumerate + for i in range(5): + time.sleep(1) # Give OS time to enumerate - ser = serial.Serial(ser_port, baudrate=1000000, timeout=1) - ser_logger.info("Connecting") + try: + ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) + ser_logger.info("Connecting") + break + except serial.SerialException: + pass ser.write("ck") # Keep this to support old firmwares @@ -53,6 +58,7 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): time.sleep(.5) ser.write('!') else: + ser.write('V') break while True: -- GitLab From 493102d1c9b68fa56b7f4e6652f875d565ceb56f Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Sat, 11 Mar 2017 13:51:19 -0500 Subject: [PATCH 34/66] Separate experiments into their own files. CA, LSV, and CV tested and working --- dstat_interface/dstat_comm.py | 580 +----------------- dstat_interface/experiments/__init__.py | 14 + dstat_interface/experiments/cal.py | 133 ++++ dstat_interface/experiments/chronoamp.py | 140 +++++ dstat_interface/experiments/cv.py | 43 ++ .../experiments/experiment_template.py | 356 +++++++++++ dstat_interface/experiments/idle.py | 36 ++ dstat_interface/experiments/lsv.py | 38 ++ dstat_interface/experiments/pot.py | 54 ++ dstat_interface/experiments/swv.py | 139 +++++ dstat_interface/main.py | 78 +-- dstat_interface/plot.py | 112 ---- 12 files changed, 998 insertions(+), 725 deletions(-) create mode 100644 dstat_interface/experiments/__init__.py create mode 100755 dstat_interface/experiments/cal.py create mode 100644 dstat_interface/experiments/chronoamp.py create mode 100644 dstat_interface/experiments/cv.py create mode 100755 dstat_interface/experiments/experiment_template.py create mode 100644 dstat_interface/experiments/idle.py create mode 100644 dstat_interface/experiments/lsv.py create mode 100644 dstat_interface/experiments/pot.py create mode 100644 dstat_interface/experiments/swv.py diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index ac58436..cc81ce3 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -30,6 +30,9 @@ logger = logging.getLogger("dstat.comm") dstat_logger = logging.getLogger("dstat.comm.DSTAT") exp_logger = logging.getLogger("dstat.comm.Experiment") +serial_instance = None +settings = {} + def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger = logging.getLogger("dstat.comm._serial_process") @@ -344,579 +347,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.flushInput() - self.serial.write('!') - - while not self.serial.read()=="C": - self.serial.flushInput() - self.serial.write('!') - - 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") + +from dstat_comm import serial_instance +from experiments.experiment_template import Experiment + +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 + serial_instance.proc_pipe_p.send(CALExp(parameters)) + logger.info("measure_offset: %s", serial_instance.proc_pipe_p.recv()) + gain_offset[gain_trim_table[i]] = serial_instance.data_pipe_p.recv() + + return gain_offset + +class CALExp(Experiment): + """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("no"): + 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 0000000..284f5e2 --- /dev/null +++ b/dstat_interface/experiments/chronoamp.py @@ -0,0 +1,140 @@ +import time +import struct + +from experiments.experiment_template import PlotBox, Experiment + +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): + """Chronoamperometry experiment""" + def setup(self): + self.plotbox = ChronoampBox('current_time') + + self.datatype = "linearData" + self.datalength = 2 + self.databytes = 8 + self.data = {'current_time' : [([],[])]} + self.plot_format = { + 'current_time' : { + 'labels' : ('Time (s)','Current (A)'), + 'xlims' : (0, sum(self.parameters['time'])) + } + } + + self.commands += "E" + self.commands[2] += "R" + str(len(self.parameters['potential'])) + " " + for i in self.parameters['potential']: + self.commands[2] += str(int(i*(65536./3000)+32768)) + " " + for i in self.parameters['time']: + self.commands[2] += str(i) + " " + 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(' 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 0000000..c14edd8 --- /dev/null +++ b/dstat_interface/experiments/cv.py @@ -0,0 +1,43 @@ +import time +import struct + +from experiments.experiment_template import PlotBox, Experiment + +class CVExp(Experiment): + """Cyclic Voltammetry experiment""" + def __init__(self, parameters): + super(CVExp, self).__init__(parameters) + self.plotbox = PlotBox() + + 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 0000000..9a7077d --- /dev/null +++ b/dstat_interface/experiments/experiment_template.py @@ -0,0 +1,356 @@ +#!/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 copy import deepcopy +from math import ceil + +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 +import matplotlib.gridspec as gridspec +import matplotlib.pyplot as plt + +from matplotlib.backends.backend_gtk3agg \ + import FigureCanvasGTK3Agg as FigureCanvas +from matplotlib.backends.backend_gtk3 \ + import NavigationToolbar2GTK3 as NavigationToolbar + +try: + import seaborn as sns +except InputError: + 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 dstat_comm + +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. + """ + + 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 = 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( + dstat_comm.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() + + def setup(self): + self.data = {'current_voltage' : [([],[])]} + self.plot_format = { + 'current_voltage' : {'labels' : ('Voltage (mV)', + 'Current (A)' + ), + 'xlims' : (0, 1) + } + } + # list of scans, tuple of dimensions, list of data + self.line_data = ([], []) + + self.plotbox = PlotBox(['current_voltage']) + + 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.flushInput() + self.serial.write('!') + + while not self.serial.read()=="C": + self.serial.flushInput() + self.serial.write('!') + + self.serial.write(i) + if not self.serial_handler(): + status = "ABORT" + + 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))) + 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("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(' 1): + gs = gridspec.GridSpec(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') + + for subplot in self.subplots.values(): + 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 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']) + + Experiment.plotbox = self + + 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 0000000..662fe68 --- /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.""" + def __init__(self): + self.databytes = 8 + + self.commands = ["EA", "EP"] + + self.commands[0] += "2 " # input buffer + self.commands[0] += "3 " # 2.5 Hz sample rate + self.commands[0] += "1 " # 2x PGA + + self.commands[1] += "0 " # no timeout + self.commands[1] += "0 " # OCP measurement mode + + def data_handler(self, data_input): + """Overrides Experiment method to only send ADC values.""" + scan, data = data_input + # 2*uint16 + int32 + seconds, milliseconds, voltage = struct.unpack(' self.lastdataline: - self.current_exp.data['data'].append( - deepcopy(self.current_exp.line_data)) + newline = True 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]) - + else: + newline = False + + self.current_exp.store_data(incoming, newline) + if comm.serial_instance.data_pipe_p.poll(): self.experiment_running_data() return True @@ -709,12 +713,12 @@ class Main(object): removed from GTK's queue. """ if self.line > self.lastline: - self.plot.addline() + self.current_exp.plotbox.addline() # make sure all of last line is added - self.plot.updateline(self.current_exp, self.lastline) + self.current_exp.plotbox.updateline(self.current_exp, self.lastline) self.lastline = self.line - self.plot.updateline(self.current_exp, self.line) - self.plot.redraw() + self.current_exp.plotbox.updateline(self.current_exp, self.line) + self.current_exp.plotbox.redraw() return True def experiment_done(self): @@ -786,19 +790,19 @@ class Main(object): 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) + # 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(): diff --git a/dstat_interface/plot.py b/dstat_interface/plot.py index 1ce5a87..7e87c41 100755 --- a/dstat_interface/plot.py +++ b/dstat_interface/plot.py @@ -91,116 +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.canvas.set_vexpand(True) - self.win = Gtk.Window() - self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.win.add(self.vbox) - self.vbox.pack_start(self.canvas, expand=True, fill=True, padding=0) - self.toolbar = NavigationToolbar(self.canvas, self.win) - self.vbox.pack_start(self.toolbar, expand=False, fill=False, padding=0) - 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() - - -- GitLab From bae7254ff3214305a1d6d352bfd389f195986546 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 14 Mar 2017 19:20:19 -0400 Subject: [PATCH 35/66] Remove ZODB support until newest version can be integrated properly. Fixes #24 --- .gitmodules | 9 - dstat_interface/db.py | 194 ---------------- dstat_interface/interface/db.glade | 219 ------------------ .../interface/dstatinterface.glade | 27 --- dstat_interface/main.py | 52 +---- dstat_interface/params.py | 4 +- pavement.py | 3 +- 7 files changed, 4 insertions(+), 504 deletions(-) delete mode 100644 dstat_interface/db.py delete mode 100644 dstat_interface/interface/db.glade diff --git a/.gitmodules b/.gitmodules index a95ffcb..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +0,0 @@ -[submodule "mr_db"] - path = mr_db - url = /Users/mdryden/src/mr_db -[submodule "dstat-interface/mr_db"] - path = dstat-interface/mr_db - url = http://microfluidics.utoronto.ca/gitlab/mdryden/mr-db.git -[submodule "dstat_interface/mr_db"] - path = dstat_interface/mr_db - url = http://microfluidics.utoronto.ca/gitlab/mdryden/mr-db.git diff --git a/dstat_interface/db.py b/dstat_interface/db.py deleted file mode 100644 index 3e1fda8..0000000 --- a/dstat_interface/db.py +++ /dev/null @@ -1,194 +0,0 @@ -import logging -from time import sleep, time -from os.path import expanduser -from uuid import uuid4 - -from BTrees.OOBTree import OOBTree - -import mr_db.mr_db as db - -logger = logging.getLogger("dstat.db") - -current_db = None - -def start_db(path=None): - global current_db - current_db = Database(data_dir=path) - -def restart_db(object, path): - logger.info("Restarting database") - global current_db - if current_db is None: - logger.info("No database running") - start_db(path=path) - else: - stop_db() - start_db(path=path) - -def stop_db(): - global current_db - if not current_db is None: - logger.info("Stopping ZEO") - current_db.disconnect() - current_db = None - db.stop_server() - else: - logger.warning("Tried to disconnect ZEO when not connected") - -class Database(object): - def __init__(self, name='dstat', data_dir=None): - if data_dir is None: - data_dir = expanduser('~/.mr_db') - self.connected = False - self.name = name - - self.db_connect(data_dir) - - self.db = self.connection.databases - - # Make sure database exists - if not self.db.has_key(name): - self.db[name] = OOBTree() - db.transaction.commit() - - def disconnect(self): - if self.connected is True: - self.connection.db.close() - else: - logger.war("Tried to disconnect ZEO when not connected") - - def db_connect(self, root_dir): - """Connects to ZEO process. Starts ZEO if not running. Returns - connection object. - """ - if root_dir == '': - root_dir = None - - while self.connected is False: - try: - self.connection = db.DbConnection(root_dir=root_dir) - self.connected = True - logger.info("Connected to ZEO server") - except db.ClientStorage.ClientDisconnected: - db.stop_server() - logger.info("Starting ZEO server -- root_dir = %s", root_dir) - db_proc = db.start_server(root_dir=root_dir) - sleep(3) - - def add_results(self, measurement_uuid=None, measurement_name=None, - experiment_uuid=None, experiment_metadata=None, - patient_id=None, - timestamp=None, - data=None): - - """Add a measurement""" - - if experiment_metadata is None: - experiment_metadata = {} - - try: - logger.info("Starting DB transaction") - db.transaction.begin() - - logger.info("Creating Experiment with id: %s", experiment_uuid) - exp_db, exp_id = self.add_experiment( - experiment_uuid=experiment_uuid, - timestamp=timestamp, - **experiment_metadata) - - logger.info("Adding Measurement with id: %s", measurement_uuid) - name = self.add_dstat_measurement(experiment=exp_db[exp_id], - measurement_uuid=measurement_uuid, - name=measurement_name, - timestamp=timestamp, - data=data) - - if patient_id is not None: - if not patient_id in self.db['patients']: - logger.info("Creating patient with id: %s", patient_id) - patient = db.Patient(pid=patient_id) - self.db['patients'][patient_id] = patient - - if not exp_id in self.db['patients'][patient_id].experiments: - logger.info("Linking experiment into patient with id: %s", - patient_id) - self.db['patients'][patient_id].link_experiment(exp_db, - exp_id) - - logger.info("Committing DB transaction") - db.transaction.commit() - - return name - - except: - logger.error("Aborting DB transaction") - db.transaction.abort() - raise - - def add_experiment(self, experiment_uuid=None, timestamp=None, **kwargs): - """Add a new experiment. Will raise KeyExistsError if id is already - in db to avoid unintended collisions. - ---- - Arguments: - id: experiment id---UUID will be generated if not supplied - timestamp: current time---Will be generated if not supplied - kwargs: additional keyword arguments that will be saved in experiment. - """ - - if experiment_uuid is None: - experiment_uuid = uuid4().hex - if timestamp is None: - timestamp = time() - - kwargs.update({'id':experiment_uuid, 'timestamp':timestamp}) - - if not experiment_uuid in self.db[self.name]: - self.db[self.name][experiment_uuid] = db.PersistentMapping() - else: - logger.info("Experiment already exists, appending") - - self.db[self.name][experiment_uuid].update(kwargs) - - return (self.db[self.name], experiment_uuid) - - def add_dstat_measurement(self, experiment, measurement_uuid=None, - data=None, timestamp=None, name=None): - - if measurement_uuid is None: - measurement_uuid = uuid4().hex - if timestamp is None: - timestamp = time() - if data is None: - data = {} - - if not 'measurements' in experiment: - experiment['measurements'] = db.PersistentMapping() - if not 'measurements_by_name' in experiment: - experiment['measurements_by_name'] = db.PersistentMapping() - - if measurement_uuid in experiment['measurements']: - raise db.KeyExistsError(measurement_uuid, - "Measurement ID already exists. Access directly to update") - - data['timestamp'] = timestamp - data['id'] = measurement_uuid - - if name is not None: - data['name'] = name - - experiment['measurements'][measurement_uuid] = data - - if name is not None: - while name in experiment['measurements_by_name']: - first, sep, last = name.rpartition('-') - - if last.isdigit(): - index = int(last) - index += 1 - name = sep.join((first,str(index))) - else: - name += '-1' - - experiment['measurements_by_name'][name] = data - - return name \ No newline at end of file diff --git a/dstat_interface/interface/db.glade b/dstat_interface/interface/db.glade deleted file mode 100644 index 20a833a..0000000 --- a/dstat_interface/interface/db.glade +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - False - Database - center-on-parent - True - - - True - False - - - Enable Database saving - True - True - False - True - True - - - - False - False - 0 - - - - - True - False - 5 - 3 - - - - - - - - - True - False - Measurement ID - - - 1 - 2 - - - - - True - False - Experiment ID - - - - - True - True - - False - False - True - True - - - 1 - 2 - - - - - True - True - False - - False - False - True - True - - - 1 - 2 - 1 - 2 - - - - - True - True - False - - False - False - True - True - - - 1 - 2 - 3 - 4 - - - - - True - False - Patient ID - - - 3 - 4 - - - - - Generate New - True - True - True - - - - 2 - 3 - - - - - True - False - DB Path - - - 4 - 5 - - - - - True - True - - False - False - True - True - - - 1 - 2 - 4 - 5 - - - - - Apply - True - True - True - - - - 2 - 3 - 4 - 5 - - - - - - - - True - False - Measurement Name - - - 2 - 3 - - - - - True - True - - False - False - True - True - - - 1 - 2 - 2 - 3 - - - - - True - True - 1 - - - - - - diff --git a/dstat_interface/interface/dstatinterface.glade b/dstat_interface/interface/dstatinterface.glade index 6503660..8ae7a00 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -749,11 +749,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin False gtk-preferences - - True - False - gtk-info - @@ -897,28 +892,6 @@ Thanks to Christian Fobel for help with Dropbot Plugin - - - True - False - Database - - - True - False - - - View… - True - False - image7 - False - - - - - - True diff --git a/dstat_interface/main.py b/dstat_interface/main.py index a7e05d1..6b72172 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -50,7 +50,6 @@ os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) from version import getVersion import interface.save as save -from interface.db import DB_Window import dstat_comm as comm import interface.exp_window as exp_window import interface.adc_pot as adc_pot @@ -59,7 +58,6 @@ import params import parameter_test import analysis import zmq -import db from errors import InputError from plugin import DstatPlugin, get_hub_uri @@ -105,12 +103,6 @@ class Main(object): self.exp_window = exp_window.Experiments(self.builder) self.analysis_opt_window = analysis.AnalysisOptions(self.builder) - - self.db_window = DB_Window() - self.builder.get_object('menu_database_options').connect_object( - 'activate', DB_Window.show, self.db_window - ) - self.db_window.connect('db_reset', db.restart_db) # Setup Autosave self.autosave_checkbox = self.builder.get_object('autosave_checkbutton') @@ -209,7 +201,6 @@ class Main(object): params.save_params(self, 'last_params.yml') self.on_serial_disconnect_clicked() - db.stop_db() gtk.main_quit() def on_gtk_about_activate(self, menuitem, data=None): @@ -438,13 +429,7 @@ class Main(object): self.metadata = metadata if self.metadata is not None: - logger.info("Loading external metadata") - self.db_window.update_from_metadata(self.metadata) - elif self.db_window.params['exp_id_entry'] is None: - logger.info("DB exp_id field blank, autogenerating") - self.db_window.on_exp_id_autogen_button_clicked() - - self.db_window.params = {'measure_id_entry':experiment_id.hex} + logger.info("Loading external metadata") def exceptions(): """ Cleans up after errors """ @@ -470,13 +455,7 @@ class Main(object): else: nb.get_nth_page(nb.page_num(self.ft_window)).hide() # nb.get_nth_page(nb.page_num(self.period_window)).hide() - - if parameters['db_enable_checkbutton']: - if db.current_db is None: - db.start_db() - elif not db.current_db.connected: - db.restart_db() - + comm.serial_instance.proc_pipe_p.send(self.current_exp) # Flush data pipe @@ -811,33 +790,6 @@ class Main(object): self.autosavedir_button.get_filename(), self.autosavename.get_text() ) - # Database output - if self.current_exp.parameters['db_enable_checkbutton']: - meta = {} - - if self.current_exp.parameters['metadata'] is not None: - metadata = self.current_exp.parameters['metadata'] - exp_metakeys = ['experiment_uuid', 'patient_id', 'name'] - meta.update( - {k: metadata[k] - for k in metadata - if k not in exp_metakeys - } - ) - - name = self.current_exp.parameters['measure_name_entry'] - - newname = db.current_db.add_results( - measurement_uuid=self.active_experiment_id.hex, - measurement_name=name, - experiment_uuid=self.current_exp.parameters['exp_id_entry'], - experiment_metadata=meta, - patient_id=self.current_exp.parameters['patient_id_entry'], - timestamp=None, - data=self.current_exp.export() - ) - - self.db_window.params = {'measure_name_entry':newname} # uDrop # UI stuff diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 6dbc720..048d1f4 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -44,8 +44,7 @@ def get_params(window): logger.info("No gain selected.") parameters.update(window.exp_window.get_params(selection)) parameters.update(window.analysis_opt_window.params) - parameters.update(window.db_window.persistent_params) - + return parameters def save_params(window, path): @@ -77,6 +76,5 @@ def set_params(window, params): window.exp_window.set_params(params['experiment_index'], params) window.analysis_opt_window.params = params - window.db_window.params = params window.params_loaded = True diff --git a/pavement.py b/pavement.py index bf1f907..0aea7f5 100644 --- a/pavement.py +++ b/pavement.py @@ -18,8 +18,7 @@ setup(name='dstat_interface', license='GPLv3', packages=['dstat_interface', ], install_requires=['matplotlib', 'numpy', 'pyserial', 'pyzmq', - 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2', 'zodb', - 'zeo', 'psutil'], + 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2'], # Install data listed in `MANIFEST.in` include_package_data=True) -- GitLab From c25da7fc60f54f7d4f42fde9fa50fa0d16c44901 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 14 Mar 2017 19:25:43 -0400 Subject: [PATCH 36/66] Update changelog and readme. --- CHANGELOG | 102 +++++++++++++++++++++++++----------------------- README.markdown | 5 +-- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f90415d..0c5a35d 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,32 +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 -Version 1.2 - -Old Microdrop interface depreciated - -New zmq_plugin based interface - -Internal changes to save functionality and plot storage. +Version 1.0.5 + -Bugfix for saving error introduced by new logging system -Version 1.3 - -Fixed a bug related to calibration mode - -Added ZODB data storage - -Integrated with zmq_plugin +Version 1.0.4 + -Adds support for synchronous electromechanical shutter detection (added in dstat/dstat-firmware@29d4c86) + -New version string system -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.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 \ No newline at end of file +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 a0157f9..c428347 100644 --- a/README.markdown +++ b/README.markdown @@ -38,9 +38,6 @@ Python and related packages needed: (versions listed are tested, older versions * XQuartz (2.7.7) * zeromq (4.0.5) and pyzmq (14.6.0) * pyyaml (3.11) -* zodb -* zeo -* psutil Optional: @@ -168,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) -- GitLab From 0ed5d5ce1888d74446241ac649e0ee584a9187e8 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 15 Mar 2017 20:14:19 -0400 Subject: [PATCH 37/66] Add dynamic plot notebook tab generation. --- dstat_interface/experiments/chronoamp.py | 20 ++--- dstat_interface/experiments/cv.py | 6 +- .../experiments/experiment_template.py | 28 +++---- dstat_interface/experiments/lsv.py | 6 +- dstat_interface/experiments/pot.py | 30 +++++--- dstat_interface/experiments/swv.py | 62 ++++++++------- .../interface/dstatinterface.glade | 66 ++-------------- dstat_interface/interface/exp_int.py | 3 +- dstat_interface/interface/plot_ui.py | 33 ++++++++ dstat_interface/main.py | 77 ++++++++++--------- 10 files changed, 157 insertions(+), 174 deletions(-) create mode 100644 dstat_interface/interface/plot_ui.py diff --git a/dstat_interface/experiments/chronoamp.py b/dstat_interface/experiments/chronoamp.py index 284f5e2..980d03c 100644 --- a/dstat_interface/experiments/chronoamp.py +++ b/dstat_interface/experiments/chronoamp.py @@ -4,7 +4,6 @@ import struct from experiments.experiment_template import PlotBox, Experiment class ChronoampBox(PlotBox): - def format_plots(self): """ Creates and formats subplots needed. Overrides superclass. @@ -19,7 +18,7 @@ class ChronoampBox(PlotBox): class Chronoamp(Experiment): """Chronoamperometry experiment""" def setup(self): - self.plotbox = ChronoampBox('current_time') + self.plots.append(ChronoampBox('current_time')) self.datatype = "linearData" self.datalength = 2 @@ -64,16 +63,19 @@ class Chronoamp(Experiment): class PDExp(Chronoamp): """Photodiode/PMT experiment""" - def __init__(self, parameters): - super(Chronoamp, self).__init__(parameters) # Don't want to call CA's init - + def setup(self): + self.plots.append(ChronoampBox('current_time')) + self.datatype = "linearData" - self.xlabel = "Time (s)" - self.ylabel = "Current (A)" self.datalength = 2 self.databytes = 8 - self.xmin = 0 - self.xmax = int(self.parameters['time']) + self.data = {'current_time' : [([],[])]} + self.plot_format = { + 'current_time' : { + 'labels' : ('Time (s)','Current (A)'), + 'xlims' : (0, int(self.parameters['time'])) + } + } if self.parameters['shutter_true']: if self.parameters['sync_true']: diff --git a/dstat_interface/experiments/cv.py b/dstat_interface/experiments/cv.py index c14edd8..cd2bc26 100644 --- a/dstat_interface/experiments/cv.py +++ b/dstat_interface/experiments/cv.py @@ -5,10 +5,8 @@ from experiments.experiment_template import PlotBox, Experiment class CVExp(Experiment): """Cyclic Voltammetry experiment""" - def __init__(self, parameters): - super(CVExp, self).__init__(parameters) - self.plotbox = PlotBox() - + def setup(self): + super(CVExp, self).setup() self.datatype = "CVData" self.xlabel = "Voltage (mV)" self.ylabel = "Current (A)" diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 9a7077d..00f35cc 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -36,11 +36,12 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_gtk3agg \ import FigureCanvasGTK3Agg as FigureCanvas -from matplotlib.backends.backend_gtk3 \ - import NavigationToolbar2GTK3 as NavigationToolbar +# from matplotlib.backends.backend_gtk3 \ +# import NavigationToolbar2GTK3 as NavigationToolbar try: import seaborn as sns + sns.set(context='paper', style='darkgrid') except InputError: pass import serial @@ -65,7 +66,7 @@ class Experiment(object): self.datapoint = 0 self.scan = 0 self.time = 0 - self.plots = {} + self.plots = [] major, minor = self.parameters['version'] @@ -97,7 +98,7 @@ class Experiment(object): self.commands[1] += "{p[gain]} {p[short_true]:d} ".format(p=self.parameters) self.setup() - + def setup(self): self.data = {'current_voltage' : [([],[])]} self.plot_format = { @@ -110,7 +111,7 @@ class Experiment(object): # list of scans, tuple of dimensions, list of data self.line_data = ([], []) - self.plotbox = PlotBox(['current_voltage']) + self.plots.append(PlotBox(['current_voltage'])) def run(self, ser, ctrl_pipe, data_pipe): """Execute experiment. Connects and sends handshake signal to DStat @@ -253,11 +254,7 @@ class PlotBox(object): def __init__(self, plots): """Initializes plots. self.box should be reparented.""" self.name = "Main" - - try: - sns.set(context='paper', style='darkgrid') - except NameError: - pass + self.continuous_refresh = True self.plotnames = plots self.subplots = {} @@ -272,12 +269,9 @@ class PlotBox(object): self.canvas = FigureCanvas(self.figure) self.canvas.set_vexpand(True) - self.win = Gtk.Window() + self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - self.win.add(self.box) self.box.pack_start(self.canvas, expand=True, fill=True, padding=0) - self.toolbar = NavigationToolbar(self.canvas, self.win) - self.box.pack_start(self.toolbar, expand=False, fill=False, padding=0) def format_plots(self): """ @@ -285,7 +279,7 @@ class PlotBox(object): """ # Calculate size of grid needed - if len(self.plotnames > 1): + if len(self.plotnames) > 1: gs = gridspec.GridSpec(ceil(len(self.plotnames)/2.),2) else: gs = gridspec.GridSpec(1,1) @@ -295,8 +289,6 @@ class PlotBox(object): for subplot in self.subplots.values(): subplot.ticklabel_format(style='sci', scilimits=(0, 3), useOffset=False, axis='y') - - for subplot in self.subplots.values(): subplot.plot([],[]) def clearall(self): @@ -341,8 +333,6 @@ class PlotBox(object): self.subplots[name].set_xlabel(subplot['labels'][0]) self.subplots[name].set_ylabel(subplot['labels'][1]) self.subplots[name].set_xlim(subplot['xlims']) - - Experiment.plotbox = self self.figure.canvas.draw() diff --git a/dstat_interface/experiments/lsv.py b/dstat_interface/experiments/lsv.py index 8733dd6..cdf17da 100644 --- a/dstat_interface/experiments/lsv.py +++ b/dstat_interface/experiments/lsv.py @@ -5,10 +5,8 @@ from experiments.experiment_template import PlotBox, Experiment class LSVExp(Experiment): """Linear Scan Voltammetry experiment""" - def __init__(self, parameters): - super(LSVExp, self).__init__(parameters) - - self.plotbox = PlotBox() + def setup(self): + super(LSVExp, self).setup() self.datatype = "linearData" self.datalength = 2 diff --git a/dstat_interface/experiments/pot.py b/dstat_interface/experiments/pot.py index a8afd63..27e26a7 100644 --- a/dstat_interface/experiments/pot.py +++ b/dstat_interface/experiments/pot.py @@ -10,33 +10,28 @@ class PotBox(PlotBox): """ self.subplots = {'voltage_time' : self.figure.add_subplot(111)} - for subplot in self.subplots: + for key, subplot in self.subplots.items(): subplot.ticklabel_format(style='sci', scilimits=(0, 3), useOffset=False, axis='y') + subplot.plot([],[]) class PotExp(Experiment): """Potentiometry experiment""" - def __init__(self, parameters): - super(PotExp, self).__init__(parameters) - self.plotbox = PotBox() + def setup(self): + self.plots.append(PotBox(['voltage_time'])) self.datatype = "linearData" - self.xlabel = "Time (s)" - self.ylabel = "Voltage (V)" self.datalength = 2 self.databytes = 8 + self.data = {'voltage_time' : [([],[])]} self.plot_format = { - 'current_time' : { + 'voltage_time' : { 'labels' : ('Time (s)', 'Voltage (V)' ), 'xlims' : (0, int(self.parameters['time'])) } } - self.plot_format['current_voltage']['xlims'] = ( - int(0), - int(self.parameters['time']) - ) self.commands += "E" self.commands[2] += "P" @@ -51,4 +46,15 @@ class PotExp(Experiment): return (scan, ( seconds+milliseconds/1000., voltage*(1.5/8388607.) ) - ) \ No newline at end of file + ) + + def store_data(self, incoming, newline): + """Stores data in data attribute. Should not be called from subprocess. + Can be overriden for custom experiments.""" + line, data = incoming + + if newline is True: + self.data['voltage_time'].append(deepcopy(self.line_data)) + + for i, item in enumerate(self.data['voltage_time'][line]): + item.append(data[i]) \ No newline at end of file diff --git a/dstat_interface/experiments/swv.py b/dstat_interface/experiments/swv.py index db3483a..e8afb66 100644 --- a/dstat_interface/experiments/swv.py +++ b/dstat_interface/experiments/swv.py @@ -1,5 +1,6 @@ import time import struct +from copy import deepcopy from experiments.experiment_template import PlotBox, Experiment @@ -9,31 +10,17 @@ class SWVBox(PlotBox): Creates and formats subplots needed. Overrides superclass. """ - self.subplots = {'current_voltage' : self.figure.add_subplot(111)} + self.subplots = {'swv' : self.figure.add_subplot(111)} - for subplot in self.subplots: + for key, subplot in self.subplots.items(): subplot.ticklabel_format(style='sci', scilimits=(0, 3), useOffset=False, axis='y') - - for subplot in self.subplots: 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].set_data( - Experiment.data[subplot][line_number][0:2]) - class SWVExp(Experiment): """Square Wave Voltammetry experiment""" - def __init__(self, parameters): - super(SWVExp, self).__init__(parameters) - self.plotbox = SWVBox() + def setup(self): + self.plots.append(SWVBox(['swv'])) self.datatype = "SWVData" self.xlabel = "Voltage (mV)" @@ -45,10 +32,14 @@ class SWVExp(Experiment): self.datalength = 2 * self.parameters['scans'] self.databytes = 10 - self.plot_format['current_voltage']['xlims'] = ( - int(self.parameters['start']), - int(self.parameters['stop']) - ) + self.plot_format = { + 'swv' : {'labels' : ('Voltage (mV)', + 'Current (A)' + ), + 'xlims' : (int(self.parameters['start']), + int(self.parameters['stop'])) + } + } self.commands += "E" self.commands[2] += "S" @@ -90,14 +81,22 @@ class SWVExp(Experiment): r_trim*(1.5/self.gain/8388607) ) ) + + def store_data(self, incoming, newline): + """Stores data in data attribute. Should not be called from subprocess. + Can be overriden for custom experiments.""" + line, data = incoming + + if newline is True: + self.data['swv'].append(deepcopy(self.line_data)) + for i, item in enumerate(self.data['swv'][line]): + item.append(data[i]) class DPVExp(SWVExp): """Diffential Pulse Voltammetry experiment.""" - def __init__(self, parameters): - """Overrides SWVExp method, extends Experiment method""" - super(SWVExp, self).__init__(parameters) - self.plotbox = SWVBox() + def setup(self): + self.plots.append(SWVBox(['swv'])) self.datatype = "SWVData" self.xlabel = "Voltage (mV)" @@ -109,9 +108,14 @@ class DPVExp(SWVExp): self.datalength = 2 self.databytes = 10 - self.xlim = (int(self.parameters['start']), - int(self.parameters['stop']) - ) + self.plot_format = { + 'swv' : {'labels' : ('Voltage (mV)', + 'Current (A)' + ), + 'xlims' : (int(self.parameters['start']), + int(self.parameters['stop'])) + } + } self.commands += "E" self.commands[2] += "D" diff --git a/dstat_interface/interface/dstatinterface.glade b/dstat_interface/interface/dstatinterface.glade index e134224..33f8acf 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -1108,76 +1108,22 @@ Thanks to Christian Fobel for help with Dropbot Plugin True bottom - - True - False - vertical - - - - + - - True - False - Main - - - False - + - - True - False - - - - - - - - - 1 - + - - True - False - FT - - - 1 - False - + - - True - False - - - - - - - - - 2 - + - - True - False - Periodogram - - - 2 - False - + diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index 44ff614..d9e6124 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -29,6 +29,7 @@ except ImportError: sys.exit(1) import dstat_comm +import experiments.cal as cal import __main__ from errors import InputError, VarError, ErrorLogger _logger = ErrorLogger(sender="dstat-interface-exp_int") @@ -447,7 +448,7 @@ class CAL(ExpInterface): __main__.MAIN.stop_ocp() __main__.MAIN.spinner.start() - offset = dstat_comm.measure_offset(self.params['time']) + offset = cal.measure_offset(self.params['time']) for i in offset: _logger.error(" ".join((i, str(-offset[i]))), "INFO") diff --git a/dstat_interface/interface/plot_ui.py b/dstat_interface/interface/plot_ui.py new file mode 100644 index 0000000..b0ff570 --- /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/main.py b/dstat_interface/main.py index dd168a0..412299f 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -53,6 +53,7 @@ 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 plot import params import parameter_test @@ -97,10 +98,9 @@ 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.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.analysis_opt_window = analysis.AnalysisOptions(self.builder) @@ -181,10 +181,10 @@ class Main(object): self.metadata = None # Should only be added to by plugin interface - 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.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.params_loaded = False # Disable 0MQ plugin API by default. @@ -429,8 +429,6 @@ class Main(object): def run_active_experiment(self, param_override=None, metadata=None): """Run currently visible experiment.""" - self.plotwindow.foreach(Gtk.Widget.destroy) - # Assign current experiment a unique identifier. experiment_id = uuid.uuid4() self.active_experiment_id = experiment_id @@ -456,22 +454,26 @@ class Main(object): def run_experiment(): """ Starts experiment """ - self.current_exp.plotbox.box.reparent(self.plotwindow) - self.plotwindow.show_all() - self.current_exp.plotbox.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() + interface.plot_ui.replace_notebook_exp( + self.plot_notebook, self.current_exp, self.window + ) + # self.current_exp.plotbox.box.reparent(self.plotwindow) + # self.plotwindow.show_all() + for plots in self.current_exp.plots: + plots.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() if parameters['db_enable_checkbutton']: if db.current_db is None: @@ -707,18 +709,21 @@ 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.current_exp.plotbox.addline() - # make sure all of last line is added - self.current_exp.plotbox.updateline(self.current_exp, self.lastline) - self.lastline = self.line - self.current_exp.plotbox.updateline(self.current_exp, self.line) - self.current_exp.plotbox.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): @@ -729,8 +734,8 @@ class Main(object): 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.experiment_running_plot(force_refresh=True) + self.databuffer.set_text("") self.databuffer.place_cursor(self.databuffer.get_start_iter()) self.rawbuffer.insert_at_cursor("\n") -- GitLab From 1695b8951256c8f8c3519899ea528ef2fb2a5eb0 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 21 Mar 2017 16:37:41 -0400 Subject: [PATCH 38/66] Remove commented code --- dstat_interface/main.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 412299f..88eb552 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -98,9 +98,6 @@ class Main(object): self.error_context_id = self.statusbar.get_context_id("error") self.message_context_id = self.statusbar.get_context_id("message") - - # 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.analysis_opt_window = analysis.AnalysisOptions(self.builder) @@ -119,9 +116,6 @@ 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) - #fill adc_pot_box self.adc_pot_box = self.builder.get_object('gain_adc_box') self.adc_pot_container = self.adc_pot.builder.get_object('vbox1') @@ -180,11 +174,6 @@ class Main(object): self.dropbot_triggered = False self.metadata = None # Should only be added to by plugin interface - - # 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.params_loaded = False # Disable 0MQ plugin API by default. @@ -457,8 +446,6 @@ class Main(object): interface.plot_ui.replace_notebook_exp( self.plot_notebook, self.current_exp, self.window ) - # self.current_exp.plotbox.box.reparent(self.plotwindow) - # self.plotwindow.show_all() for plots in self.current_exp.plots: plots.changetype(self.current_exp) -- GitLab From 8de52207e608aeb801a21cb90a0f47ed626f03ae Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 21 Mar 2017 16:39:00 -0400 Subject: [PATCH 39/66] interface.exp_int: Switch to native logging. --- dstat_interface/interface/exp_int.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index d9e6124..c56dc91 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -19,6 +19,7 @@ import os import sys +import logging try: import gi @@ -31,8 +32,8 @@ except ImportError: import dstat_comm 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): """Generic experiment interface class. Should be subclassed to implement @@ -69,7 +70,7 @@ 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): @@ -438,7 +439,7 @@ class CAL(ExpInterface): 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: @@ -451,7 +452,7 @@ class CAL(ExpInterface): offset = cal.measure_offset(self.params['time']) for i in offset: - _logger.error(" ".join((i, str(-offset[i]))), "INFO") + logger.info("{} {}".format(i, str(-offset[i]))) dstat_comm.settings[i][1] = str(-offset[i]) self.entry['R100'].set_text(str( -- GitLab From 78792bed6a0592623bff3ff08c51514c63169f75 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 21 Mar 2017 16:40:35 -0400 Subject: [PATCH 40/66] Make experiment windows and running modular. --- dstat_interface/experiments/cal.py | 1 + dstat_interface/experiments/chronoamp.py | 2 + dstat_interface/experiments/cv.py | 1 + .../experiments/experiment_template.py | 20 ++- dstat_interface/experiments/idle.py | 2 + dstat_interface/experiments/lsv.py | 1 + dstat_interface/experiments/pot.py | 1 + dstat_interface/experiments/swv.py | 2 + .../interface/dstatinterface.glade | 63 +------ dstat_interface/interface/exp_int.py | 124 +++++++++++-- dstat_interface/interface/exp_window.py | 73 +++++--- dstat_interface/main.py | 163 ++++-------------- dstat_interface/params.py | 7 +- 13 files changed, 215 insertions(+), 245 deletions(-) diff --git a/dstat_interface/experiments/cal.py b/dstat_interface/experiments/cal.py index f775c29..f57c26f 100755 --- a/dstat_interface/experiments/cal.py +++ b/dstat_interface/experiments/cal.py @@ -49,6 +49,7 @@ def measure_offset(time): return gain_offset class CALExp(Experiment): + id = 'cal' """Offset calibration experiment""" def __init__(self, parameters): self.parameters = parameters diff --git a/dstat_interface/experiments/chronoamp.py b/dstat_interface/experiments/chronoamp.py index 980d03c..a57ae83 100644 --- a/dstat_interface/experiments/chronoamp.py +++ b/dstat_interface/experiments/chronoamp.py @@ -16,6 +16,7 @@ class ChronoampBox(PlotBox): subplot.plot([],[]) class Chronoamp(Experiment): + id = 'cae' """Chronoamperometry experiment""" def setup(self): self.plots.append(ChronoampBox('current_time')) @@ -63,6 +64,7 @@ class Chronoamp(Experiment): class PDExp(Chronoamp): """Photodiode/PMT experiment""" + id = 'pde' def setup(self): self.plots.append(ChronoampBox('current_time')) diff --git a/dstat_interface/experiments/cv.py b/dstat_interface/experiments/cv.py index cd2bc26..fbeda56 100644 --- a/dstat_interface/experiments/cv.py +++ b/dstat_interface/experiments/cv.py @@ -4,6 +4,7 @@ import struct from experiments.experiment_template import PlotBox, Experiment class CVExp(Experiment): + id = 'cve' """Cyclic Voltammetry experiment""" def setup(self): super(CVExp, self).setup() diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 00f35cc..ee1587e 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -36,8 +36,6 @@ import matplotlib.pyplot as plt from matplotlib.backends.backend_gtk3agg \ import FigureCanvasGTK3Agg as FigureCanvas -# from matplotlib.backends.backend_gtk3 \ -# import NavigationToolbar2GTK3 as NavigationToolbar try: import seaborn as sns @@ -56,9 +54,11 @@ import dstat_comm 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. + 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 @@ -94,8 +94,10 @@ class Experiment(object): 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.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() @@ -329,11 +331,17 @@ class PlotBox(object): """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): diff --git a/dstat_interface/experiments/idle.py b/dstat_interface/experiments/idle.py index 662fe68..5bf84bd 100644 --- a/dstat_interface/experiments/idle.py +++ b/dstat_interface/experiments/idle.py @@ -5,6 +5,7 @@ from experiments.experiment_template import Experiment class OCPExp(Experiment): """Open circuit potential measumement in statusbar.""" + id = 'ocp' def __init__(self): self.databytes = 8 @@ -26,6 +27,7 @@ class OCPExp(Experiment): class PMTIdle(Experiment): """Open circuit potential measumement in statusbar.""" + id = "pmt_idle" def __init__(self): self.databytes = 8 diff --git a/dstat_interface/experiments/lsv.py b/dstat_interface/experiments/lsv.py index cdf17da..9042a5e 100644 --- a/dstat_interface/experiments/lsv.py +++ b/dstat_interface/experiments/lsv.py @@ -5,6 +5,7 @@ from experiments.experiment_template import PlotBox, Experiment class LSVExp(Experiment): """Linear Scan Voltammetry experiment""" + id = 'lsv' def setup(self): super(LSVExp, self).setup() diff --git a/dstat_interface/experiments/pot.py b/dstat_interface/experiments/pot.py index 27e26a7..b4de225 100644 --- a/dstat_interface/experiments/pot.py +++ b/dstat_interface/experiments/pot.py @@ -16,6 +16,7 @@ class PotBox(PlotBox): subplot.plot([],[]) class PotExp(Experiment): + id = 'pot' """Potentiometry experiment""" def setup(self): self.plots.append(PotBox(['voltage_time'])) diff --git a/dstat_interface/experiments/swv.py b/dstat_interface/experiments/swv.py index e8afb66..f4b328f 100644 --- a/dstat_interface/experiments/swv.py +++ b/dstat_interface/experiments/swv.py @@ -19,6 +19,7 @@ class SWVBox(PlotBox): class SWVExp(Experiment): """Square Wave Voltammetry experiment""" + id = 'swv' def setup(self): self.plots.append(SWVBox(['swv'])) @@ -95,6 +96,7 @@ class SWVExp(Experiment): class DPVExp(SWVExp): """Diffential Pulse Voltammetry experiment.""" + id = 'dpv' def setup(self): self.plots.append(SWVBox(['swv'])) diff --git a/dstat_interface/interface/dstatinterface.glade b/dstat_interface/interface/dstatinterface.glade index 33f8acf..345ab09 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -2,63 +2,6 @@ - - - - - - - - - - - - 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 @@ -999,15 +942,13 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True False - ExpComboListStore - False - False + True 2 diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index c56dc91..37e8ca9 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -30,6 +30,7 @@ except ImportError: sys.exit(1) import dstat_comm +import experiments as exp import experiments.cal as cal import __main__ from errors import InputError, VarError @@ -37,8 +38,11 @@ logger = logging.getLogger("dstat.interface.exp_int") class ExpInterface(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. """ + id = None + experiment = None def __init__(self, glade_path): self.builder = Gtk.Builder() self.builder.add_from_file(glade_path) @@ -77,6 +81,9 @@ class ExpInterface(object): """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') class Chronoamp(ExpInterface): """Experiment class for chronoamperometry. Extends ExpInterface class to @@ -87,10 +94,14 @@ 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') @@ -159,13 +170,70 @@ class Chronoamp(ExpInterface): for i in table: self.model.append(i) - + +class EIS(ExpInterface): + """Experiment class for EIS.""" + id = 'eis' + def __init__(self): + experiment = exp.EISExp + """Adds entry listings to superclass's self.entry dict""" + self.name = "Impedance Spectroscopy" + + self.entry = {} # to be used only for str parameters + self._params = None + + self.window = Gtk.ScrolledWindow() + self.window.set_vexpand(True) + + grid = Gtk.Grid() + grid.set_column_homogeneous(False) + + entries = {'start' : 'Start (Hz)', + 'stop' : 'Stop (Hz)', + 'n_increments' : 'Number of steps', + 'cycles' : 'Number of settling cycles'} + + for n, i in enumerate(entries.items()): + key, value = i + grid.attach(Gtk.Label(label=value), 0, n, 1, 1) + self.entry[key] = Gtk.Entry() + grid.attach(self.entry[key], 1, n, 1, 1) + + buttons = ('') + + self.window.add(grid) + + def get_window(self): + return self.window + + def _fill_params(self): + super(EIS, self)._fill_params() + + self._params['cyclic_true'] = False + + def _get_params(self): + """Updates self._params from UI.""" + super(EIS, self)._get_params() + + self._params['cyclic_true'] = self.builder.get_object( + 'cyclic_checkbutton').get_active() + + def _set_params(self): + """Updates UI with new parameters.""" + super(EIS, self)._set_params() + + self.builder.get_object('cyclic_checkbutton').set_active( + self._params['cyclic_true']) + 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') @@ -176,10 +244,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') @@ -192,10 +263,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') @@ -228,10 +302,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') @@ -243,23 +321,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') @@ -361,17 +445,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') diff --git a/dstat_interface/interface/exp_window.py b/dstat_interface/interface/exp_window.py index 10fa69c..8e7c0a3 100755 --- a/dstat_interface/interface/exp_window.py +++ b/dstat_interface/interface/exp_window.py @@ -17,40 +17,69 @@ # 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 + import interface.exp_int as exp +logger = logging.getLogger("dstat.interface.exp_window") + class Experiments: def __init__(self, builder): 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(): + 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_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 +87,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/main.py b/dstat_interface/main.py index 88eb552..274d2bb 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -138,12 +138,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') @@ -158,9 +152,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 @@ -208,14 +202,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.""" @@ -441,53 +427,12 @@ class Main(object): self.stopbutton.set_sensitive(False) self.start_ocp() - def run_experiment(): - """ Starts experiment """ - interface.plot_ui.replace_notebook_exp( - self.plot_notebook, self.current_exp, self.window - ) - for plots in self.current_exp.plots: - plots.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() - - if parameters['db_enable_checkbutton']: - if db.current_db is None: - db.start_db() - elif not db.current_db.connected: - db.restart_db() - - 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() - selection = self.expcombobox.get_active() parameters = {} parameters['version'] = self.version parameters['metadata'] = self.metadata @@ -509,84 +454,36 @@ 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 = exp.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 - parameter_test.lsv_test(parameters) - - self.current_exp = exp.LSVExp(parameters) - run_experiment() - - return experiment_id - - elif selection == 2: # CV - parameter_test.cv_test(parameters) - - self.current_exp = exp.CVExp(parameters) - run_experiment() - - return experiment_id - - elif selection == 3: # SWV - parameter_test.swv_test(parameters) - - self.current_exp = exp.SWVExp(parameters) - run_experiment() - - return experiment_id - - elif selection == 4: # DPV - parameter_test.dpv_test(parameters) - - self.current_exp = exp.DPVExp(parameters) - run_experiment() - - return experiment_id - - elif selection == 6: # PD - parameter_test.pd_test(parameters) - - self.current_exp = exp.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 - - parameter_test.pot_test(parameters) - - self.current_exp = exp.PotExp(parameters) - run_experiment() + for plots in self.current_exp.plots: + plots.changetype(self.current_exp) + + if parameters['db_enable_checkbutton']: + if db.current_db is None: + db.start_db() + elif not db.current_db.connected: + db.restart_db() + + comm.serial_instance.proc_pipe_p.send(self.current_exp) - return experiment_id + # Flush data pipe + while comm.serial_instance.data_pipe_p.poll(): + comm.serial_instance.data_pipe_p.recv() - 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) diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 6dbc720..1bf6db8 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -28,9 +28,8 @@ 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: @@ -71,9 +70,7 @@ def load_params(window, path): def set_params(window, params): window.adc_pot.params = params if 'experiment_index' in params: - window.expcombobox.set_active( - window.exp_window.classes[params['experiment_index']][0] - ) + 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 -- GitLab From d23840202a057b7741b59151734f6ffcf0453814 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 21 Mar 2017 17:10:15 -0400 Subject: [PATCH 41/66] CV: Fix experiment window order. --- dstat_interface/interface/cv.glade | 184 ++++++++++++++--------------- 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/dstat_interface/interface/cv.glade b/dstat_interface/interface/cv.glade index c65e4bc..9cc5d7f 100644 --- a/dstat_interface/interface/cv.glade +++ b/dstat_interface/interface/cv.glade @@ -21,13 +21,13 @@ False vertical - + True False 0 out - + True False 5 @@ -40,56 +40,62 @@ True True - + True False - Start (mV) + False + Potential (mV) - 0 + 1 0 - + + True + False + Time (s) + + + 2 + 0 + + + + True False - Vertex 1 (mV) 0 - 1 + 0 - + True False - Vertex 2 (mV) + Cleaning 0 - 2 + 1 - + True - True - - 8 - 0 - 1 - False - False + False + Deposition - 1 - 0 + 0 + 2 - + True True @@ -105,7 +111,7 @@ - + True True @@ -121,29 +127,7 @@ - - True - False - Slope (mV/s) - - - 0 - 3 - - - - - True - False - Scans - - - 0 - 4 - - - - + True True @@ -154,12 +138,12 @@ False - 1 - 3 + 2 + 2 - + True True @@ -170,8 +154,8 @@ False - 1 - 4 + 2 + 1 @@ -179,10 +163,10 @@ - + True False - Experiment + Preconditioning True @@ -190,18 +174,18 @@ False False - 1 + 2 0 - + True False 0 out - + True False 5 @@ -214,62 +198,56 @@ True True - - True - False - False - Potential (mV) - - - 1 - 0 - - - - + True False - Time (s) + Start (mV) - 2 + 0 0 - + True False + Vertex 1 (mV) 0 - 0 + 1 - + True False - Cleaning + Vertex 2 (mV) 0 - 1 + 2 - + True - False - Deposition + True + + 8 + 0 + 1 + False + False - 0 - 2 + 1 + 0 - + True True @@ -285,7 +263,7 @@ - + True True @@ -301,7 +279,29 @@ - + + True + False + Slope (mV/s) + + + 0 + 3 + + + + + True + False + Scans + + + 0 + 4 + + + + True True @@ -312,12 +312,12 @@ False - 2 - 2 + 1 + 3 - + True True @@ -328,8 +328,8 @@ False - 2 - 1 + 1 + 4 @@ -337,10 +337,10 @@ - + True False - Preconditioning + Experiment True @@ -348,7 +348,7 @@ False False - 2 + 1 1 -- GitLab From 7a2e409204feb3c3ac78918ca43b87ae02acc0d0 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 21 Mar 2017 17:22:39 -0400 Subject: [PATCH 42/66] Remove missed db call --- dstat_interface/main.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index c4b65be..27c017d 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -449,12 +449,6 @@ class Main(object): for plots in self.current_exp.plots: plots.changetype(self.current_exp) - if parameters['db_enable_checkbutton']: - if db.current_db is None: - db.start_db() - elif not db.current_db.connected: - db.restart_db() - comm.serial_instance.proc_pipe_p.send(self.current_exp) # Flush data pipe -- GitLab From e19217ece77bb7c64bee6979034aa0c3b78a1aa4 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 21 Mar 2017 17:23:00 -0400 Subject: [PATCH 43/66] Fixed parameter loading. --- dstat_interface/params.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dstat_interface/params.py b/dstat_interface/params.py index 6d384e6..0aa8580 100755 --- a/dstat_interface/params.py +++ b/dstat_interface/params.py @@ -61,6 +61,8 @@ def load_params(window, path): 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) -- GitLab From e4a8643ec243e8052788edd2f1109fdad7aea198 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 27 Mar 2017 16:48:54 -0400 Subject: [PATCH 44/66] Convert to future print syntax. --- dstat_interface/interface/exp_int.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index 37e8ca9..caa4a58 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -16,6 +16,7 @@ # # 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 @@ -26,7 +27,7 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: - print "ERR: GTK not available" + print("ERR: GTK not available") sys.exit(1) import dstat_comm -- GitLab From 257f3d83df62f2d341c270650802bb4e1a490c55 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 27 Mar 2017 16:49:33 -0400 Subject: [PATCH 45/66] Plot: fix gridspec syntax for multiple experiments on one plot. --- dstat_interface/experiments/experiment_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index ee1587e..3cc2d66 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -282,7 +282,7 @@ class PlotBox(object): # Calculate size of grid needed if len(self.plotnames) > 1: - gs = gridspec.GridSpec(ceil(len(self.plotnames)/2.),2) + gs = gridspec.GridSpec(int(ceil(len(self.plotnames)/2.)),2) else: gs = gridspec.GridSpec(1,1) for n, i in enumerate(self.plotnames): -- GitLab From c36aec51d2ec0905195f054bc4490d5cad53f582 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 27 Mar 2017 20:02:50 -0400 Subject: [PATCH 46/66] Make data display modular. --- dstat_interface/experiments/chronoamp.py | 6 +- .../experiments/experiment_template.py | 52 +++++++++-- dstat_interface/experiments/pot.py | 5 +- dstat_interface/experiments/swv.py | 6 +- dstat_interface/interface/data_view.py | 76 ++++++++++++++++ .../interface/dstatinterface.glade | 58 ++----------- dstat_interface/main.py | 87 ++++++++----------- 7 files changed, 174 insertions(+), 116 deletions(-) create mode 100644 dstat_interface/interface/data_view.py diff --git a/dstat_interface/experiments/chronoamp.py b/dstat_interface/experiments/chronoamp.py index a57ae83..1f09855 100644 --- a/dstat_interface/experiments/chronoamp.py +++ b/dstat_interface/experiments/chronoamp.py @@ -25,9 +25,10 @@ class Chronoamp(Experiment): self.datalength = 2 self.databytes = 8 self.data = {'current_time' : [([],[])]} + self.columns = ['Time (s)', 'Current (A)'] self.plot_format = { 'current_time' : { - 'labels' : ('Time (s)','Current (A)'), + 'labels' : self.columns, 'xlims' : (0, sum(self.parameters['time'])) } } @@ -72,9 +73,10 @@ class PDExp(Chronoamp): self.datalength = 2 self.databytes = 8 self.data = {'current_time' : [([],[])]} + self.columns = ['Time (s)', 'Current (A)'] self.plot_format = { 'current_time' : { - 'labels' : ('Time (s)','Current (A)'), + 'labels' : self.columns, 'xlims' : (0, int(self.parameters['time'])) } } diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 3cc2d66..72e1e25 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -19,6 +19,8 @@ # 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 @@ -36,7 +38,9 @@ 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') @@ -100,18 +104,22 @@ class Experiment(object): p=self.parameters) self.setup() + self.time = [datetime.utcnow()] def setup(self): - self.data = {'current_voltage' : [([],[])]} + self.data = OrderedDict( + (('current_voltage', [([],[])])) + ) + + self.columns = ['Voltage (mV)', 'Current (A)'] self.plot_format = { - 'current_voltage' : {'labels' : ('Voltage (mV)', - 'Current (A)' - ), + 'current_voltage' : {'labels' : self.columns, 'xlims' : (0, 1) } } # list of scans, tuple of dimensions, list of data self.line_data = ([], []) + self.columns = ('Voltage (mV)', 'Current (A)') self.plots.append(PlotBox(['current_voltage'])) @@ -235,6 +243,11 @@ class Experiment(object): return data + def experiment_done(self): + """Runs when experiment is finished (all data acquired)""" + self.data_to_pandas() + self.time += [datetime.utcnow()] + def export(self): """Return a dict containing data for saving.""" output = { @@ -250,7 +263,34 @@ class Experiment(object): return output - + def data_to_pandas(self): + """Convert data to pandas DataFrame and set as member of .df + attribute.""" + self.df = OrderedDict() + + for name, data in self.data.items(): + df = DataFrame(columns=['Scan'] + self.columns) + + for n, line in enumerate(data): + df = df.append(DataFrame( + OrderedDict(zip(['Scan'] + self.columns, + [n] + list(line)) + ) + ), ignore_index = True + ) + + self.df[name] = df + + def get_info_text(self): + """Return string of text to disply on Info tab.""" + buf = "#Time: S{} E{}\n".format(self.time[0], self.time[1]) + buf += "#Commands:\n" + + for line in self.commands: + buf += '#{}\n'.format(line) + + return buf + class PlotBox(object): """Contains data plot and associated methods.""" def __init__(self, plots): diff --git a/dstat_interface/experiments/pot.py b/dstat_interface/experiments/pot.py index b4de225..2859e46 100644 --- a/dstat_interface/experiments/pot.py +++ b/dstat_interface/experiments/pot.py @@ -25,11 +25,10 @@ class PotExp(Experiment): self.datalength = 2 self.databytes = 8 self.data = {'voltage_time' : [([],[])]} + self.columns = ['Time (s)', 'Voltage (V)'] self.plot_format = { 'voltage_time' : { - 'labels' : ('Time (s)', - 'Voltage (V)' - ), + 'labels' : self.columns, 'xlims' : (0, int(self.parameters['time'])) } } diff --git a/dstat_interface/experiments/swv.py b/dstat_interface/experiments/swv.py index f4b328f..2f3ba80 100644 --- a/dstat_interface/experiments/swv.py +++ b/dstat_interface/experiments/swv.py @@ -32,7 +32,8 @@ class SWVExp(Experiment): self.line_data = ([], [], [], []) self.datalength = 2 * self.parameters['scans'] self.databytes = 10 - + self.columns = ['Voltage (mV)', 'Net Current (A)', + 'Forward Current (A)', 'Reverse Current (A)'] self.plot_format = { 'swv' : {'labels' : ('Voltage (mV)', 'Current (A)' @@ -109,7 +110,8 @@ class DPVExp(SWVExp): self.line_data = ([], [], [], []) self.datalength = 2 self.databytes = 10 - + self.columns = ['Voltage (mV)', 'Net Current (A)', + 'Forward Current (A)', 'Reverse Current (A)'] self.plot_format = { 'swv' : {'labels' : ('Voltage (mV)', 'Current (A)' diff --git a/dstat_interface/interface/data_view.py b/dstat_interface/interface/data_view.py new file mode 100644 index 0000000..2ef8ff5 --- /dev/null +++ b/dstat_interface/interface/data_view.py @@ -0,0 +1,76 @@ +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.""" + self.textview.set_buffer(self.buffers[self.combobox.get_active_id()]) + +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/dstatinterface.glade b/dstat_interface/interface/dstatinterface.glade index 02aab91..09dfb6c 100644 --- a/dstat_interface/interface/dstatinterface.glade +++ b/dstat_interface/interface/dstatinterface.glade @@ -1007,7 +1007,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin - + True True True @@ -1122,64 +1122,16 @@ Thanks to Christian Fobel for help with Dropbot Plugin - - True - True - - - True - True - False - 10 - 10 - databuffer1 - - - - - 1 - + - - True - False - Raw Data - - - 1 - False - + - - True - True - - - True - True - False - 10 - 10 - databuffer2 - - - - - 2 - + - - True - False - Extra Data - - - 2 - False - + diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 27c017d..f30fbff 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -53,6 +53,7 @@ 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 @@ -88,8 +89,6 @@ class Main(object): 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() @@ -107,6 +106,9 @@ class Main(object): # Setup Plots self.plot_notebook = self.builder.get_object('plot_notebook') + 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') @@ -446,8 +448,11 @@ class Main(object): self.plot_notebook, self.current_exp, self.window ) - for plots in self.current_exp.plots: - plots.changetype(self.current_exp) + for plot in self.current_exp.plots: + plot.changetype(self.current_exp) + + self.data_view.clear_exps() + self.info_page.clear() comm.serial_instance.proc_pipe_p.send(self.current_exp) @@ -594,46 +599,40 @@ class Main(object): copy data to raw data tab. Saves data if autosave enabled. """ try: - self.current_exp.time = datetime.now() + 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) - 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) + + # # 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 - for i in self.current_exp.commands: - self.rawbuffer.insert_at_cursor(i) - - self.rawbuffer.insert_at_cursor("\n") - + self.info_page.set_text(self.current_exp.get_info_text()) + try: self.statusbar.push( self.message_context_id, @@ -656,21 +655,9 @@ class Main(object): ) for i in analysis_buffer: - self.rawbuffer.insert_at_cursor("%s\n" % i) + self.info_page.add_line(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) + self.data_view.add_exp(self.current_exp) # Autosaving if self.autosave_checkbox.get_active(): -- GitLab From 06e6f61e905699c9109c89983df880fa8da84ea7 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 28 Mar 2017 15:19:48 -0400 Subject: [PATCH 47/66] Fixed saving with separate experiments and gtk3. --- .../experiments/experiment_template.py | 8 ++ dstat_interface/interface/save.py | 110 ++++++------------ 2 files changed, 43 insertions(+), 75 deletions(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 72e1e25..a6fc978 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -290,6 +290,14 @@ class Experiment(object): buf += '#{}\n'.format(line) return buf + + def get_save_strings(self): + """Return dict of strings with experiment parameters and data.""" + buf = {} + buf['params'] = self.get_info_text() + buf.update({exp : df.to_string() for exp, df in self.df.items()}) + + return buf class PlotBox(object): """Contains data plot and associated methods.""" diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 982dcee..515b4d9 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -37,12 +37,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)) - + 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("Space separated text (.txt)") + filters[0].set_name("Text (.txt)") filters[0].add_pattern("*.txt") fcd.set_do_overwrite_confirmation(True) @@ -51,7 +51,7 @@ def manSave(current_exp): response = fcd.run() - if response == Gtk.RESPONSE_OK: + if response == Gtk.ResponseType.OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() @@ -61,14 +61,14 @@ def manSave(current_exp): 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)) + Gtk.FileChooserAction.SAVE, + (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) filters = [Gtk.FileFilter()] filters[0].set_name("Portable Document Format (.pdf)") @@ -83,7 +83,7 @@ def plot_save_dialog(plots): response = fcd.run() - if response == Gtk.RESPONSE_OK: + if response == Gtk.ResponseType.OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) filter_selection = fcd.get_filter().get_name() @@ -100,17 +100,16 @@ 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…", 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[0].set_name("Parameter File (.yml)") @@ -122,7 +121,7 @@ def man_param_save(window): response = fcd.run() - if response == Gtk.RESPONSE_OK: + if response == Gtk.ResponseType.OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) @@ -133,16 +132,15 @@ 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…", 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[0].set_name("Parameter File (.yml)") @@ -153,7 +151,7 @@ def man_param_load(window): response = fcd.run() - if response == Gtk.RESPONSE_OK: + if response == Gtk.ResponseType.OK: path = fcd.get_filename() logger.info("Selected filepath: %s", path) @@ -161,7 +159,7 @@ def man_param_load(window): fcd.destroy() - elif response == Gtk.RESPONSE_CANCEL: + elif response == Gtk.ResponseType.CANCEL: fcd.destroy() def autoSave(exp, path, name): @@ -186,62 +184,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. @@ -256,9 +214,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("%s%s-%s.%s" % (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("%s%s-%s.%s" % (name, num, plot_type, ext)) \ No newline at end of file -- GitLab From da3a56637e2b1c562a9854669229a399b6c38cfb Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 3 Apr 2017 17:20:08 -0400 Subject: [PATCH 48/66] Save: Switch to tab-separated output. --- dstat_interface/experiments/experiment_template.py | 2 +- dstat_interface/interface/save.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index a6fc978..96888d4 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -295,7 +295,7 @@ class Experiment(object): """Return dict of strings with experiment parameters and data.""" buf = {} buf['params'] = self.get_info_text() - buf.update({exp : df.to_string() for exp, df in self.df.items()}) + buf.update({exp : df.to_csv(sep='\t') for exp, df in self.df.items()}) return buf diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 515b4d9..8cba566 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -42,7 +42,7 @@ def manSave(current_exp): Gtk.STOCK_SAVE, Gtk.ResponseType.OK)) filters = [Gtk.FileFilter()] - filters[0].set_name("Text (.txt)") + filters[0].set_name("Tab-separated Text (.txt)") filters[0].add_pattern("*.txt") fcd.set_do_overwrite_confirmation(True) -- GitLab From b57e9791ccc8d2b796dc40b82c474a5fdcb14ad2 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 3 Apr 2017 18:27:22 -0400 Subject: [PATCH 49/66] data_view: suppress error when nothing selected. --- dstat_interface/interface/data_view.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dstat_interface/interface/data_view.py b/dstat_interface/interface/data_view.py index 2ef8ff5..e4bd711 100644 --- a/dstat_interface/interface/data_view.py +++ b/dstat_interface/interface/data_view.py @@ -47,7 +47,12 @@ class DataPage(object): def combobox_changed(self, object): """Switch displayed data buffer.""" - self.textview.set_buffer(self.buffers[self.combobox.get_active_id()]) + try: + self.textview.set_buffer( + self.buffers[self.combobox.get_active_id()] + ) + except KeyError: + pass class InfoPage(object): def __init__(self, notebook, name="Info"): -- GitLab From cbbb9216baa771952ef7ed08190131178114bd49 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 3 Apr 2017 20:25:41 -0400 Subject: [PATCH 50/66] Moved serial instance into common state file. --- dstat_interface/dstat_comm.py | 204 +++++++++++++----- dstat_interface/experiments/cal.py | 8 +- .../experiments/experiment_template.py | 4 +- dstat_interface/interface/exp_int.py | 53 ++--- dstat_interface/main.py | 80 +++---- dstat_interface/state.py | 4 + 6 files changed, 218 insertions(+), 135 deletions(-) create mode 100644 dstat_interface/state.py diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index cc81ce3..a468d47 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -22,24 +22,47 @@ from serial.tools import list_ports import time import struct import multiprocessing as mp +from collections import OrderedDict import logging +try: + import gi + gi.require_version('Gtk', '3.0') + from gi.repository import Gtk, GObject +except ImportError: + print "ERR: GTK not available" + sys.exit(1) + from errors import InputError, VarError logger = logging.getLogger("dstat.comm") dstat_logger = logging.getLogger("dstat.comm.DSTAT") exp_logger = logging.getLogger("dstat.comm.Experiment") -serial_instance = None -settings = {} +import state + +class AlreadyConnectedError(Exception): + def __init__(self): + super(AlreadyConnectedError, self).__init__(self, + "Serial instance already connected.") + +class NotConnectedError(Exception): + def __init__(self): + super(NotConnectedError, self).__init__(self, + "Serial instance not connected.") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger = logging.getLogger("dstat.comm._serial_process") - ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) - ser_logger.info("Reattaching DStat udc") - ser.write("!R") # Send restart command - ser.close() + connected = False + + try: + ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) + ser_logger.info("Reattaching DStat udc") + # ser.write("!R") # Send restart command + ser.close() + except serial.SerialException: + return 1 for i in range(5): time.sleep(1) # Give OS time to enumerate @@ -47,9 +70,15 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): try: ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) ser_logger.info("Connecting") - break + connected = True except serial.SerialException: pass + + if connected is True: + break + + if ser.isOpen() is False: + return 1 ser.write("ck") # Keep this to support old firmwares @@ -78,7 +107,7 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger.info("DISCONNECT") ser.close() proc_pipe.send("DISCONNECT") - return False + return 0 elif proc_pipe.poll(): while ctrl_pipe.poll(): @@ -94,19 +123,91 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): -class SerialConnection(object): - def __init__(self, ser_port): - self.proc_pipe_p, self.proc_pipe_c = mp.Pipe(duplex=True) - self.ctrl_pipe_p, self.ctrl_pipe_c = mp.Pipe(duplex=True) - self.data_pipe_p, self.data_pipe_c = mp.Pipe(duplex=True) - - self.proc = mp.Process(target=_serial_process, args=(ser_port, - self.proc_pipe_c, self.ctrl_pipe_c, - self.data_pipe_c)) - self.proc.start() +class SerialConnection(GObject.GObject): + __gsignals__ = { + 'connected': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'disconnected': (GObject.SIGNAL_RUN_FIRST, None, ()) + } + + def __init__(self): + super(SerialConnection, self).__init__() + self.connected = False + + def connect(self, ser_port): + if self.connected is False: + self.proc_pipe_p, self.proc_pipe_c = mp.Pipe(duplex=True) + self.ctrl_pipe_p, self.ctrl_pipe_c = mp.Pipe(duplex=True) + self.data_pipe_p, self.data_pipe_c = mp.Pipe(duplex=True) + + self.proc = mp.Process(target=_serial_process, args=(ser_port, + self.proc_pipe_c, self.ctrl_pipe_c, + self.data_pipe_c)) + self.proc.start() + time.sleep(.5) + if self.proc.is_alive() is False: + raise ConnectionError() + return False + self.connected = True + self.emit('connected') + return True + else: + raise AlreadyConnectedError() + return False + + def assert_connected(self): + if self.connected is False: + raise NotConnectedError() + + def start_exp(self, exp): + self.assert_connected() + + self.proc_pipe_p.send(exp) + + def stop_exp(self): + self.assert_connected() + self.send_ctrl('a') + def get_proc(self, block=False): + self.assert_connected() + + if block is True: + return self.proc_pipe_p.recv() + else: + if self.proc_pipe_p.poll() is True: + return self.proc_pipe_p.recv() + else: + return None + + def get_data(self, block=False): + self.assert_connected() + + if block is True: + return self.data_pipe_p.recv() + else: + if self.data_pipe_p.poll() is True: + return self.data_pipe_p.recv() + else: + return None + + def flush_data(self): + self.assert_connected() + + while self.proc_pipe_p.poll() is True: + self.proc_pipe_p.recv() + + def send_ctrl(self, ctrl): + self.assert_connected() + + self.ctrl_pipe_p.send(ctrl) + + def disconnect(self): + self.send_ctrl('a') + time.sleep(.2) + self.proc.terminate() + self.emit('disconnected') + self.connected = False -class VersionCheck: +class VersionCheck(object): def __init__(self): pass @@ -160,24 +261,24 @@ def version_check(ser_port): Arguments: ser_port -- address of serial port to use """ - try: - global serial_instance - serial_instance = SerialConnection(ser_port) - - serial_instance.proc_pipe_p.send(VersionCheck()) - result = serial_instance.proc_pipe_p.recv() - if result == "SERIAL_ERROR": - buffer = 1 - else: - buffer = serial_instance.data_pipe_p.recv() - logger.debug("version_check done") - - return buffer + # try: + state.ser = SerialConnection() + + state.ser.connect(ser_port) + state.ser.start_exp(VersionCheck()) + result = state.ser.get_proc(block=True) + if result == "SERIAL_ERROR": + buffer = 1 + else: + buffer = state.ser.get_data(block=True) + logger.debug("version_check done") + + return buffer - except: - pass + # except: + # pass -class Settings: +class Settings(object): def __init__(self, task, settings=None): self.task = task self.settings = settings @@ -200,7 +301,7 @@ class Settings: return status def read(self): - settings = {} + settings = OrderedDict() self.ser.flushInput() self.ser.write('!') @@ -252,16 +353,11 @@ def read_settings(): settings. """ - global settings - settings = {} - - while serial_instance.data_pipe_p.poll(): - serial_instance.data_pipe_p.recv() - - serial_instance.proc_pipe_p.send(Settings(task='r')) - settings = serial_instance.data_pipe_p.recv() + state.ser.flush_data() + state.ser.start_exp(Settings(task='r')) + state.settings = state.ser.get_data(block=True) - logger.debug("read_settings: %s", serial_instance.proc_pipe_p.recv()) + logger.debug("read_settings: %s", state.ser.get_proc(block=True)) return @@ -269,12 +365,10 @@ def write_settings(): """Tries to write settings to DStat from global settings var. """ - while serial_instance.data_pipe_p.poll(): - serial_instance.data_pipe_p.recv() + state.ser.flush_data() + state.ser.start_exp(Settings(task='w', settings=state.settings)) - serial_instance.proc_pipe_p.send(Settings(task='w', settings=settings)) - - logger.debug("write_settings: %s", serial_instance.proc_pipe_p.recv()) + logger.debug("write_settings: %s", state.ser.get_proc(block=True)) return @@ -317,14 +411,12 @@ def read_light_sensor(): light sensor clear channel. """ - while serial_instance.data_pipe_p.poll(): - serial_instance.data_pipe_p.recv() - - serial_instance.proc_pipe_p.send(LightSensor()) + state.ser.flush_data() + state.ser.start_exp(LightSensor()) - logger.info("read_light_sensor: %s", serial_instance.proc_pipe_p.recv()) + logger.debug("read_light_sensor: %s", state.ser.get_proc(block=True)) - return serial_instance.data_pipe_p.recv() + return state.ser.get_data(block=True) class delayedSerial(serial.Serial): diff --git a/dstat_interface/experiments/cal.py b/dstat_interface/experiments/cal.py index f57c26f..9f965b7 100755 --- a/dstat_interface/experiments/cal.py +++ b/dstat_interface/experiments/cal.py @@ -28,7 +28,7 @@ import serial logger = logging.getLogger("dstat.experiments.cal") -from dstat_comm import serial_instance +import state from experiments.experiment_template import Experiment def measure_offset(time): @@ -42,9 +42,9 @@ def measure_offset(time): for i in range(1,8): parameters['gain'] = i - serial_instance.proc_pipe_p.send(CALExp(parameters)) - logger.info("measure_offset: %s", serial_instance.proc_pipe_p.recv()) - gain_offset[gain_trim_table[i]] = serial_instance.data_pipe_p.recv() + state.ser.start_exp(CALExp(parameters)) + logger.info("measure_offset: %s", state.ser.get_proc(block=True)) + gain_offset[gain_trim_table[i]] = state.ser.get_data(block=True) return gain_offset diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 96888d4..47a49a1 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -53,7 +53,7 @@ dstat_logger = logging.getLogger("dstat.comm.DSTAT") exp_logger = logging.getLogger("dstat.comm.Experiment") from errors import InputError, VarError -import dstat_comm +import state class Experiment(object): """Store and acquire a potentiostat experiment. Meant to be subclassed @@ -87,7 +87,7 @@ class Experiment(object): self.gain = self.__gaintable[int(self.parameters['gain'])] self.gain_trim = int( - dstat_comm.settings[ + state.settings[ self.__gain_trim_table[int(self.parameters['gain'])] ][1] ) diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index caa4a58..3e1cb67 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -31,6 +31,7 @@ except ImportError: sys.exit(1) import dstat_comm +import state import experiments as exp import experiments.cal as cal import __main__ @@ -406,11 +407,11 @@ class PD(ExpInterface): self.builder.get_object('light_label').set_text(str( dstat_comm.read_light_sensor())) dstat_comm.read_settings() - dstat_comm.settings['tcs_enabled'][1] = '1' # Make sure TCS enabled + state.settings['tcs_enabled'][1] = '1' # Make sure TCS enabled dstat_comm.write_settings() self.builder.get_object('threshold_entry').set_text(str( - dstat_comm.settings['tcs_clear_threshold'][1])) + state.settings['tcs_clear_threshold'][1])) __main__.MAIN.start_ocp() finally: @@ -424,12 +425,12 @@ class PD(ExpInterface): i.set_sensitive(False) try: - dstat_comm.settings['tcs_clear_threshold'][1] = self.builder.get_object( + state.settings['tcs_clear_threshold'][1] = self.builder.get_object( 'threshold_entry').get_text() dstat_comm.write_settings() dstat_comm.read_settings() self.builder.get_object('threshold_entry').set_text( - str(dstat_comm.settings['tcs_clear_threshold'][1])) + str(state.settings['tcs_clear_threshold'][1])) __main__.MAIN.start_ocp() finally: @@ -487,19 +488,19 @@ class CAL(ExpInterface): dstat_comm.read_settings() self.entry['R100'].set_text(str( - dstat_comm.settings['r100_trim'][1])) + state.settings['r100_trim'][1])) self.entry['R3k'].set_text(str( - dstat_comm.settings['r3k_trim'][1])) + state.settings['r3k_trim'][1])) self.entry['R30k'].set_text(str( - dstat_comm.settings['r30k_trim'][1])) + state.settings['r30k_trim'][1])) self.entry['R300k'].set_text(str( - dstat_comm.settings['r300k_trim'][1])) + state.settings['r300k_trim'][1])) self.entry['R3M'].set_text(str( - dstat_comm.settings['r3M_trim'][1])) + state.settings['r3M_trim'][1])) self.entry['R30M'].set_text(str( - dstat_comm.settings['r30M_trim'][1])) + state.settings['r30M_trim'][1])) self.entry['R100M'].set_text(str( - dstat_comm.settings['r100M_trim'][1])) + state.settings['r100M_trim'][1])) __main__.MAIN.start_ocp() @@ -514,13 +515,13 @@ class CAL(ExpInterface): __main__.MAIN.on_pot_stop_clicked() __main__.MAIN.stop_ocp() - dstat_comm.settings['r100_trim'][1] = self.entry['R100'].get_text() - dstat_comm.settings['r3k_trim'][1] = self.entry['R3k'].get_text() - dstat_comm.settings['r30k_trim'][1] = self.entry['R30k'].get_text() - dstat_comm.settings['r300k_trim'][1] = self.entry['R300k'].get_text() - dstat_comm.settings['r3M_trim'][1] = self.entry['R3M'].get_text() - dstat_comm.settings['r30M_trim'][1] = self.entry['R30M'].get_text() - dstat_comm.settings['r100M_trim'][1] = self.entry['R100M'].get_text() + state.settings['r100_trim'][1] = self.entry['R100'].get_text() + state.settings['r3k_trim'][1] = self.entry['R3k'].get_text() + state.settings['r30k_trim'][1] = self.entry['R30k'].get_text() + state.settings['r300k_trim'][1] = self.entry['R300k'].get_text() + state.settings['r3M_trim'][1] = self.entry['R3M'].get_text() + state.settings['r30M_trim'][1] = self.entry['R30M'].get_text() + state.settings['r100M_trim'][1] = self.entry['R100M'].get_text() dstat_comm.write_settings() __main__.MAIN.start_ocp() @@ -544,22 +545,22 @@ class CAL(ExpInterface): for i in offset: logger.info("{} {}".format(i, str(-offset[i]))) - dstat_comm.settings[i][1] = str(-offset[i]) + state.settings[i][1] = str(-offset[i]) self.entry['R100'].set_text(str( - dstat_comm.settings['r100_trim'][1])) + state.settings['r100_trim'][1])) self.entry['R3k'].set_text(str( - dstat_comm.settings['r3k_trim'][1])) + state.settings['r3k_trim'][1])) self.entry['R30k'].set_text(str( - dstat_comm.settings['r30k_trim'][1])) + state.settings['r30k_trim'][1])) self.entry['R300k'].set_text(str( - dstat_comm.settings['r300k_trim'][1])) + state.settings['r300k_trim'][1])) self.entry['R3M'].set_text(str( - dstat_comm.settings['r3M_trim'][1])) + state.settings['r3M_trim'][1])) self.entry['R30M'].set_text(str( - dstat_comm.settings['r30M_trim'][1])) + state.settings['r30M_trim'][1])) self.entry['R100M'].set_text(str( - dstat_comm.settings['r100M_trim'][1])) + state.settings['r100M_trim'][1])) __main__.MAIN.start_ocp() finally: diff --git a/dstat_interface/main.py b/dstat_interface/main.py index f30fbff..5621e2e 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -59,6 +59,7 @@ import params import parameter_test import analysis import zmq +import state from errors import InputError from plugin import DstatPlugin, get_hub_uri @@ -258,8 +259,8 @@ class Main(object): self.stop_ocp() else: self.on_pot_stop_clicked() - comm.serial_instance.ctrl_pipe_p.send("DISCONNECT") - comm.serial_instance.proc.terminate() + state.ser.send_ctrl("DISCONNECT") + state.ser.disconnect() except AttributeError as err: logger.warning("AttributeError: %s", err) @@ -284,16 +285,15 @@ class Main(object): if self.version[0] >= 1 and self.version[1] >= 2: # Flush data pipe - while comm.serial_instance.data_pipe_p.poll(): - comm.serial_instance.data_pipe_p.recv() + state.ser.flush_data() if self.pmt_mode == True: logger.info("Start PMT idle mode") - comm.serial_instance.proc_pipe_p.send(exp.PMTIdle()) + state.ser.start_exp(exp.PMTIdle()) else: logger.info("Start OCP") - comm.serial_instance.proc_pipe_p.send(exp.OCPExp()) + state.ser.start_exp(exp.OCPExp()) self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data), GObject.timeout_add(250, self.ocp_running_proc) @@ -312,7 +312,7 @@ class Main(object): logger.info("Stop PMT idle mode") else: logger.info("Stop OCP") - comm.serial_instance.ctrl_pipe_p.send('a') + state.ser.send_ctrl('a') for i in self.ocp_proc: GObject.source_remove(i) @@ -334,9 +334,8 @@ class Main(object): """ try: - if comm.serial_instance.data_pipe_p.poll(): - incoming = comm.serial_instance.data_pipe_p.recv() - + incoming = state.ser.get_data() + while incoming is not None: if isinstance(incoming, basestring): # test if incoming is str self.on_serial_disconnect_clicked() return False @@ -346,9 +345,7 @@ class Main(object): " V"]) self.ocp_disp.set_text(data) - if comm.serial_instance.data_pipe_p.poll(): - self.ocp_running_data() - return True + incoming = state.ser.get_data() return True @@ -367,19 +364,16 @@ class Main(object): """ try: - if comm.serial_instance.proc_pipe_p.poll(): - proc_buffer = comm.serial_instance.proc_pipe_p.recv() + proc_buffer = state.ser.get_proc() + while proc_buffer is not None: logger.debug("ocp_running_proc: %s", proc_buffer) if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: if proc_buffer == "SERIAL_ERROR": self.on_serial_disconnect_clicked() - while comm.serial_instance.data_pipe_p.poll(): - comm.serial_instance.data_pipe_p.recv() + state.ser.flush_data() return False - - return True - + proc_buffer = state.ser.get_proc() return True except EOFError: @@ -417,8 +411,7 @@ class Main(object): self.stop_ocp() self.statusbar.remove_all(self.error_context_id) - while comm.serial_instance.data_pipe_p.poll(): # Clear data pipe - comm.serial_instance.data_pipe_p.recv() + state.ser.flush_data() parameters = {} parameters['version'] = self.version @@ -454,11 +447,10 @@ class Main(object): self.data_view.clear_exps() self.info_page.clear() - comm.serial_instance.proc_pipe_p.send(self.current_exp) + state.ser.start_exp(self.current_exp) # Flush data pipe - while comm.serial_instance.data_pipe_p.poll(): - comm.serial_instance.data_pipe_p.recv() + state.ser.flush_data() self.plot_proc = GObject.timeout_add(200, self.experiment_running_plot) @@ -515,22 +507,19 @@ class Main(object): function from GTK's queue. """ try: - if comm.serial_instance.data_pipe_p.poll(): - incoming = comm.serial_instance.data_pipe_p.recv() - - self.line, data = incoming - if self.line > self.lastdataline: - newline = True - self.lastdataline = self.line - else: - newline = False - - self.current_exp.store_data(incoming, newline) - - if comm.serial_instance.data_pipe_p.poll(): - self.experiment_running_data() - return True - + incoming = state.ser.get_data() + while incoming is not None: + try: + self.line, data = incoming + if self.line > self.lastdataline: + newline = True + self.lastdataline = self.line + else: + newline = False + self.current_exp.store_data(incoming, newline) + except TypeError: + pass + incoming = state.ser.get_data() return True except EOFError as err: @@ -552,9 +541,8 @@ class Main(object): function from GTK's queue. """ try: - if comm.serial_instance.proc_pipe_p.poll(): - proc_buffer = comm.serial_instance.proc_pipe_p.recv() - + proc_buffer = state.ser.get_proc() + if proc_buffer is not None: if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: self.experiment_done() if proc_buffer == "SERIAL_ERROR": @@ -563,9 +551,7 @@ class Main(object): else: logger.warning("Unrecognized experiment return code: %s", proc_buffer) - return False - return True except EOFError as err: @@ -687,7 +673,7 @@ class Main(object): def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" try: - comm.serial_instance.ctrl_pipe_p.send('a') + state.ser.stop_exp() except AttributeError: pass diff --git a/dstat_interface/state.py b/dstat_interface/state.py new file mode 100644 index 0000000..74295bd --- /dev/null +++ b/dstat_interface/state.py @@ -0,0 +1,4 @@ +from collections import OrderedDict + +settings = OrderedDict() +ser = None \ No newline at end of file -- GitLab From 1ed5d00b499b7d210ec2d10b5e0e78abca331243 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 4 Apr 2017 19:57:58 -0400 Subject: [PATCH 51/66] Change GObject.GObject to GObject.Object --- dstat_interface/dstat_comm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index a468d47..96d64fe 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -123,7 +123,7 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): -class SerialConnection(GObject.GObject): +class SerialConnection(GObject.Object): __gsignals__ = { 'connected': (GObject.SIGNAL_RUN_FIRST, None, ()), 'disconnected': (GObject.SIGNAL_RUN_FIRST, None, ()) -- GitLab From d0407c516be0e63b8652b367e09a14ee91f716b8 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 4 Apr 2017 19:59:17 -0400 Subject: [PATCH 52/66] Change DStat version parameter to state variable. --- .../experiments/experiment_template.py | 2 +- dstat_interface/main.py | 15 +++++++-------- dstat_interface/state.py | 3 ++- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 47a49a1..33ece7d 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -72,7 +72,7 @@ class Experiment(object): self.time = 0 self.plots = [] - major, minor = self.parameters['version'] + major, minor = state.dstat_version if major >= 1: if minor == 1: diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 5621e2e..8fa9ff6 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -210,22 +210,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() @@ -283,7 +283,7 @@ class Main(object): def start_ocp(self): """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 state.ser.flush_data() @@ -307,7 +307,7 @@ class Main(object): def stop_ocp(self): """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: @@ -414,7 +414,6 @@ class Main(object): state.ser.flush_data() parameters = {} - parameters['version'] = self.version parameters['metadata'] = self.metadata # Make sure these are defined diff --git a/dstat_interface/state.py b/dstat_interface/state.py index 74295bd..7e3f195 100644 --- a/dstat_interface/state.py +++ b/dstat_interface/state.py @@ -1,4 +1,5 @@ from collections import OrderedDict settings = OrderedDict() -ser = None \ No newline at end of file +ser = None +dstat_version = None \ No newline at end of file -- GitLab From ae18c63b9fee69a1dbe7a2e6cd23b0d9d8d61027 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 4 Apr 2017 19:59:59 -0400 Subject: [PATCH 53/66] Except OCP display errors. --- dstat_interface/main.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 8fa9ff6..b0d5cb4 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -339,11 +339,14 @@ class Main(object): if isinstance(incoming, basestring): # test if incoming is str self.on_serial_disconnect_clicked() return False - - data = "".join(["OCP: ", - "{0:.3f}".format(incoming), - " V"]) - self.ocp_disp.set_text(data) + + try: + data = "".join(["OCP: ", + "{0:.3f}".format(incoming), + " V"]) + self.ocp_disp.set_text(data) + except ValueError: + pass incoming = state.ser.get_data() -- GitLab From ac5079baed183334a6a9bc80caa4a091c0979004 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 4 Apr 2017 20:02:42 -0400 Subject: [PATCH 54/66] Implement GObject signals for running utility functions (outside normal experiment procedure) --- .../experiments/experiment_template.py | 2 +- dstat_interface/interface/exp_int.py | 18 +++++++++++--- dstat_interface/interface/exp_window.py | 24 +++++++++++++++++-- dstat_interface/main.py | 7 ++++-- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 33ece7d..88c5a39 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -27,7 +27,7 @@ from math import ceil try: import gi gi.require_version('Gtk', '3.0') - from gi.repository import Gtk + from gi.repository import Gtk, GObject except ImportError: print "ERR: GTK not available" sys.exit(1) diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index 3e1cb67..83ebfac 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -38,14 +38,21 @@ import __main__ 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. 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): + super(ExpInterface, self).__init__() self.builder = Gtk.Builder() self.builder.add_from_file(glade_path) self.builder.connect_signals(self) @@ -86,7 +93,11 @@ class ExpInterface(object): 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. @@ -176,9 +187,10 @@ class Chronoamp(ExpInterface): class EIS(ExpInterface): """Experiment class for EIS.""" id = 'eis' + experiment = exp.EISExp def __init__(self): - experiment = exp.EISExp """Adds entry listings to superclass's self.entry dict""" + super(ExpInterface, self).__init__() # initialize GObject self.name = "Impedance Spectroscopy" self.entry = {} # to be used only for str parameters diff --git a/dstat_interface/interface/exp_window.py b/dstat_interface/interface/exp_window.py index 8e7c0a3..413d43a 100755 --- a/dstat_interface/interface/exp_window.py +++ b/dstat_interface/interface/exp_window.py @@ -21,12 +21,25 @@ 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 logger = logging.getLogger("dstat.interface.exp_window") -class Experiments: +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) @@ -39,12 +52,13 @@ class Experiments: } self.classes = OrderedDict(sorted(classes.items())) - #fill exp_section exp_section = self.builder.get_object('exp_section_box') self.containers = {} 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: @@ -61,6 +75,12 @@ class Experiments: 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()) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index b0d5cb4..f1a082f 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -98,6 +98,9 @@ class Main(object): self.message_context_id = self.statusbar.get_context_id("message") 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 @@ -280,7 +283,7 @@ 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 state.dstat_version[0] >= 1 and state.dstat_version[1] >= 2: @@ -304,7 +307,7 @@ 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 state.dstat_version[0] >= 1 and state.dstat_version[1] >= 2: -- GitLab From da8afa1fc805ac022aeac172f058bd8a848700da Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 12 Apr 2017 17:44:31 -0400 Subject: [PATCH 55/66] Change default log format. --- dstat_interface/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index f1a082f..59b0373 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -69,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) -- GitLab From 00a9e7e17d060af7a1a276a4cb9b265da7b28e6a Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 19 Apr 2017 15:37:10 -0400 Subject: [PATCH 56/66] Fix missing ConnectionError definition. --- dstat_interface/dstat_comm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 96d64fe..1266b37 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -50,6 +50,12 @@ 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") -- GitLab From 24c4d24d7a654086f2b9820e78a2620ab3826ef0 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 1 May 2017 19:13:04 -0400 Subject: [PATCH 57/66] Remove old error logger. --- dstat_interface/errors.py | 12 +----------- dstat_interface/interface/adc_pot.py | 3 +-- 2 files changed, 2 insertions(+), 13 deletions(-) mode change 100644 => 100755 dstat_interface/errors.py diff --git a/dstat_interface/errors.py b/dstat_interface/errors.py old mode 100644 new mode 100755 index ac61310..9661262 --- a/dstat_interface/errors.py +++ b/dstat_interface/errors.py @@ -44,14 +44,4 @@ class VarError(Error): def __init__(self, var, msg): self.var = var - self.msg = msg - -class ErrorLogger(object): - def __init__(self, sender="dstat-interface", level=('ERR', 'WAR', 'INFO')): - self.sender = str(sender) - self.level = level - self.levels = ('ERR', 'WAR', 'INFO', 'DBG') - - def error(self, msg, level): - if level in self.level: - print "[%s (%s)] %s" % (self.sender, level, str(msg)) \ No newline at end of file + self.msg = msg \ No newline at end of file diff --git a/dstat_interface/interface/adc_pot.py b/dstat_interface/interface/adc_pot.py index 1b236e1..8e73162 100755 --- a/dstat_interface/interface/adc_pot.py +++ b/dstat_interface/interface/adc_pot.py @@ -26,8 +26,7 @@ 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"), -- GitLab From 2d46d9b40c4fafb3ead46dcb06d59708fe7124ad Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 1 May 2017 19:21:43 -0400 Subject: [PATCH 58/66] Fix import typo. --- dstat_interface/experiments/experiment_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 88c5a39..722b35d 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -44,7 +44,7 @@ from pandas import DataFrame try: import seaborn as sns sns.set(context='paper', style='darkgrid') -except InputError: +except ImportError: pass import serial -- GitLab From ae33560cd19141657c3f96d262841fe172ef2e98 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 9 May 2017 16:15:18 -0400 Subject: [PATCH 59/66] Fix pandas saving to work with Unicode characters. --- .../experiments/experiment_template.py | 5 ++++- dstat_interface/interface/save.py | 22 ++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 722b35d..d926175 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -295,7 +295,10 @@ class Experiment(object): """Return dict of strings with experiment parameters and data.""" buf = {} buf['params'] = self.get_info_text() - buf.update({exp : df.to_csv(sep='\t') for exp, df in self.df.items()}) + buf.update( + {exp : df.to_csv(sep='\t', encoding='utf-8') + for exp, df in self.df.items()} + ) return buf diff --git a/dstat_interface/interface/save.py b/dstat_interface/interface/save.py index 8cba566..76a714e 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/interface/save.py @@ -18,6 +18,8 @@ # 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 @@ -26,7 +28,7 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk except ImportError: - print "ERR: GTK not available" + print("ERR: GTK not available") sys.exit(1) import numpy as np import logging @@ -52,9 +54,9 @@ def manSave(current_exp): response = fcd.run() if response == Gtk.ResponseType.OK: - path = fcd.get_filename() + 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) @@ -84,9 +86,9 @@ def plot_save_dialog(plots): response = fcd.run() if response == Gtk.ResponseType.OK: - path = fcd.get_filename() - logger.info("Selected filepath: %s", path) - filter_selection = fcd.get_filter().get_name() + 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"): @@ -122,7 +124,7 @@ def man_param_save(window): response = fcd.run() if response == Gtk.ResponseType.OK: - path = fcd.get_filename() + path = fcd.get_filename().decode("utf-8") logger.info("Selected filepath: %s", path) if not path.endswith(".yml"): @@ -152,7 +154,7 @@ def man_param_load(window): response = fcd.run() if response == Gtk.ResponseType.OK: - path = fcd.get_filename() + path = fcd.get_filename().decode("utf-8") logger.info("Selected filepath: %s", path) load_params(window, path) @@ -215,10 +217,10 @@ def save_plot(exp, path): for i in exp.plots: # Test for any existing files plot_type = '_'.join(i.name.lower().split()) - while os.path.exists("%s%s-%s.%s" % (name, num, plot_type, ext)): + while os.path.exists("{}{}-{}.{}".format(name, num, plot_type, ext)): j += 1 num = j for i in exp.plots: # save data plot_type = '_'.join(i.name.lower().split()) - i.figure.savefig("%s%s-%s.%s" % (name, num, plot_type, ext)) \ No newline at end of file + i.figure.savefig("{}{}-{}.{}".format(name, num, plot_type, ext)) \ No newline at end of file -- GitLab From 880a51ca235670347b9e77817fb42ff43c748121 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 16 May 2017 20:30:22 -0400 Subject: [PATCH 60/66] Switch to new communications protocol from dstat-firmware@fe50c38 --- dstat_interface/dstat_comm.py | 154 ++++++++++-------- dstat_interface/experiments/cal.py | 2 +- dstat_interface/experiments/chronoamp.py | 36 ++-- .../experiments/experiment_template.py | 98 +++++++++-- dstat_interface/experiments/idle.py | 4 +- 5 files changed, 189 insertions(+), 105 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 1266b37..511b059 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -63,7 +63,9 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): connected = False try: - ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) + 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() @@ -74,8 +76,9 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): time.sleep(1) # Give OS time to enumerate try: - ser = delayedSerial(ser_port, baudrate=1000000, timeout=1) + ser = serial.Serial(ser_port, timeout=1) ser_logger.info("Connecting") + time.sleep(2) connected = True except serial.SerialException: pass @@ -84,20 +87,21 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): break if ser.isOpen() is False: + ser_logger.info("Connection Error") return 1 - ser.write("ck") # Keep this to support old firmwares - - 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: - ser.write('V') - 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 @@ -118,8 +122,11 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): 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) @@ -149,7 +156,7 @@ class SerialConnection(GObject.Object): self.proc_pipe_c, self.ctrl_pipe_c, self.data_pipe_c)) self.proc.start() - time.sleep(.5) + time.sleep(2) if self.proc.is_alive() is False: raise ConnectionError() return False @@ -225,22 +232,29 @@ class VersionCheck(object): ser_port -- address of serial port to use """ try: - ser.flushInput() - ser.write('!') - - while not ser.read()=="C": - ser.flushInput() - ser.write('!') - - 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('.') @@ -266,8 +280,7 @@ def version_check(ser_port): Arguments: ser_port -- address of serial port to use - """ - # try: + """ state.ser = SerialConnection() state.ser.connect(ser_port) @@ -278,11 +291,10 @@ def version_check(ser_port): else: buffer = state.ser.get_data(block=True) logger.debug("version_check done") + time.sleep(.1) return buffer - - # except: - # pass + class Settings(object): def __init__(self, task, settings=None): @@ -308,25 +320,30 @@ class Settings(object): def read(self): settings = OrderedDict() - - self.ser.flushInput() - self.ser.write('!') + 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": - self.ser.flushInput() - 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)): @@ -335,24 +352,35 @@ class Settings(object): return settings def write(self): - self.ser.flushInput() - self.ser.write('!') - - while not self.ser.read()=="C": - self.ser.flushInput() - 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 @@ -387,11 +415,11 @@ class LightSensor: light sensor clear channel. """ - ser.flushInput() + ser.reset_input_buffer() ser.write('!') - while not ser.read()=="C": - self.ser.flushInput() + while not ser.read()=="@": + self.ser.reset_input_buffer() ser.write('!') ser.write('T') @@ -400,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('.') @@ -423,16 +451,6 @@ def read_light_sensor(): logger.debug("read_light_sensor: %s", state.ser.get_proc(block=True)) return state.ser.get_data(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) class SerialDevices(object): """Retrieves and stores list of serial devices in self.ports""" diff --git a/dstat_interface/experiments/cal.py b/dstat_interface/experiments/cal.py index 9f965b7..9abdc20 100755 --- a/dstat_interface/experiments/cal.py +++ b/dstat_interface/experiments/cal.py @@ -97,7 +97,7 @@ class CALExp(Experiment): 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.serial.flushInput() return True diff --git a/dstat_interface/experiments/chronoamp.py b/dstat_interface/experiments/chronoamp.py index 1f09855..f2fefa1 100644 --- a/dstat_interface/experiments/chronoamp.py +++ b/dstat_interface/experiments/chronoamp.py @@ -1,7 +1,9 @@ import time import struct +import numpy as np +import serial -from experiments.experiment_template import PlotBox, Experiment +from experiments.experiment_template import PlotBox, Experiment, exp_logger class ChronoampBox(PlotBox): def format_plots(self): @@ -33,13 +35,14 @@ class Chronoamp(Experiment): } } - self.commands += "E" - self.commands[2] += "R" + str(len(self.parameters['potential'])) + " " + self.commands.append( + ("ER"+ str(len(self.parameters['potential'])) + " 0 ", []) + ) + for i in self.parameters['potential']: - self.commands[2] += str(int(i*(65536./3000)+32768)) + " " + self.commands[-1][1].append(str(int(i*(65536./3000)+32768))) for i in self.parameters['time']: - self.commands[2] += str(i) + " " - self.commands[2] += "0 " # disable photodiode interlock + self.commands[-1][1].append(str(i)) def data_handler(self, data_input): """Overrides Experiment method to not convert x axis to mV.""" @@ -89,21 +92,20 @@ class PDExp(Chronoamp): else: self.commands.append("E2") - self.commands.append("ER1 ") + self.commands.append(("ER1 ", [])) + if self.parameters['interlock_true']: + self.commands[-1][0] += "1 " + else: + self.commands[-1][0] += "0 " + if self.parameters['voltage'] == 0: # Special case where V=0 - self.commands[-1] += "65535" + self.commands[-1][1].append("65535") else: - self.commands[-1] += str(int( + self.commands[-1][1].append(str(int( 65535-(self.parameters['voltage']*(65536./3000)))) - self.commands[-1] += " " - self.commands[-1] += str(self.parameters['time']) - self.commands[-1] += " " - if self.parameters['interlock_true']: - self.commands[-1] += "1" - else: - self.commands[-1] += "0" - self.commands[-1] += " " + ) + self.commands[-1][1].append(str(self.parameters['time'])) if self.parameters['shutter_true']: if self.parameters['sync_true']: diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index d926175..8afe88e 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -23,6 +23,7 @@ from datetime import datetime from collections import OrderedDict from copy import deepcopy from math import ceil +import time try: import gi @@ -123,6 +124,60 @@ class Experiment(object): 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, @@ -135,21 +190,30 @@ class Experiment(object): exp_logger.info("Experiment running") try: - self.serial.flushInput() - status = "DONE" - for i in self.commands: - logger.info("Command: %s", i) - self.serial.flushInput() - self.serial.write('!') + 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 - while not self.serial.read()=="C": - self.serial.flushInput() - self.serial.write('!') - - self.serial.write(i) if not self.serial_handler(): status = "ABORT" + break + + time.sleep(0.5) except serial.SerialException: status = "SERIAL_ERROR" @@ -169,17 +233,19 @@ class Experiment(object): if self.ctrl_pipe.poll(): input = self.ctrl_pipe.recv() logger.debug("serial_handler: %s", input) - if input == ('a' or "DISCONNECT"): + 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') - logger.info("serial_handler: ABORT pressed!") - return False if line.startswith('B'): data = self.data_handler( @@ -198,9 +264,9 @@ class Experiment(object): 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.serial.flushInput() + time.sleep(.3) return True except serial.SerialException: diff --git a/dstat_interface/experiments/idle.py b/dstat_interface/experiments/idle.py index 5bf84bd..23e871b 100644 --- a/dstat_interface/experiments/idle.py +++ b/dstat_interface/experiments/idle.py @@ -9,14 +9,12 @@ class OCPExp(Experiment): def __init__(self): self.databytes = 8 - self.commands = ["EA", "EP"] + 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 - self.commands[1] += "0 " # no timeout - self.commands[1] += "0 " # OCP measurement mode def data_handler(self, data_input): """Overrides Experiment method to only send ADC values.""" -- GitLab From af96f65f70701c525f6f1cfeb3f862916d99d084 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 16 May 2017 20:30:52 -0400 Subject: [PATCH 61/66] Save: Fix unicode issue. --- dstat_interface/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 59b0373..32c58f0 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -653,12 +653,12 @@ class Main(object): # Autosaving if self.autosave_checkbox.get_active(): save.autoSave(self.current_exp, - self.autosavedir_button.get_filename(), + self.autosavedir_button.get_filename().decode('utf-8'), self.autosavename.get_text() ) save.autoPlot(self.current_exp, - self.autosavedir_button.get_filename(), + self.autosavedir_button.get_filename().decode('utf-8'), self.autosavename.get_text() ) -- GitLab From b348121066fa49f8ee00770fc9e9ef9727458fe2 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 16 May 2017 20:31:22 -0400 Subject: [PATCH 62/66] Fix errors in default experiment template setup. --- dstat_interface/experiments/experiment_template.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/experiments/experiment_template.py index 8afe88e..29aa839 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -108,9 +108,7 @@ class Experiment(object): self.time = [datetime.utcnow()] def setup(self): - self.data = OrderedDict( - (('current_voltage', [([],[])])) - ) + self.data = OrderedDict(current_voltage=[([],[])]) self.columns = ['Voltage (mV)', 'Current (A)'] self.plot_format = { @@ -120,7 +118,6 @@ class Experiment(object): } # list of scans, tuple of dimensions, list of data self.line_data = ([], []) - self.columns = ('Voltage (mV)', 'Current (A)') self.plots.append(PlotBox(['current_voltage'])) -- GitLab From 62eef820fb260dc014aec62487cb04d25dd7e754 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 2 Jun 2017 18:00:15 -0400 Subject: [PATCH 63/66] dstat_comm: Flush correct pipe. --- dstat_interface/dstat_comm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index 511b059..b50aa64 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -205,8 +205,8 @@ class SerialConnection(GObject.Object): def flush_data(self): self.assert_connected() - while self.proc_pipe_p.poll() is True: - self.proc_pipe_p.recv() + while self.data_pipe_p.poll() is True: + self.data_pipe_p.recv() def send_ctrl(self, ctrl): self.assert_connected() -- GitLab From 11b6964f6970163a17cced8a868731268a896874 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 2 Jun 2017 18:15:25 -0400 Subject: [PATCH 64/66] Remove old gtk2 code. --- dstat_interface/analysis.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dstat_interface/analysis.py b/dstat_interface/analysis.py index 9526872..3190989 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') -- GitLab From a0ac8ac260ebabd5489b3f9d951a20732ac33ca2 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 2 Jun 2017 18:17:01 -0400 Subject: [PATCH 65/66] cal: fix missing import --- dstat_interface/experiments/cal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/experiments/cal.py b/dstat_interface/experiments/cal.py index 9abdc20..98d82b6 100755 --- a/dstat_interface/experiments/cal.py +++ b/dstat_interface/experiments/cal.py @@ -29,7 +29,7 @@ import serial logger = logging.getLogger("dstat.experiments.cal") import state -from experiments.experiment_template import Experiment +from experiments.experiment_template import Experiment, dstat_logger def measure_offset(time): gain_trim_table = [None, 'r100_trim', 'r3k_trim', 'r30k_trim', 'r300k_trim', -- GitLab From 9216f0259b8281e7123ed8a2ba70f763cd945d55 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 5 Jun 2017 15:00:59 -0400 Subject: [PATCH 66/66] Remove non-existant experiment. --- dstat_interface/interface/exp_int.py | 55 ---------------------------- 1 file changed, 55 deletions(-) diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/interface/exp_int.py index 83ebfac..397fe91 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/interface/exp_int.py @@ -183,61 +183,6 @@ class Chronoamp(ExpInterface): for i in table: self.model.append(i) - -class EIS(ExpInterface): - """Experiment class for EIS.""" - id = 'eis' - experiment = exp.EISExp - def __init__(self): - """Adds entry listings to superclass's self.entry dict""" - super(ExpInterface, self).__init__() # initialize GObject - self.name = "Impedance Spectroscopy" - - self.entry = {} # to be used only for str parameters - self._params = None - - self.window = Gtk.ScrolledWindow() - self.window.set_vexpand(True) - - grid = Gtk.Grid() - grid.set_column_homogeneous(False) - - entries = {'start' : 'Start (Hz)', - 'stop' : 'Stop (Hz)', - 'n_increments' : 'Number of steps', - 'cycles' : 'Number of settling cycles'} - - for n, i in enumerate(entries.items()): - key, value = i - grid.attach(Gtk.Label(label=value), 0, n, 1, 1) - self.entry[key] = Gtk.Entry() - grid.attach(self.entry[key], 1, n, 1, 1) - - buttons = ('') - - self.window.add(grid) - - def get_window(self): - return self.window - - def _fill_params(self): - super(EIS, self)._fill_params() - - self._params['cyclic_true'] = False - - def _get_params(self): - """Updates self._params from UI.""" - super(EIS, self)._get_params() - - self._params['cyclic_true'] = self.builder.get_object( - 'cyclic_checkbutton').get_active() - - def _set_params(self): - """Updates UI with new parameters.""" - super(EIS, self)._set_params() - - self.builder.get_object('cyclic_checkbutton').set_active( - self._params['cyclic_true']) class LSV(ExpInterface): """Experiment class for LSV.""" -- GitLab