From 47f9999feaa382bd0d37babfcd41d09c0a1c9e09 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 6 Jun 2017 17:31:30 -0400 Subject: [PATCH 001/111] Fix version detection to work without git. --- dstat_interface/version.py | 6 ++++-- version.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dstat_interface/version.py b/dstat_interface/version.py index 343f158..ae2f3d2 100644 --- a/dstat_interface/version.py +++ b/dstat_interface/version.py @@ -48,9 +48,11 @@ __all__ = ('getVersion') import re import subprocess import sys +import os.path +import inspect - -RELEASE_VERSION_FILE = 'RELEASE-VERSION' +RELEASE_VERSION_FILE = '{}/RELEASE-VERSION'.format( + os.path.dirname(os.path.abspath(inspect.stack()[0][1]))) # http://www.python.org/dev/peps/pep-0386/ _PEP386_SHORT_VERSION_RE = r'\d+(?:\.\d+)+(?:(?:[abc]|rc)\d+(?:\.\d+)*)?' diff --git a/version.py b/version.py index 343f158..ae2f3d2 100644 --- a/version.py +++ b/version.py @@ -48,9 +48,11 @@ __all__ = ('getVersion') import re import subprocess import sys +import os.path +import inspect - -RELEASE_VERSION_FILE = 'RELEASE-VERSION' +RELEASE_VERSION_FILE = '{}/RELEASE-VERSION'.format( + os.path.dirname(os.path.abspath(inspect.stack()[0][1]))) # http://www.python.org/dev/peps/pep-0386/ _PEP386_SHORT_VERSION_RE = r'\d+(?:\.\d+)+(?:(?:[abc]|rc)\d+(?:\.\d+)*)?' -- GitLab From 5e547d178ea4113d74e66f13015af17070ea6f3b Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 6 Jun 2017 17:50:22 -0400 Subject: [PATCH 002/111] Remove mr_db submodule properly --- dstat_interface/mr_db | 1 - 1 file changed, 1 deletion(-) delete mode 160000 dstat_interface/mr_db diff --git a/dstat_interface/mr_db b/dstat_interface/mr_db deleted file mode 160000 index 72481e5..0000000 --- a/dstat_interface/mr_db +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 72481e585d92d0adab6537718a4d18164df1b445 -- GitLab From e05c5ac81407ca11a7a646a09fb76a5cd7c2fa95 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 7 Jun 2017 15:45:31 -0400 Subject: [PATCH 003/111] Update Linux instructions in README.markdown. --- README.markdown | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.markdown b/README.markdown index e47af15..978120a 100644 --- a/README.markdown +++ b/README.markdown @@ -56,9 +56,16 @@ The final requirements, can be installed using python's pip system: ## Linux Linux prerequisite installation is similar to that of MacOS with Homebrew, only using your distribution's native package manager rather than Homebrew, and X11 will likely be installed already. Some distributions may not have packages available for installing matplotlib or numpy, in which case, they should be installed using pip. -The final requirements, can be installed using python's pip system: +These instructions were tested on Ubuntu 17.04: - pip install pandas pyserial pyzmq pyyaml seaborn zmq-plugin +```` +sudo apt-get install gobject-introspection python-gobject python-pip +pip install dstat-interface +```` + +You will need to add your user to the `dialout` group to access virtual serial ports (replace with your username): + + sudo usermod -a -G dialout ## Windows The following terminal commands will result in a full installation of dstat-interface and its requirements, assuming [64-bit Miniconda][1] is installed: -- GitLab From dc5e1451a2391f4b2925b752699e58b2b98ab621 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 12 Jun 2017 15:17:39 -0400 Subject: [PATCH 004/111] Fix paver stuff to make version detection work and not include unnecessary files. --- MANIFEST.in | 2 ++ dstat_interface/interface_test.spec.bak | 25 ------------------------- 2 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 dstat_interface/interface_test.spec.bak diff --git a/MANIFEST.in b/MANIFEST.in index fefb590..94bb773 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,8 @@ include RELEASE-VERSION include version.py include setup.py include paver-minilib.zip +include dstat_interface/RELEASE-VERSION recursive-include dstat_interface * recursive-exclude dstat_interface *.pyc recursive-exclude dstat_interface *~ +recursive-exclude dstat_interface .* diff --git a/dstat_interface/interface_test.spec.bak b/dstat_interface/interface_test.spec.bak deleted file mode 100644 index 2a51ef2..0000000 --- a/dstat_interface/interface_test.spec.bak +++ /dev/null @@ -1,25 +0,0 @@ -# -*- mode: python -*- -a = Analysis(['interface_test.py'], - hiddenimports=[], - hookspath=None, - runtime_hooks=None) -glade_tree = Tree('./interface', prefix = 'interface', excludes=['*.py','*.pyc']) -drivers_tree = Tree('./drivers', prefix = 'drivers') -pyz = PYZ(a.pure) -exe = EXE(pyz, - a.scripts, - exclude_binaries=True, - name='interface_test.exe', - debug=False, - strip=None, - upx=True, - console=True ) -coll = COLLECT(exe, - drivers_tree, - glade_tree, - a.binaries, - a.zipfiles, - a.datas, - strip=None, - upx=True, - name='interface_test') -- GitLab From 5a4421d94103dbf812f9a012eaa70c309c843765 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 12 Jun 2017 15:38:35 -0400 Subject: [PATCH 005/111] Tweak connection process. --- dstat_interface/dstat_comm.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/dstat_comm.py index b50aa64..ae689a5 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/dstat_comm.py @@ -62,15 +62,6 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): connected = False - try: - ser = serial.Serial(ser_port, timeout=1) - time.sleep(2) - ser.reset_input_buffer() - ser_logger.info("Reattaching DStat udc") - # ser.write("!R") # Send restart command - ser.close() - except serial.SerialException: - return 1 for i in range(5): time.sleep(1) # Give OS time to enumerate -- GitLab From 7b1efc6f1308ced8a27da6022932afcac72cbe9d Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 12 Jun 2017 15:39:02 -0400 Subject: [PATCH 006/111] Increase glitched point removal. --- 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 29aa839..a0e619e 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/experiments/experiment_template.py @@ -299,7 +299,7 @@ class Experiment(object): in subclass. """ try: - if self.datapoint == 0: + if self.datapoint <= 1: return None except AttributeError: # Datapoint counting is optional pass -- GitLab From 8b7c4892a928adb970cd8b2719e72044fcb127dd Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 12 Jun 2017 15:42:08 -0400 Subject: [PATCH 007/111] Make LSV, CV, SWV, and DPV axes always run low to high. Fixes #26 --- dstat_interface/experiments/cv.py | 7 +++---- dstat_interface/experiments/lsv.py | 10 ++++++---- dstat_interface/experiments/swv.py | 16 +++++++++++----- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/dstat_interface/experiments/cv.py b/dstat_interface/experiments/cv.py index fbeda56..c679603 100644 --- a/dstat_interface/experiments/cv.py +++ b/dstat_interface/experiments/cv.py @@ -13,10 +13,9 @@ class CVExp(Experiment): 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.plot_format['current_voltage']['xlims'] = tuple( + sorted((int(self.parameters['v1']), int(self.parameters['v2']))) + ) self.commands += "E" self.commands[2] += "C" diff --git a/dstat_interface/experiments/lsv.py b/dstat_interface/experiments/lsv.py index 9042a5e..e4c7e47 100644 --- a/dstat_interface/experiments/lsv.py +++ b/dstat_interface/experiments/lsv.py @@ -12,10 +12,12 @@ class LSVExp(Experiment): self.datatype = "linearData" self.datalength = 2 self.databytes = 6 # uint16 + int32 - self.plot_format['current_voltage']['xlims'] = ( - int(self.parameters['start']), - int(self.parameters['stop']) - ) + self.plot_format['current_voltage']['xlims'] = tuple( + sorted( + (int(self.parameters['start']), + int(self.parameters['stop'])) + ) + ) self.commands += "E" self.commands[2] += "L" diff --git a/dstat_interface/experiments/swv.py b/dstat_interface/experiments/swv.py index 2f3ba80..91f2ea7 100644 --- a/dstat_interface/experiments/swv.py +++ b/dstat_interface/experiments/swv.py @@ -38,11 +38,14 @@ class SWVExp(Experiment): 'swv' : {'labels' : ('Voltage (mV)', 'Current (A)' ), - 'xlims' : (int(self.parameters['start']), - int(self.parameters['stop'])) + 'xlims' : tuple(sorted( + (int(self.parameters['start']), + int(self.parameters['stop'])) + ) + ) } } - + self.commands += "E" self.commands[2] += "S" self.commands[2] += str(self.parameters['clean_s']) @@ -116,8 +119,11 @@ class DPVExp(SWVExp): 'swv' : {'labels' : ('Voltage (mV)', 'Current (A)' ), - 'xlims' : (int(self.parameters['start']), - int(self.parameters['stop'])) + 'xlims' : tuple(sorted( + (int(self.parameters['start']), + int(self.parameters['stop'])) + ) + ) } } -- GitLab From dd1ae5b62136013d4d49e75643095bc566d369f1 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 12 Jun 2017 15:44:54 -0400 Subject: [PATCH 008/111] Update Changelog. --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 8dd9358..d3d2b5c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +Version 1.4.1 + -Fixed voltage axis orientation for LSV, CV, SWV, and DPV (Thanks to Dan Bizzotto @ UBC) + -Tweaked paver files to make version detection work without git. + Version 1.4 -Switched to GTK+3 -Support new DStat communications protocol (requires dstat-firmware>fe50c38) -- GitLab From 61949a58b54a89a0af5c10658347702db4c33c76 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 12 Jun 2017 22:55:17 -0400 Subject: [PATCH 009/111] Fix pip packaging. --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 94bb773..390abfd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,4 +6,5 @@ include dstat_interface/RELEASE-VERSION recursive-include dstat_interface * recursive-exclude dstat_interface *.pyc recursive-exclude dstat_interface *~ -recursive-exclude dstat_interface .* +exclude dstat_interface/last_params.yml +exclude dstat_interface/.DS_Store \ No newline at end of file -- GitLab From 4279bd069d08a989c38e518f0c02ca364ddcc829 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 13 Jun 2017 20:38:18 -0400 Subject: [PATCH 010/111] Fix massively borked packaged version by refactoring and switching everything to the correct relative imports. Fixes #28 --- MANIFEST.in | 7 +- .../{interface => core}/__init__.py | 0 dstat_interface/{ => core}/analysis.py | 5 +- .../{ => core}/drivers/VirtualSerial.inf | 0 .../{ => core}/dstat-interface.bat | 0 dstat_interface/{ => core}/dstat.spec | 0 dstat_interface/core/dstat/__init__.py | 0 .../{dstat_comm.py => core/dstat/comm.py} | 4 +- dstat_interface/{ => core/dstat}/state.py | 0 dstat_interface/{ => core}/errors.py | 0 dstat_interface/core/experiments/__init__.py | 17 +++ dstat_interface/{ => core}/experiments/cal.py | 10 +- .../{ => core}/experiments/chronoamp.py | 2 +- dstat_interface/{ => core}/experiments/cv.py | 2 +- .../experiments/experiment_template.py | 13 +- .../{ => core}/experiments/idle.py | 2 +- dstat_interface/{ => core}/experiments/lsv.py | 2 +- .../{ => core/experiments}/parameter_test.py | 0 dstat_interface/{ => core}/experiments/pot.py | 2 +- dstat_interface/{ => core}/experiments/swv.py | 2 +- dstat_interface/core/interface/__init__.py | 0 .../{ => core}/interface/acv.glade | 0 .../{ => core}/interface/adc_pot.glade | 0 .../{ => core}/interface/adc_pot.py | 7 +- .../interface/analysis_options.glade | 0 .../{ => core}/interface/calib.glade | 0 .../{ => core}/interface/chronoamp.glade | 0 dstat_interface/{ => core}/interface/cv.glade | 0 .../{ => core}/interface/data_view.py | 0 dstat_interface/{ => core}/interface/db.glade | 0 dstat_interface/{ => core}/interface/db.py | 0 .../{ => core}/interface/dpv.glade | 0 .../{ => core}/interface/dstatinterface.glade | 0 .../{ => core}/interface/exp_int.py | 56 +++++---- .../{ => core}/interface/exp_window.py | 8 +- .../{ => core}/interface/lsv.glade | 0 dstat_interface/{ => core}/interface/pd.glade | 0 dstat_interface/{ => core/interface}/plot.py | 0 .../{ => core}/interface/plot_ui.py | 0 .../{ => core}/interface/potexp.glade | 0 dstat_interface/{ => core}/interface/save.py | 10 +- .../{ => core}/interface/swv.glade | 0 dstat_interface/{ => core}/microdrop.py | 0 dstat_interface/{ => core}/params.py | 0 dstat_interface/{ => core}/plugin.py | 0 dstat_interface/core/utils/__init__.py | 0 dstat_interface/{ => core/utils}/version.py | 0 dstat_interface/experiments/__init__.py | 14 --- dstat_interface/main.py | 115 +++++++++--------- pavement.py | 4 +- 50 files changed, 143 insertions(+), 139 deletions(-) rename dstat_interface/{interface => core}/__init__.py (100%) rename dstat_interface/{ => core}/analysis.py (96%) rename dstat_interface/{ => core}/drivers/VirtualSerial.inf (100%) rename dstat_interface/{ => core}/dstat-interface.bat (100%) rename dstat_interface/{ => core}/dstat.spec (100%) create mode 100644 dstat_interface/core/dstat/__init__.py rename dstat_interface/{dstat_comm.py => core/dstat/comm.py} (99%) rename dstat_interface/{ => core/dstat}/state.py (100%) rename dstat_interface/{ => core}/errors.py (100%) create mode 100644 dstat_interface/core/experiments/__init__.py rename dstat_interface/{ => core}/experiments/cal.py (96%) rename dstat_interface/{ => core}/experiments/chronoamp.py (97%) rename dstat_interface/{ => core}/experiments/cv.py (94%) rename dstat_interface/{ => core}/experiments/experiment_template.py (98%) rename dstat_interface/{ => core}/experiments/idle.py (91%) rename dstat_interface/{ => core}/experiments/lsv.py (93%) rename dstat_interface/{ => core/experiments}/parameter_test.py (100%) rename dstat_interface/{ => core}/experiments/pot.py (95%) rename dstat_interface/{ => core}/experiments/swv.py (98%) create mode 100644 dstat_interface/core/interface/__init__.py rename dstat_interface/{ => core}/interface/acv.glade (100%) rename dstat_interface/{ => core}/interface/adc_pot.glade (100%) rename dstat_interface/{ => core}/interface/adc_pot.py (96%) rename dstat_interface/{ => core}/interface/analysis_options.glade (100%) rename dstat_interface/{ => core}/interface/calib.glade (100%) rename dstat_interface/{ => core}/interface/chronoamp.glade (100%) rename dstat_interface/{ => core}/interface/cv.glade (100%) rename dstat_interface/{ => core}/interface/data_view.py (100%) rename dstat_interface/{ => core}/interface/db.glade (100%) rename dstat_interface/{ => core}/interface/db.py (100%) rename dstat_interface/{ => core}/interface/dpv.glade (100%) rename dstat_interface/{ => core}/interface/dstatinterface.glade (100%) rename dstat_interface/{ => core}/interface/exp_int.py (93%) rename dstat_interface/{ => core}/interface/exp_window.py (93%) rename dstat_interface/{ => core}/interface/lsv.glade (100%) rename dstat_interface/{ => core}/interface/pd.glade (100%) rename dstat_interface/{ => core/interface}/plot.py (100%) rename dstat_interface/{ => core}/interface/plot_ui.py (100%) rename dstat_interface/{ => core}/interface/potexp.glade (100%) rename dstat_interface/{ => core}/interface/save.py (98%) rename dstat_interface/{ => core}/interface/swv.glade (100%) rename dstat_interface/{ => core}/microdrop.py (100%) rename dstat_interface/{ => core}/params.py (100%) rename dstat_interface/{ => core}/plugin.py (100%) create mode 100644 dstat_interface/core/utils/__init__.py rename dstat_interface/{ => core/utils}/version.py (100%) delete mode 100644 dstat_interface/experiments/__init__.py diff --git a/MANIFEST.in b/MANIFEST.in index 390abfd..af0474d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,11 @@ include RELEASE-VERSION include version.py include setup.py +include main.py include paver-minilib.zip -include dstat_interface/RELEASE-VERSION +include core/utils/RELEASE-VERSION recursive-include dstat_interface * recursive-exclude dstat_interface *.pyc recursive-exclude dstat_interface *~ -exclude dstat_interface/last_params.yml -exclude dstat_interface/.DS_Store \ No newline at end of file +exclude last_params.yml +recursive-exclude . .DS_Store \ No newline at end of file diff --git a/dstat_interface/interface/__init__.py b/dstat_interface/core/__init__.py similarity index 100% rename from dstat_interface/interface/__init__.py rename to dstat_interface/core/__init__.py diff --git a/dstat_interface/analysis.py b/dstat_interface/core/analysis.py similarity index 96% rename from dstat_interface/analysis.py rename to dstat_interface/core/analysis.py index 3190989..76c1be0 100755 --- a/dstat_interface/analysis.py +++ b/dstat_interface/core/analysis.py @@ -21,16 +21,19 @@ Functions for analyzing data. """ import logging +import os from numpy import mean, trapz logger = logging.getLogger('dstat.analysis') +mod_dir = os.path.dirname(os.path.abspath(__file__)) class AnalysisOptions(object): """Analysis options window.""" def __init__(self, builder): self.builder = builder - self.builder.add_from_file('interface/analysis_options.glade') + self.builder.add_from_file( + os.path.join(mod_dir, 'interface/analysis_options.glade')) self.builder.connect_signals(self) self.window = self.builder.get_object('analysis_dialog') diff --git a/dstat_interface/drivers/VirtualSerial.inf b/dstat_interface/core/drivers/VirtualSerial.inf similarity index 100% rename from dstat_interface/drivers/VirtualSerial.inf rename to dstat_interface/core/drivers/VirtualSerial.inf diff --git a/dstat_interface/dstat-interface.bat b/dstat_interface/core/dstat-interface.bat similarity index 100% rename from dstat_interface/dstat-interface.bat rename to dstat_interface/core/dstat-interface.bat diff --git a/dstat_interface/dstat.spec b/dstat_interface/core/dstat.spec similarity index 100% rename from dstat_interface/dstat.spec rename to dstat_interface/core/dstat.spec diff --git a/dstat_interface/core/dstat/__init__.py b/dstat_interface/core/dstat/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dstat_interface/dstat_comm.py b/dstat_interface/core/dstat/comm.py similarity index 99% rename from dstat_interface/dstat_comm.py rename to dstat_interface/core/dstat/comm.py index ae689a5..9395d92 100755 --- a/dstat_interface/dstat_comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -33,13 +33,13 @@ except ImportError: print "ERR: GTK not available" sys.exit(1) -from errors import InputError, VarError +from ..errors import InputError, VarError logger = logging.getLogger("dstat.comm") dstat_logger = logging.getLogger("dstat.comm.DSTAT") exp_logger = logging.getLogger("dstat.comm.Experiment") -import state +from . import state class AlreadyConnectedError(Exception): def __init__(self): diff --git a/dstat_interface/state.py b/dstat_interface/core/dstat/state.py similarity index 100% rename from dstat_interface/state.py rename to dstat_interface/core/dstat/state.py diff --git a/dstat_interface/errors.py b/dstat_interface/core/errors.py similarity index 100% rename from dstat_interface/errors.py rename to dstat_interface/core/errors.py diff --git a/dstat_interface/core/experiments/__init__.py b/dstat_interface/core/experiments/__init__.py new file mode 100644 index 0000000..6991a97 --- /dev/null +++ b/dstat_interface/core/experiments/__init__.py @@ -0,0 +1,17 @@ +# __all__ = [] +# +# import pkgutil +# import inspect +# from . import cal, chronoamp, cv +# +# +# for loader, name, is_pkg in pkgutil.walk_packages(__path__): +# print loader, name, is_pkg +# module = loader.find_module(name).load_module(name) +# +# for name, value in inspect.getmembers(module): +# if name.startswith('__'): +# continue +# +# globals()[name] = value +# __all__.append(name) \ No newline at end of file diff --git a/dstat_interface/experiments/cal.py b/dstat_interface/core/experiments/cal.py similarity index 96% rename from dstat_interface/experiments/cal.py rename to dstat_interface/core/experiments/cal.py index 98d82b6..1cd3721 100755 --- a/dstat_interface/experiments/cal.py +++ b/dstat_interface/core/experiments/cal.py @@ -21,15 +21,13 @@ import time import struct import logging - -from errors import InputError, VarError +logger = logging.getLogger("dstat.experiments.cal") import serial -logger = logging.getLogger("dstat.experiments.cal") - -import state -from experiments.experiment_template import Experiment, dstat_logger +from ..errors import InputError, VarError +from ..dstat import state +from ..experiments.experiment_template import Experiment, dstat_logger def measure_offset(time): gain_trim_table = [None, 'r100_trim', 'r3k_trim', 'r30k_trim', 'r300k_trim', diff --git a/dstat_interface/experiments/chronoamp.py b/dstat_interface/core/experiments/chronoamp.py similarity index 97% rename from dstat_interface/experiments/chronoamp.py rename to dstat_interface/core/experiments/chronoamp.py index f2fefa1..eae24dd 100644 --- a/dstat_interface/experiments/chronoamp.py +++ b/dstat_interface/core/experiments/chronoamp.py @@ -3,7 +3,7 @@ import struct import numpy as np import serial -from experiments.experiment_template import PlotBox, Experiment, exp_logger +from .experiment_template import PlotBox, Experiment, exp_logger class ChronoampBox(PlotBox): def format_plots(self): diff --git a/dstat_interface/experiments/cv.py b/dstat_interface/core/experiments/cv.py similarity index 94% rename from dstat_interface/experiments/cv.py rename to dstat_interface/core/experiments/cv.py index c679603..475f11b 100644 --- a/dstat_interface/experiments/cv.py +++ b/dstat_interface/core/experiments/cv.py @@ -1,7 +1,7 @@ import time import struct -from experiments.experiment_template import PlotBox, Experiment +from .experiment_template import PlotBox, Experiment class CVExp(Experiment): id = 'cve' diff --git a/dstat_interface/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py similarity index 98% rename from dstat_interface/experiments/experiment_template.py rename to dstat_interface/core/experiments/experiment_template.py index a0e619e..495471b 100755 --- a/dstat_interface/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -41,20 +41,17 @@ from matplotlib.backends.backend_gtk3agg \ import FigureCanvasGTK3Agg as FigureCanvas from pandas import DataFrame - -try: - import seaborn as sns - sns.set(context='paper', style='darkgrid') -except ImportError: - pass +import seaborn as sns +sns.set(context='paper', style='darkgrid') + import serial logger = logging.getLogger("dstat.comm") dstat_logger = logging.getLogger("dstat.comm.DSTAT") exp_logger = logging.getLogger("dstat.comm.Experiment") -from errors import InputError, VarError -import state +from ..errors import InputError, VarError +from ..dstat import state class Experiment(object): """Store and acquire a potentiostat experiment. Meant to be subclassed diff --git a/dstat_interface/experiments/idle.py b/dstat_interface/core/experiments/idle.py similarity index 91% rename from dstat_interface/experiments/idle.py rename to dstat_interface/core/experiments/idle.py index 23e871b..013ac4c 100644 --- a/dstat_interface/experiments/idle.py +++ b/dstat_interface/core/experiments/idle.py @@ -1,7 +1,7 @@ import time import struct -from experiments.experiment_template import Experiment +from .experiment_template import Experiment class OCPExp(Experiment): """Open circuit potential measumement in statusbar.""" diff --git a/dstat_interface/experiments/lsv.py b/dstat_interface/core/experiments/lsv.py similarity index 93% rename from dstat_interface/experiments/lsv.py rename to dstat_interface/core/experiments/lsv.py index e4c7e47..c494e67 100644 --- a/dstat_interface/experiments/lsv.py +++ b/dstat_interface/core/experiments/lsv.py @@ -1,7 +1,7 @@ import time import struct -from experiments.experiment_template import PlotBox, Experiment +from .experiment_template import PlotBox, Experiment class LSVExp(Experiment): """Linear Scan Voltammetry experiment""" diff --git a/dstat_interface/parameter_test.py b/dstat_interface/core/experiments/parameter_test.py similarity index 100% rename from dstat_interface/parameter_test.py rename to dstat_interface/core/experiments/parameter_test.py diff --git a/dstat_interface/experiments/pot.py b/dstat_interface/core/experiments/pot.py similarity index 95% rename from dstat_interface/experiments/pot.py rename to dstat_interface/core/experiments/pot.py index 2859e46..a14c928 100644 --- a/dstat_interface/experiments/pot.py +++ b/dstat_interface/core/experiments/pot.py @@ -1,7 +1,7 @@ import time import struct -from experiments.experiment_template import PlotBox, Experiment +from .experiment_template import PlotBox, Experiment class PotBox(PlotBox): def format_plots(self): diff --git a/dstat_interface/experiments/swv.py b/dstat_interface/core/experiments/swv.py similarity index 98% rename from dstat_interface/experiments/swv.py rename to dstat_interface/core/experiments/swv.py index 91f2ea7..b9b4f40 100644 --- a/dstat_interface/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -2,7 +2,7 @@ import time import struct from copy import deepcopy -from experiments.experiment_template import PlotBox, Experiment +from .experiment_template import PlotBox, Experiment class SWVBox(PlotBox): def format_plots(self): diff --git a/dstat_interface/core/interface/__init__.py b/dstat_interface/core/interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dstat_interface/interface/acv.glade b/dstat_interface/core/interface/acv.glade similarity index 100% rename from dstat_interface/interface/acv.glade rename to dstat_interface/core/interface/acv.glade diff --git a/dstat_interface/interface/adc_pot.glade b/dstat_interface/core/interface/adc_pot.glade similarity index 100% rename from dstat_interface/interface/adc_pot.glade rename to dstat_interface/core/interface/adc_pot.glade diff --git a/dstat_interface/interface/adc_pot.py b/dstat_interface/core/interface/adc_pot.py similarity index 96% rename from dstat_interface/interface/adc_pot.py rename to dstat_interface/core/interface/adc_pot.py index 8e73162..2624946 100755 --- a/dstat_interface/interface/adc_pot.py +++ b/dstat_interface/core/interface/adc_pot.py @@ -17,6 +17,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path try: import gi @@ -26,7 +27,9 @@ except ImportError: print "ERR: GTK not available" sys.exit(1) -from errors import InputError, VarError +from ..errors import InputError, VarError + +mod_dir = os.path.dirname(os.path.abspath(__file__)) v1_1_gain = [(0, "100 Ω (15 mA FS)", "0"), (1, "300 Ω (5 mA FS)", "1"), @@ -50,7 +53,7 @@ v1_2_gain = [(0, "Bypass", "0"), class adc_pot(object): def __init__(self): self.builder = Gtk.Builder() - self.builder.add_from_file('interface/adc_pot.glade') + self.builder.add_from_file(os.path.join(mod_dir,'adc_pot.glade')) self.builder.connect_signals(self) self.cell = Gtk.CellRendererText() diff --git a/dstat_interface/interface/analysis_options.glade b/dstat_interface/core/interface/analysis_options.glade similarity index 100% rename from dstat_interface/interface/analysis_options.glade rename to dstat_interface/core/interface/analysis_options.glade diff --git a/dstat_interface/interface/calib.glade b/dstat_interface/core/interface/calib.glade similarity index 100% rename from dstat_interface/interface/calib.glade rename to dstat_interface/core/interface/calib.glade diff --git a/dstat_interface/interface/chronoamp.glade b/dstat_interface/core/interface/chronoamp.glade similarity index 100% rename from dstat_interface/interface/chronoamp.glade rename to dstat_interface/core/interface/chronoamp.glade diff --git a/dstat_interface/interface/cv.glade b/dstat_interface/core/interface/cv.glade similarity index 100% rename from dstat_interface/interface/cv.glade rename to dstat_interface/core/interface/cv.glade diff --git a/dstat_interface/interface/data_view.py b/dstat_interface/core/interface/data_view.py similarity index 100% rename from dstat_interface/interface/data_view.py rename to dstat_interface/core/interface/data_view.py diff --git a/dstat_interface/interface/db.glade b/dstat_interface/core/interface/db.glade similarity index 100% rename from dstat_interface/interface/db.glade rename to dstat_interface/core/interface/db.glade diff --git a/dstat_interface/interface/db.py b/dstat_interface/core/interface/db.py similarity index 100% rename from dstat_interface/interface/db.py rename to dstat_interface/core/interface/db.py diff --git a/dstat_interface/interface/dpv.glade b/dstat_interface/core/interface/dpv.glade similarity index 100% rename from dstat_interface/interface/dpv.glade rename to dstat_interface/core/interface/dpv.glade diff --git a/dstat_interface/interface/dstatinterface.glade b/dstat_interface/core/interface/dstatinterface.glade similarity index 100% rename from dstat_interface/interface/dstatinterface.glade rename to dstat_interface/core/interface/dstatinterface.glade diff --git a/dstat_interface/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py similarity index 93% rename from dstat_interface/interface/exp_int.py rename to dstat_interface/core/interface/exp_int.py index 397fe91..9389745 100755 --- a/dstat_interface/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -30,14 +30,16 @@ except ImportError: print("ERR: GTK not available") sys.exit(1) -import dstat_comm -import state -import experiments as exp -import experiments.cal as cal +from ..dstat import comm, state +from ..experiments import (cal, chronoamp, cv, experiment_template, + idle, lsv, pot, swv) import __main__ -from errors import InputError, VarError +from ..errors import InputError, VarError + logger = logging.getLogger("dstat.interface.exp_int") +mod_dir = os.path.dirname(os.path.abspath(__file__)) + class ExpInterface(GObject.Object): """Generic experiment interface class. Should be subclassed to implement experiment interfaces by populating self.entry. Override class attributes @@ -108,10 +110,10 @@ class Chronoamp(ExpInterface): get_params(self) """ id = 'cae' - experiment = exp.Chronoamp + experiment = chronoamp.Chronoamp def __init__(self): """Extends superclass method to support treeview.""" - super(Chronoamp, self).__init__('interface/chronoamp.glade') + super(Chronoamp, self).__init__(os.path.join(mod_dir, 'chronoamp.glade')) self.name = "Chronoamperometry" @@ -187,10 +189,10 @@ class Chronoamp(ExpInterface): class LSV(ExpInterface): """Experiment class for LSV.""" id = 'lsv' - experiment = exp.LSVExp + experiment = lsv.LSVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(LSV, self).__init__('interface/lsv.glade') + super(LSV, self).__init__(os.path.join(mod_dir, 'lsv.glade')) self.name = "Linear Sweep Voltammetry" self.entry['clean_mV'] = self.builder.get_object('clean_mV') @@ -204,10 +206,10 @@ class LSV(ExpInterface): class CV(ExpInterface): """Experiment class for CV.""" id = 'cve' - experiment = exp.CVExp + experiment = cv.CVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(CV, self).__init__('interface/cv.glade') + super(CV, self).__init__(os.path.join(mod_dir, 'cv.glade')) self.name = "Cyclic Voltammetry" self.entry['clean_mV'] = self.builder.get_object('clean_mV') @@ -223,10 +225,10 @@ class CV(ExpInterface): class SWV(ExpInterface): """Experiment class for SWV.""" id = 'swv' - experiment = exp.SWVExp + experiment = swv.SWVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(SWV, self).__init__('interface/swv.glade') + super(SWV, self).__init__(os.path.join(mod_dir, 'swv.glade')) self.name = "Square Wave Voltammetry" self.entry['clean_mV'] = self.builder.get_object('clean_mV') @@ -262,10 +264,10 @@ class SWV(ExpInterface): class DPV(ExpInterface): """Experiment class for DPV.""" id = 'dpv' - experiment = exp.DPVExp + experiment = swv.DPVExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(DPV, self).__init__('interface/dpv.glade') + super(DPV, self).__init__(os.path.join(mod_dir, 'dpv.glade')) self.name = "Differential Pulse Voltammetry" @@ -297,10 +299,10 @@ class DPV(ExpInterface): class PD(ExpInterface): """Experiment class for PD.""" id = 'pde' - experiment = exp.PDExp + experiment = chronoamp.PDExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(PD, self).__init__('interface/pd.glade') + super(PD, self).__init__(os.path.join(mod_dir, 'pd.glade')) self.name = "Photodiode/PMT" @@ -363,9 +365,9 @@ class PD(ExpInterface): try: self.builder.get_object('light_label').set_text(str( dstat_comm.read_light_sensor())) - dstat_comm.read_settings() + comm.read_settings() state.settings['tcs_enabled'][1] = '1' # Make sure TCS enabled - dstat_comm.write_settings() + comm.write_settings() self.builder.get_object('threshold_entry').set_text(str( state.settings['tcs_clear_threshold'][1])) @@ -384,8 +386,8 @@ class PD(ExpInterface): try: state.settings['tcs_clear_threshold'][1] = self.builder.get_object( 'threshold_entry').get_text() - dstat_comm.write_settings() - dstat_comm.read_settings() + comm.write_settings() + comm.read_settings() self.builder.get_object('threshold_entry').set_text( str(state.settings['tcs_clear_threshold'][1])) __main__.MAIN.start_ocp() @@ -405,10 +407,10 @@ class PD(ExpInterface): class POT(ExpInterface): """Experiment class for Potentiometry.""" id = 'pot' - experiment = exp.PotExp + experiment = pot.PotExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(POT, self).__init__('interface/potexp.glade') + super(POT, self).__init__(os.path.join(mod_dir, 'potexp.glade')) self.name = "Potentiometry" self.entry['time'] = self.builder.get_object('time_entry') @@ -416,10 +418,10 @@ class POT(ExpInterface): class CAL(ExpInterface): """Experiment class for Calibrating gain.""" id = 'cal' - experiment = exp.CALExp + experiment = cal.CALExp def __init__(self): """Adds entry listings to superclass's self.entry dict""" - super(CAL, self).__init__('interface/calib.glade') + super(CAL, self).__init__(os.path.join(mod_dir, 'calib.glade')) self.name = "Calilbration" self.entry['time'] = self.builder.get_object('time_entry') @@ -442,7 +444,7 @@ class CAL(ExpInterface): try: __main__.MAIN.on_pot_stop_clicked() __main__.MAIN.stop_ocp() - dstat_comm.read_settings() + comm.read_settings() self.entry['R100'].set_text(str( state.settings['r100_trim'][1])) @@ -479,7 +481,7 @@ class CAL(ExpInterface): 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() + comm.write_settings() __main__.MAIN.start_ocp() diff --git a/dstat_interface/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py similarity index 93% rename from dstat_interface/interface/exp_window.py rename to dstat_interface/core/interface/exp_window.py index 413d43a..79c16ed 100755 --- a/dstat_interface/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -29,7 +29,7 @@ except ImportError: print "ERR: GTK not available" sys.exit(1) -import interface.exp_int as exp +from . import exp_int logger = logging.getLogger("dstat.interface.exp_window") @@ -46,9 +46,9 @@ class Experiments(GObject.Object): # (experiment index in UI, experiment) 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 + for _, c in inspect.getmembers(exp_int, inspect.isclass) + if issubclass(c, exp_int.ExpInterface) + and c is not exp_int.ExpInterface } self.classes = OrderedDict(sorted(classes.items())) diff --git a/dstat_interface/interface/lsv.glade b/dstat_interface/core/interface/lsv.glade similarity index 100% rename from dstat_interface/interface/lsv.glade rename to dstat_interface/core/interface/lsv.glade diff --git a/dstat_interface/interface/pd.glade b/dstat_interface/core/interface/pd.glade similarity index 100% rename from dstat_interface/interface/pd.glade rename to dstat_interface/core/interface/pd.glade diff --git a/dstat_interface/plot.py b/dstat_interface/core/interface/plot.py similarity index 100% rename from dstat_interface/plot.py rename to dstat_interface/core/interface/plot.py diff --git a/dstat_interface/interface/plot_ui.py b/dstat_interface/core/interface/plot_ui.py similarity index 100% rename from dstat_interface/interface/plot_ui.py rename to dstat_interface/core/interface/plot_ui.py diff --git a/dstat_interface/interface/potexp.glade b/dstat_interface/core/interface/potexp.glade similarity index 100% rename from dstat_interface/interface/potexp.glade rename to dstat_interface/core/interface/potexp.glade diff --git a/dstat_interface/interface/save.py b/dstat_interface/core/interface/save.py similarity index 98% rename from dstat_interface/interface/save.py rename to dstat_interface/core/interface/save.py index 76a714e..850ed0b 100755 --- a/dstat_interface/interface/save.py +++ b/dstat_interface/core/interface/save.py @@ -22,6 +22,9 @@ from __future__ import division, absolute_import, print_function, unicode_litera import io import os +import logging +logger = logging.getLogger("dstat.interface.save") + try: import gi @@ -31,12 +34,9 @@ except ImportError: print("ERR: GTK not available") sys.exit(1) import numpy as np -import logging - -logger = logging.getLogger("dstat.interface.save") -from errors import InputError, VarError -from params import save_params, load_params +from ..errors import InputError, VarError +from ..params import save_params, load_params def manSave(current_exp): fcd = Gtk.FileChooserDialog("Save…", None, Gtk.FileChooserAction.SAVE, diff --git a/dstat_interface/interface/swv.glade b/dstat_interface/core/interface/swv.glade similarity index 100% rename from dstat_interface/interface/swv.glade rename to dstat_interface/core/interface/swv.glade diff --git a/dstat_interface/microdrop.py b/dstat_interface/core/microdrop.py similarity index 100% rename from dstat_interface/microdrop.py rename to dstat_interface/core/microdrop.py diff --git a/dstat_interface/params.py b/dstat_interface/core/params.py similarity index 100% rename from dstat_interface/params.py rename to dstat_interface/core/params.py diff --git a/dstat_interface/plugin.py b/dstat_interface/core/plugin.py similarity index 100% rename from dstat_interface/plugin.py rename to dstat_interface/core/plugin.py diff --git a/dstat_interface/core/utils/__init__.py b/dstat_interface/core/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dstat_interface/version.py b/dstat_interface/core/utils/version.py similarity index 100% rename from dstat_interface/version.py rename to dstat_interface/core/utils/version.py diff --git a/dstat_interface/experiments/__init__.py b/dstat_interface/experiments/__init__.py deleted file mode 100644 index b1fb5ae..0000000 --- a/dstat_interface/experiments/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -__all__ = [] - -import pkgutil -import inspect - -for loader, name, is_pkg in pkgutil.walk_packages(__path__): - module = loader.find_module(name).load_module(name) - - for name, value in inspect.getmembers(module): - if name.startswith('__'): - continue - - globals()[name] = value - __all__.append(name) \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 32c58f0..9564645 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -27,13 +27,8 @@ import uuid from copy import deepcopy from collections import OrderedDict from datetime import datetime +import logging -# try: -# import pygtk -# pygtk.require('2.0') -# except ImportError: -# print "ERR: PyGTK 2.0 not available" -# sys.exit(1) try: import gi gi.require_version('Gtk', '3.0') @@ -43,26 +38,18 @@ except ImportError: sys.exit(1) from serial import SerialException -import logging -os.chdir(os.path.dirname(os.path.abspath(sys.argv[0]))) - -from version import getVersion -import interface.save as save -import dstat_comm as comm -import experiments as exp -import interface.exp_window as exp_window -import interface.adc_pot as adc_pot -import interface.plot_ui -import interface.data_view -import plot -import params -import parameter_test -import analysis + import zmq -import state -from errors import InputError -from plugin import DstatPlugin, get_hub_uri +from core.utils.version import getVersion +import core.dstat as dstat +from core.experiments import idle, pot +from core import params, analysis +from core.interface import exp_window, adc_pot, plot_ui, data_view, save +from core.errors import InputError +from core.plugin import DstatPlugin, get_hub_uri + +mod_dir = os.path.dirname(os.path.abspath(__file__)) # Setup Logging root_logger = logging.getLogger("dstat") @@ -81,7 +68,8 @@ class Main(object): """Main program """ def __init__(self): self.builder = Gtk.Builder() - self.builder.add_from_file('interface/dstatinterface.glade') + self.builder.add_from_file( + os.path.join(mod_dir, 'core/interface/dstatinterface.glade')) self.builder.connect_signals(self) self.cell = Gtk.CellRendererText() @@ -111,8 +99,8 @@ 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) + self.data_view = data_view.DataPage(self.main_notebook) + self.info_page = data_view.InfoPage(self.main_notebook) #fill adc_pot_box self.adc_pot_box = self.builder.get_object('gain_adc_box') @@ -129,7 +117,7 @@ class Main(object): self.serial_combobox.add_attribute(self.cell, 'text', 0) self.serial_liststore = self.builder.get_object('serial_liststore') - self.serial_devices = comm.SerialDevices() + self.serial_devices = dstat.comm.SerialDevices() for i in self.serial_devices.ports: self.serial_liststore.append([i]) @@ -213,25 +201,30 @@ class Main(object): try: self.serial_connect.set_sensitive(False) - state.dstat_version = comm.version_check(self.serial_liststore.get_value( - self.serial_combobox.get_active_iter(), 0)) + dstat.state.dstat_version = dstat.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(state.dstat_version) == 2: + if not len(dstat.state.dstat_version) == 2: self.statusbar.push(self.error_context_id, "Communication Error") return else: - self.adc_pot.set_version(state.dstat_version) - self.statusbar.push(self.error_context_id, - "".join(["DStat version: ", - str(state.dstat_version[0]), - ".", str(state.dstat_version[1])]) - ) + self.adc_pot.set_version(dstat.state.dstat_version) + self.statusbar.push( + self.error_context_id, + "DStat version: {}.{}".format( + str(dstat.state.dstat_version[0]), + str(dstat.state.dstat_version[1]) + ) + ) - comm.read_settings() + dstat.comm.read_settings() self.start_ocp() self.connected = True @@ -262,8 +255,8 @@ class Main(object): self.stop_ocp() else: self.on_pot_stop_clicked() - state.ser.send_ctrl("DISCONNECT") - state.ser.disconnect() + dstat.state.ser.send_ctrl("DISCONNECT") + dstat.state.ser.disconnect() except AttributeError as err: logger.warning("AttributeError: %s", err) @@ -286,17 +279,19 @@ class Main(object): def start_ocp(self, data=None): """Start OCP measurements.""" - if state.dstat_version[0] >= 1 and state.dstat_version[1] >= 2: + if (dstat.state.dstat_version[0] >= 1 and + dstat.state.dstat_version[1] >= 2): + # Flush data pipe - state.ser.flush_data() + dstat.state.ser.flush_data() if self.pmt_mode == True: logger.info("Start PMT idle mode") - state.ser.start_exp(exp.PMTIdle()) + dstat.state.ser.start_exp(idle.PMTIdle()) else: logger.info("Start OCP") - state.ser.start_exp(exp.OCPExp()) + dstat.state.ser.start_exp(idle.OCPExp()) self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data), GObject.timeout_add(250, self.ocp_running_proc) @@ -310,12 +305,14 @@ class Main(object): def stop_ocp(self, data=None): """Stop OCP measurements.""" - if state.dstat_version[0] >= 1 and state.dstat_version[1] >= 2: + if (dstat.state.dstat_version[0] >= 1 and + dstat.state.dstat_version[1] >= 2): + if self.pmt_mode == True: logger.info("Stop PMT idle mode") else: logger.info("Stop OCP") - state.ser.send_ctrl('a') + dstat.state.ser.send_ctrl('a') for i in self.ocp_proc: GObject.source_remove(i) @@ -337,7 +334,7 @@ class Main(object): """ try: - incoming = state.ser.get_data() + incoming = dstat.state.ser.get_data() while incoming is not None: if isinstance(incoming, basestring): # test if incoming is str self.on_serial_disconnect_clicked() @@ -351,7 +348,7 @@ class Main(object): except ValueError: pass - incoming = state.ser.get_data() + incoming = dstat.state.ser.get_data() return True @@ -370,16 +367,16 @@ class Main(object): """ try: - proc_buffer = state.ser.get_proc() + proc_buffer = dstat.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() - state.ser.flush_data() + dstat.state.ser.flush_data() return False - proc_buffer = state.ser.get_proc() + proc_buffer = dstat.state.ser.get_proc() return True except EOFError: @@ -417,7 +414,7 @@ class Main(object): self.stop_ocp() self.statusbar.remove_all(self.error_context_id) - state.ser.flush_data() + dstat.state.ser.flush_data() parameters = {} parameters['metadata'] = self.metadata @@ -442,7 +439,7 @@ class Main(object): self.current_exp = self.exp_window.setup_exp(parameters) - interface.plot_ui.replace_notebook_exp( + plot_ui.replace_notebook_exp( self.plot_notebook, self.current_exp, self.window ) @@ -452,10 +449,10 @@ class Main(object): self.data_view.clear_exps() self.info_page.clear() - state.ser.start_exp(self.current_exp) + dstat.state.ser.start_exp(self.current_exp) # Flush data pipe - state.ser.flush_data() + dstat.state.ser.flush_data() self.plot_proc = GObject.timeout_add(200, self.experiment_running_plot) @@ -512,7 +509,7 @@ class Main(object): function from GTK's queue. """ try: - incoming = state.ser.get_data() + incoming = dstat.state.ser.get_data() while incoming is not None: try: self.line, data = incoming @@ -524,7 +521,7 @@ class Main(object): self.current_exp.store_data(incoming, newline) except TypeError: pass - incoming = state.ser.get_data() + incoming = dstat.state.ser.get_data() return True except EOFError as err: @@ -546,7 +543,7 @@ class Main(object): function from GTK's queue. """ try: - proc_buffer = state.ser.get_proc() + proc_buffer = dstat.state.ser.get_proc() if proc_buffer is not None: if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: self.experiment_done() @@ -678,7 +675,7 @@ class Main(object): def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" try: - state.ser.stop_exp() + dstat.state.ser.stop_exp() except AttributeError: pass diff --git a/pavement.py b/pavement.py index 0aea7f5..710bd07 100644 --- a/pavement.py +++ b/pavement.py @@ -1,7 +1,7 @@ import sys from paver.easy import task, needs, path, sh, cmdopts, options -from paver.setuputils import setup, install_distutils_tasks +from paver.setuputils import setup, install_distutils_tasks, find_packages from distutils.extension import Extension from distutils.dep_util import newer @@ -16,7 +16,7 @@ setup(name='dstat_interface', author_email='mdryden@chemutoronto.ca', url='http://microfluidics.utoronto.ca/dstat', license='GPLv3', - packages=['dstat_interface', ], + packages=find_packages(), install_requires=['matplotlib', 'numpy', 'pyserial', 'pyzmq', 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2'], # Install data listed in `MANIFEST.in` -- GitLab From 89a7c0780049c343427894a574a423d928b3e3f4 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 13 Jun 2017 20:39:31 -0400 Subject: [PATCH 011/111] Update changelog. --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index d3d2b5c..6191384 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +Version 1.4.2 + -Refactor to fix critical bug preventing running packaged versions. Version 1.4.1 -Fixed voltage axis orientation for LSV, CV, SWV, and DPV (Thanks to Dan Bizzotto @ UBC) -Tweaked paver files to make version detection work without git. -- GitLab From ae6cf112ccd18d09edd827e34ddd9d6dfcedb2ea Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 14:32:45 -0400 Subject: [PATCH 012/111] Set module search path when doing fork emulation on Windows. Fixes #29 --- dstat_interface/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 9564645..d2f38a2 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -51,6 +51,9 @@ from core.plugin import DstatPlugin, get_hub_uri mod_dir = os.path.dirname(os.path.abspath(__file__)) +if __name__ == "__parents_main__": # Only runs for forking emulation on win + sys.path.append(mod_dir) + # Setup Logging root_logger = logging.getLogger("dstat") root_logger.setLevel(level=logging.INFO) -- GitLab From 48ac580e80aea8217bc81891881275403f7dc3ee Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 14:52:42 -0400 Subject: [PATCH 013/111] Except error when quitting without having connected --- dstat_interface/main.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index d2f38a2..cbcf0db 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -177,9 +177,13 @@ class Main(object): def quit(self): """Disconnect and save parameters on quit.""" - params.save_params(self, 'last_params.yml') + try: + params.save_params(self, 'last_params.yml') - self.on_serial_disconnect_clicked() + self.on_serial_disconnect_clicked() + except KeyError: + pass + mainloop.quit() def on_gtk_about_activate(self, menuitem, data=None): -- GitLab From 835055d25afbdb82dbe33178ebb58eda6858f3b6 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 15:04:10 -0400 Subject: [PATCH 014/111] Update changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 6191384..a75cca2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +Version 1.4.3 + -Fix another critical bug with Windows multiprocessing + -Allow normal exit even if DStat was never connected Version 1.4.2 -Refactor to fix critical bug preventing running packaged versions. Version 1.4.1 -- GitLab From 9ef919868f8d44f28ba94f3be557a59f837c3fe2 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 15:38:22 -0400 Subject: [PATCH 015/111] Packaging tweaks. --- MANIFEST.in | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index af0474d..711422b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,9 +3,12 @@ include version.py include setup.py include main.py include paver-minilib.zip +include LICENSE +include CHANGELOG +include README.markdown include core/utils/RELEASE-VERSION recursive-include dstat_interface * recursive-exclude dstat_interface *.pyc recursive-exclude dstat_interface *~ -exclude last_params.yml +recursive_exclude core last_params.yml recursive-exclude . .DS_Store \ No newline at end of file -- GitLab From 884c7302e2ecf7bc1ed189bc7e532a698dbf55d0 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 15:38:57 -0400 Subject: [PATCH 016/111] Store last parameters in user folder instead of install location. --- dstat_interface/main.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index cbcf0db..ad4704e 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -50,6 +50,7 @@ from core.errors import InputError from core.plugin import DstatPlugin, get_hub_uri mod_dir = os.path.dirname(os.path.abspath(__file__)) +conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') if __name__ == "__parents_main__": # Only runs for forking emulation on win sys.path.append(mod_dir) @@ -177,8 +178,9 @@ class Main(object): def quit(self): """Disconnect and save parameters on quit.""" + try: - params.save_params(self, 'last_params.yml') + params.save_params(self, os.path.join(conf_path, 'last_params.yml')) self.on_serial_disconnect_clicked() except KeyError: @@ -248,7 +250,14 @@ class Main(object): if self.params_loaded == False: try: - params.load_params(self, 'last_params.yml') + try: + os.makedirs(conf_path) + except OSError: + if not os.path.isdir(conf_path): + raise + params.load_params(self, + os.path.join(conf_path, 'last_params.yml') + ) except IOError: logger.info("No previous parameters found.") -- GitLab From 4957155ea296affcdcc60c7389009895e2b07f04 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 15:39:52 -0400 Subject: [PATCH 017/111] Update changelog. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index a75cca2..b64fecf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Version 1.4.3 -Fix another critical bug with Windows multiprocessing -Allow normal exit even if DStat was never connected + -Store last parameters in user folder Version 1.4.2 -Refactor to fix critical bug preventing running packaged versions. Version 1.4.1 -- GitLab From 0a2b75830e46be31bc5ae870dbf5f51ced278722 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 15:44:51 -0400 Subject: [PATCH 018/111] pavement: fix typo --- pavement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pavement.py b/pavement.py index 710bd07..9a19d79 100644 --- a/pavement.py +++ b/pavement.py @@ -13,7 +13,7 @@ setup(name='dstat_interface', description='Interface software for DStat potentiostat.', keywords='', author='Michael D. M Dryden', - author_email='mdryden@chemutoronto.ca', + author_email='mdryden@chem.utoronto.ca', url='http://microfluidics.utoronto.ca/dstat', license='GPLv3', packages=find_packages(), -- GitLab From 49ce0d104034804d06ec005f01a9c5ec2686c196 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Wed, 14 Jun 2017 16:24:50 -0400 Subject: [PATCH 019/111] Remove random file --- images/k.pdf | Bin 97118 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 images/k.pdf diff --git a/images/k.pdf b/images/k.pdf deleted file mode 100644 index c831664d9292c8f42f4c5a20dc52a31e1d64fe25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97118 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Ⓢry;FAA`7w<`Z5^_r`g5NgI>v65D3xz{3a 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 Date: Wed, 14 Jun 2017 17:40:48 -0400 Subject: [PATCH 020/111] Actually fix #29 --- dstat_interface/main.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index ad4704e..c731b20 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -41,6 +41,12 @@ from serial import SerialException import zmq +mod_dir = os.path.dirname(os.path.abspath(__file__)) +conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') + +if __name__ == "__parents_main__": # Only runs for forking emulation on win + sys.path.append(mod_dir) + from core.utils.version import getVersion import core.dstat as dstat from core.experiments import idle, pot @@ -48,12 +54,6 @@ from core import params, analysis from core.interface import exp_window, adc_pot, plot_ui, data_view, save from core.errors import InputError from core.plugin import DstatPlugin, get_hub_uri - -mod_dir = os.path.dirname(os.path.abspath(__file__)) -conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') - -if __name__ == "__parents_main__": # Only runs for forking emulation on win - sys.path.append(mod_dir) # Setup Logging root_logger = logging.getLogger("dstat") -- GitLab From 400bf865d2b6aa648f5ea641017797e375d8b7da Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Thu, 15 Jun 2017 18:33:47 -0400 Subject: [PATCH 021/111] Tweak connection code. --- dstat_interface/core/dstat/comm.py | 4 + .../core/experiments/experiment_template.py | 102 +++++++++++++----- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 9395d92..c1e57eb 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -56,6 +56,10 @@ class ConnectionError(Exception): super(ConnectionError, self).__init__(self, "Could not connect.") +class TransmitError(Exception): + def __init__(self): + super(TransmitError, self).__init__(self, + "No reply received.") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger = logging.getLogger("dstat.comm._serial_process") diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 495471b..18574f7 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -52,6 +52,7 @@ exp_logger = logging.getLogger("dstat.comm.Experiment") from ..errors import InputError, VarError from ..dstat import state +from dstat_comm import TransmitError class Experiment(object): """Store and acquire a potentiostat experiment. Meant to be subclassed @@ -118,58 +119,101 @@ class Experiment(object): self.plots.append(PlotBox(['current_voltage'])) - def write_command(self, cmd, params=None, retry=10): + def write_command(self, cmd, params=None, retry=5): """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 + def get_reply(retries = 3): + while True: + reply = self.serial.readline().rstrip() + if reply.startswith('#'): + dstat_logger.info(reply) + elif reply == "": + retries -= 1 + if retries <= 0: + raise TransmitError + else: + 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() - + tries = 5 + while True: + time.sleep(0.2) + self.serial.reset_input_buffer() + self.serial.write('!{}\n'.format(n)) + time.sleep(.1) + + try: + reply = get_reply() + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + if reply != "@ACK {}".format(n): - logger.warning("Invalid response: {}".format(reply)) + logger.warning("Expected ACK got: {}".format(reply)) continue - - self.serial.write('{}\n'.format(cmd)) - reply = get_reply() + tries = 5 + while True: + self.serial.write('{}\n'.format(cmd)) + try: + reply = get_reply() + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + if reply != "@RCV {}".format(n): - logger.warning("Invalid response: {}".format(reply)) + logger.warning("Expected RCV got: {}".format(reply)) continue if params is None: return True - reply = get_reply() + tries = 5 + while True: + try: + reply = get_reply() + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + if reply != "@RQP {}".format(n_params): - logger.warning("Invalid response: {}".format(reply)) + logger.warning("Expected RQP got: {}".format(reply)) continue - - self.serial.write(" ".join(params) + " ") - - reply = get_reply() - if reply != "@RCP {}".format(n_params): - logger.warning("Invalid response: {}".format(reply)) - continue + tries = 5 + for i in params: + while True: + self.serial.write(i + " ") + try: + reply = get_reply() + if reply == "@RCVC {}".format(i): + break + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break return True - return False def run(self, ser, ctrl_pipe, data_pipe): -- GitLab From 2a6e0ede435957599452a26cb7c213dcefb10c08 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 16 Jun 2017 14:47:37 -0400 Subject: [PATCH 022/111] Keep execute button disabled until DStat is ready. Closes #13 --- .../core/interface/dstatinterface.glade | 2 +- dstat_interface/main.py | 66 +++++++++++-------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/dstat_interface/core/interface/dstatinterface.glade b/dstat_interface/core/interface/dstatinterface.glade index 09dfb6c..cabd94c 100644 --- a/dstat_interface/core/interface/dstatinterface.glade +++ b/dstat_interface/core/interface/dstatinterface.glade @@ -1211,7 +1211,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin True True True - + False diff --git a/dstat_interface/main.py b/dstat_interface/main.py index c731b20..468c4ee 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -84,6 +84,7 @@ class Main(object): self.aboutdialog = self.builder.get_object('aboutdialog1') self.stopbutton = self.builder.get_object('pot_stop') self.startbutton = self.builder.get_object('pot_start') + self.startbutton.set_sensitive(False) self.adc_pot = adc_pot.adc_pot() self.error_context_id = self.statusbar.get_context_id("error") @@ -205,11 +206,16 @@ class Main(object): for i in self.serial_devices.ports: self.serial_liststore.append([i]) - def on_serial_connect_clicked(self, data=None): + def on_serial_connect_clicked(self, widget): """Connect and retrieve DStat version.""" - + if widget is self.serial_pmt_connect: + self.pmt_mode = True + self.adc_pot.ui['short_true'].set_active(True) + self.adc_pot.ui['short_true'].set_sensitive(False) + try: self.serial_connect.set_sensitive(False) + self.serial_pmt_connect.set_sensitive(False) dstat.state.dstat_version = dstat.comm.version_check( self.serial_liststore.get_value( self.serial_combobox.get_active_iter(), 0 @@ -237,16 +243,13 @@ class Main(object): self.start_ocp() self.connected = True - self.serial_connect.set_sensitive(False) - self.serial_pmt_connect.set_sensitive(False) self.serial_disconnect.set_sensitive(True) - except AttributeError as err: - logger.warning("AttributeError: %s", err) - self.serial_connect.set_sensitive(True) - except TypeError as err: - logger.warning("TypeError: %s", err) + except: self.serial_connect.set_sensitive(True) + self.serial_pmt_connect.set_sensitive(True) + self.adc_pot.ui['short_true'].set_sensitive(True) + raise if self.params_loaded == False: try: @@ -277,42 +280,45 @@ class Main(object): except AttributeError as err: logger.warning("AttributeError: %s", err) pass - + + if self.pmt_mode is True: + self.adc_pot.ui['short_true'].set_sensitive(True) + self.pmt_mode = False self.connected = False self.serial_connect.set_sensitive(True) self.serial_pmt_connect.set_sensitive(True) self.serial_disconnect.set_sensitive(False) + self.startbutton.set_sensitive(False) + self.stopbutton.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) - def on_pmt_mode_clicked(self, data=None): - """Connect in PMT mode""" - self.pmt_mode = True - self.adc_pot.ui['short_true'].set_active(True) - self.adc_pot.ui['short_true'].set_sensitive(False) - self.on_serial_connect_clicked() - def start_ocp(self, data=None): """Start OCP measurements.""" - if (dstat.state.dstat_version[0] >= 1 and dstat.state.dstat_version[1] >= 2): # Flush data pipe dstat.state.ser.flush_data() - if self.pmt_mode == True: + if self.pmt_mode is True: logger.info("Start PMT idle mode") dstat.state.ser.start_exp(idle.PMTIdle()) + self.ocp_is_running = True + self.ocp_proc = (GObject.timeout_add(250, self.ocp_running_proc) + , + ) else: logger.info("Start OCP") dstat.state.ser.start_exp(idle.OCPExp()) - self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data), - GObject.timeout_add(250, self.ocp_running_proc) - ) - self.ocp_is_running = True + self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data), + GObject.timeout_add(250, self.ocp_running_proc) + ) + self.ocp_is_running = False + + GObject.timeout_add(100, self.ocp_assert) # Check if getting data else: logger.info("OCP measurements not supported on v1.1 boards.") @@ -323,23 +329,29 @@ class Main(object): if (dstat.state.dstat_version[0] >= 1 and dstat.state.dstat_version[1] >= 2): - if self.pmt_mode == True: logger.info("Stop PMT idle mode") else: logger.info("Stop OCP") dstat.state.ser.send_ctrl('a') - + for i in self.ocp_proc: GObject.source_remove(i) while self.ocp_running_proc(): pass - self.ocp_is_running = False self.ocp_disp.set_text("") + self.ocp_is_running = False else: logger.error("OCP measurements not supported on v1.1 boards.") return + def ocp_assert(self): + if self.ocp_is_running: + self.startbutton.set_sensitive(True) + return False + else: + return True + def ocp_running_data(self): """Receive OCP value from experiment process and update ocp_disp field @@ -361,6 +373,7 @@ class Main(object): "{0:.3f}".format(incoming), " V"]) self.ocp_disp.set_text(data) + self.ocp_is_running = True except ValueError: pass @@ -681,7 +694,6 @@ class Main(object): self.metadata = None # Reset metadata self.spinner.stop() - self.startbutton.set_sensitive(True) self.stopbutton.set_sensitive(False) self.start_ocp() -- GitLab From d44f85fdccc62d5963948fc485843220af2d67e9 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 16 Jun 2017 15:26:43 -0400 Subject: [PATCH 023/111] Update README.markdown: Added upgrade instructions --- README.markdown | 48 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/README.markdown b/README.markdown index 978120a..ed2fdb5 100644 --- a/README.markdown +++ b/README.markdown @@ -14,6 +14,7 @@ It currently has no abilities for analyzing recorded data or opening previously 2. [Old Homebrew Instructions](#old-homebrew-instructions) 1. [Linux](#linux) 2. [Windows](#windows) + 3. [Upgrading](#upgrading) 2. [Getting Started](#Getting-Started) # Introduction @@ -40,10 +41,12 @@ dstat-interface has moved to gtk+3 and we now recommend Anaconda/Miniconda for i #### Old Homebrew Instructions The easiest way to get most of the necessary requirements to run dstat-interface is using [Homebrew](http://brew.sh): - brew tap homebrew/python - brew update - brew install python gobject-introspection gtk+3 pygobject3 py2cairo scipy zeromq - brew install matplotlib --with-pygtk +```shell +brew tap homebrew/python +brew update +brew install python gobject-introspection gtk+3 pygobject3 py2cairo scipy zeromq +brew install matplotlib --with-pygtk +``` Be patient on the last step—matplotlib needs to be compiled and may take 2 or 3 minutes. @@ -58,22 +61,24 @@ Linux prerequisite installation is similar to that of MacOS with Homebrew, only These instructions were tested on Ubuntu 17.04: -```` +````shell sudo apt-get install gobject-introspection python-gobject python-pip pip install dstat-interface ```` You will need to add your user to the `dialout` group to access virtual serial ports (replace with your username): - sudo usermod -a -G dialout +```shell +sudo usermod -a -G dialout +``` ## Windows The following terminal commands will result in a full installation of dstat-interface and its requirements, assuming [64-bit Miniconda][1] is installed: -```` +```shell conda create -n dstat -c mdryden python=2 dstat-interface activate dstat -```` +``` To finish the installation, GTK+3 and its Python bindings must be installed: @@ -91,12 +96,35 @@ 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 +```shell +activate dstat +python -m dstat_interface.main +``` [1]: https://repo.continuum.io/miniconda/Miniconda2-latest-Windows-x86_64.exe [2]: https://sourceforge.net/projects/pygobjectwin32/ +## Upgrading + +Anaconda builds can be upgraded to the latest version by issuing this command (from an activated conda environment): + +```shell +conda upgrade -c mdryden dstat-interface # For MacOS, be sure to upgrade dstat-interface-deps as well +``` + +pip installs can be upgraded similarly: + +```shell +pip install --upgrade dstat-interface +``` + +You can also run development builds directly from a cloned git repository (from an activated conda environment): + +```shell +cd ~/src/dstat-interface/dstat_interface # Replace with path to dstat_interface folder inside repository +python -m main +``` + # Getting started ## Interface overview -- GitLab From 8a61dc008c275295fd83d1fc003e119b8071fb76 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 16 Jun 2017 18:08:05 -0400 Subject: [PATCH 024/111] Fix bad import --- dstat_interface/core/experiments/experiment_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 18574f7..5f09808 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -52,7 +52,7 @@ exp_logger = logging.getLogger("dstat.comm.Experiment") from ..errors import InputError, VarError from ..dstat import state -from dstat_comm import TransmitError +from ..dstat.comm import TransmitError class Experiment(object): """Store and acquire a potentiostat experiment. Meant to be subclassed -- GitLab From 67aa110c27603d654c756036b6c8c9ecab8da659 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 16 Jun 2017 18:09:11 -0400 Subject: [PATCH 025/111] Standardize log identifiers --- dstat_interface/core/analysis.py | 2 +- dstat_interface/core/dstat/comm.py | 6 +++--- dstat_interface/core/experiments/cal.py | 2 +- .../core/experiments/experiment_template.py | 10 +++++----- dstat_interface/core/experiments/parameter_test.py | 2 +- dstat_interface/core/interface/data_view.py | 2 +- dstat_interface/core/interface/db.py | 2 +- dstat_interface/core/interface/exp_int.py | 2 +- dstat_interface/core/interface/exp_window.py | 2 +- dstat_interface/core/interface/plot_ui.py | 2 +- dstat_interface/core/interface/save.py | 2 +- dstat_interface/core/params.py | 2 +- dstat_interface/main.py | 11 +++++++---- 13 files changed, 25 insertions(+), 22 deletions(-) diff --git a/dstat_interface/core/analysis.py b/dstat_interface/core/analysis.py index 76c1be0..747df18 100755 --- a/dstat_interface/core/analysis.py +++ b/dstat_interface/core/analysis.py @@ -25,7 +25,7 @@ import os from numpy import mean, trapz -logger = logging.getLogger('dstat.analysis') +logger = logging.getLogger(__name__) mod_dir = os.path.dirname(os.path.abspath(__file__)) class AnalysisOptions(object): diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index c1e57eb..be90194 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -35,9 +35,9 @@ except ImportError: from ..errors import InputError, VarError -logger = logging.getLogger("dstat.comm") -dstat_logger = logging.getLogger("dstat.comm.DSTAT") -exp_logger = logging.getLogger("dstat.comm.Experiment") +logger = logging.getLogger(__name__) +dstat_logger = logging.getLogger("{}.DSTAT".format(__name__)) +exp_logger = logging.getLogger("{}.Experiment".format(__name__)) from . import state diff --git a/dstat_interface/core/experiments/cal.py b/dstat_interface/core/experiments/cal.py index 1cd3721..3f1aff2 100755 --- a/dstat_interface/core/experiments/cal.py +++ b/dstat_interface/core/experiments/cal.py @@ -21,7 +21,7 @@ import time import struct import logging -logger = logging.getLogger("dstat.experiments.cal") +logger = logging.getLogger(__name__) import serial diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 5f09808..24e3bfb 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -46,14 +46,14 @@ sns.set(context='paper', style='darkgrid') 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 -from ..dstat import state +from ..dstat import state, comm from ..dstat.comm import TransmitError +logger = logging.getLogger(__name__) +dstat_logger = logging.getLogger("{}.DSTAT".format(comm.__name__)) +exp_logger = logging.getLogger("{}.Experiment".format(__name__)) + class Experiment(object): """Store and acquire a potentiostat experiment. Meant to be subclassed to by different experiment types and not used instanced directly. Subclass diff --git a/dstat_interface/core/experiments/parameter_test.py b/dstat_interface/core/experiments/parameter_test.py index ec62943..fff0751 100755 --- a/dstat_interface/core/experiments/parameter_test.py +++ b/dstat_interface/core/experiments/parameter_test.py @@ -22,7 +22,7 @@ import logging from errors import InputError -logger = logging.getLogger("dstat.parameter_test") +logger = logging.getLogger(__name__) def lsv_test(params): """Test LSV parameters for sanity""" diff --git a/dstat_interface/core/interface/data_view.py b/dstat_interface/core/interface/data_view.py index e4bd711..e7c4db6 100644 --- a/dstat_interface/core/interface/data_view.py +++ b/dstat_interface/core/interface/data_view.py @@ -1,7 +1,7 @@ from __future__ import division, absolute_import, print_function, unicode_literals import logging -logger = logging.getLogger("dstat.interface.data_view") +logger = logging.getLogger(__name__) from collections import OrderedDict diff --git a/dstat_interface/core/interface/db.py b/dstat_interface/core/interface/db.py index cc31db7..e362292 100755 --- a/dstat_interface/core/interface/db.py +++ b/dstat_interface/core/interface/db.py @@ -30,7 +30,7 @@ except ImportError: print "ERR: GTK not available" sys.exit(1) -logger = logging.getLogger('dstat.interface.db') +logger = logging.getLogger(__name__) class DB_Window(GObject.GObject): __gsignals__ = { diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index 9389745..6b271a0 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -36,7 +36,7 @@ from ..experiments import (cal, chronoamp, cv, experiment_template, import __main__ from ..errors import InputError, VarError -logger = logging.getLogger("dstat.interface.exp_int") +logger = logging.getLogger(__name__) mod_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/dstat_interface/core/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py index 79c16ed..89b59ab 100755 --- a/dstat_interface/core/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -31,7 +31,7 @@ except ImportError: from . import exp_int -logger = logging.getLogger("dstat.interface.exp_window") +logger = logging.getLogger(__name__) class Experiments(GObject.Object): __gsignals__ = { diff --git a/dstat_interface/core/interface/plot_ui.py b/dstat_interface/core/interface/plot_ui.py index b0ff570..b0013e8 100644 --- a/dstat_interface/core/interface/plot_ui.py +++ b/dstat_interface/core/interface/plot_ui.py @@ -1,5 +1,5 @@ import logging -logger = logging.getLogger("dstat.interface.plot_ui") +logger = logging.getLogger(__name__) try: import gi diff --git a/dstat_interface/core/interface/save.py b/dstat_interface/core/interface/save.py index 850ed0b..44a7884 100755 --- a/dstat_interface/core/interface/save.py +++ b/dstat_interface/core/interface/save.py @@ -23,7 +23,7 @@ from __future__ import division, absolute_import, print_function, unicode_litera import io import os import logging -logger = logging.getLogger("dstat.interface.save") +logger = logging.getLogger(__name__) try: diff --git a/dstat_interface/core/params.py b/dstat_interface/core/params.py index 0aa8580..29af977 100755 --- a/dstat_interface/core/params.py +++ b/dstat_interface/core/params.py @@ -23,7 +23,7 @@ import yaml from errors import InputError -logger = logging.getLogger('dstat.params') +logger = logging.getLogger(__name__) def get_params(window): """Fetches and returns dict of all parameters for saving.""" diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 468c4ee..f7366ff 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -56,17 +56,20 @@ from core.errors import InputError from core.plugin import DstatPlugin, get_hub_uri # Setup Logging -root_logger = logging.getLogger("dstat") -root_logger.setLevel(level=logging.INFO) +logger = logging.getLogger(__name__) +core_logger = logging.getLogger("core") +loggers = [logger, core_logger] + log_handler = logging.StreamHandler() log_formatter = logging.Formatter( fmt='%(asctime)s %(levelname)s: [%(name)s] %(message)s', datefmt='%H:%M:%S' ) log_handler.setFormatter(log_formatter) -root_logger.addHandler(log_handler) -logger = logging.getLogger("dstat.main") +for i in loggers: + i.setLevel(level=logging.INFO) + i.addHandler(log_handler) class Main(object): """Main program """ -- GitLab From 1c8e5e4c57753927a9f09f390893aabc4c4013e5 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 16 Jun 2017 18:10:07 -0400 Subject: [PATCH 026/111] Formatting --- dstat_interface/main.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index f7366ff..27a9963 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -148,7 +148,6 @@ class Main(object): self.mainwindow.show_all() self.exp_window.hide_exps() - self.expnumber = 0 self.connected = False @@ -200,7 +199,6 @@ class Main(object): def on_menu_analysis_options_activate(self, menuitem, data=None): self.analysis_opt_window.show() - def on_serial_refresh_clicked(self, data=None): """Refresh list of serial devices.""" self.serial_devices.refresh() -- GitLab From 1e6fd813adef7077c5283cb2c2264f7d35e088ef Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 16 Jun 2017 18:10:58 -0400 Subject: [PATCH 027/111] Keep initial connection failures from blocking. --- dstat_interface/core/dstat/comm.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index be90194..0378aa0 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -73,18 +73,24 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): try: ser = serial.Serial(ser_port, timeout=1) ser_logger.info("Connecting") - time.sleep(2) + time.sleep(.5) connected = True except serial.SerialException: pass if connected is True: break - - if ser.isOpen() is False: + + try: + if ser.isOpen() is False: + ser_logger.info("Connection Error") + proc_pipe.send("SERIAL_ERROR") + return 1 + except UnboundLocalError: # ser doesn't exist ser_logger.info("Connection Error") + proc_pipe.send("SERIAL_ERROR") return 1 - + ser.write('!0 ') for i in range(10): -- GitLab From 0f16af49fe38a59871613b3ed41381d55a8bdfd5 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 19 Jun 2017 16:02:56 -0400 Subject: [PATCH 028/111] main: Add futures import --- dstat_interface/main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 27a9963..c556cf0 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -20,6 +20,8 @@ """ GUI Interface for Wheeler Lab DStat """ +from __future__ import division, absolute_import, print_function, unicode_literals + import sys import os import multiprocessing @@ -34,7 +36,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) from serial import SerialException @@ -555,11 +557,11 @@ class Main(object): return True except EOFError as err: - print err + logger.error(err) self.experiment_done() return False except IOError as err: - print err + logger.error(err) self.experiment_done() return False -- GitLab From 93879249c80abba1123b64abff61cb0fd61c9220 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 19 Jun 2017 16:05:16 -0400 Subject: [PATCH 029/111] Do version saving to state outside of main. --- dstat_interface/core/dstat/comm.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 0378aa0..47d6ba2 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -276,8 +276,8 @@ class VersionCheck(object): return status def version_check(ser_port): - """Tries to contact DStat and get version. Returns a list of - [(major, minor), serial instance]. If no response, returns empty tuple. + """Tries to contact DStat and get version. Stores version in state. + If no response, returns False, otherwise True. Arguments: ser_port -- address of serial port to use @@ -288,14 +288,15 @@ def version_check(ser_port): state.ser.start_exp(VersionCheck()) result = state.ser.get_proc(block=True) if result == "SERIAL_ERROR": - buffer = 1 + state.dstat_version = None + return False else: buffer = state.ser.get_data(block=True) + state.dstat_version = buffer logger.debug("version_check done") time.sleep(.1) - return buffer - + return True class Settings(object): def __init__(self, task, settings=None): -- GitLab From 6c554c424cb1e0c932c3639041936a20ca3ea452 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 19 Jun 2017 16:22:25 -0400 Subject: [PATCH 030/111] Added DStat version info window. --- dstat_interface/core/dstat/comm.py | 30 +- dstat_interface/core/dstat/dfu.py | 255 +++++++ dstat_interface/core/dstat/state.py | 3 +- .../core/interface/dstatinterface.glade | 651 +----------------- dstat_interface/core/interface/hw_info.py | 38 + dstat_interface/main.py | 16 +- 6 files changed, 360 insertions(+), 633 deletions(-) create mode 100755 dstat_interface/core/dstat/dfu.py create mode 100755 dstat_interface/core/interface/hw_info.py diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 47d6ba2..ff5a4c2 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -259,19 +259,34 @@ class VersionCheck(object): break parted = input.rstrip().split('.') - e = "PCB version: " - e += str(input.rstrip()) - dstat_logger.info(e) - data_pipe.send((int(parted[0]), int(parted[1]))) - status = "DONE" + try: + logger.info( + "PCB Version: {}".format(".".join(parted[:2])) + ) + logger.info( + "Firmware Version: {}".format( + hex(int(parted[2])).lstrip('0x') + ) + ) + data_pipe.send(( + ( int(parted[0]), int(parted[1]) ), + hex(int(parted[2])).lstrip('0x') + )) + except IndexError: + logger.info("Your firmware does not support version detection.") + data_pipe.send(( + ( int(parted[0]), int(parted[1]) ), + False + )) + finally: + status = "DONE" except UnboundLocalError as e: status = "SERIAL_ERROR" except SerialException as e: logger.error('SerialException: %s', e) status = "SERIAL_ERROR" - finally: return status @@ -289,10 +304,11 @@ def version_check(ser_port): result = state.ser.get_proc(block=True) if result == "SERIAL_ERROR": state.dstat_version = None + state.firmware_version = None return False else: buffer = state.ser.get_data(block=True) - state.dstat_version = buffer + state.dstat_version, state.firmware_version = buffer logger.debug("version_check done") time.sleep(.1) diff --git a/dstat_interface/core/dstat/dfu.py b/dstat_interface/core/dstat/dfu.py new file mode 100755 index 0000000..c24857f --- /dev/null +++ b/dstat_interface/core/dstat/dfu.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# 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 . + +from __future__ import (absolute_import, division, + print_function, unicode_literals) + +import subprocess +import sys +import os +import time +import logging +from tempfile import mkdtemp +from zipfile import ZipFile + +if sys.version_info >= (3,): + import urllib.request as urllib2 + import urllib.parse as urlparse +else: + import urllib2 + import urlparse + +logger = logging.getLogger(__name__) + +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 serial + +from . import state + +url = "http://microfluidics.utoronto.ca/gitlab/api/v4/projects/4/jobs/artifacts/master/download?job=1.2.3&private_token=zkgSx1FaaTP7yLyFKkX6" + +def download_fw(): # from https://stackoverflow.com/a/16518224 + temp_dir = mkdtemp() + logger.info("Temporary directory: {}".format(temp_dir)) + os.chdir(temp_dir) # Go to temporary directory + + u = urllib2.urlopen(url) + + scheme, netloc, path, query, fragment = urlparse.urlsplit(url) + filename = os.path.basename(path) + if not filename: + filename = 'downloaded.file' + + with open(filename, 'wb') as f: + meta = u.info() + meta_func = meta.getheaders if hasattr(meta, 'getheaders') else meta.get_all + meta_length = meta_func("Content-Length") + file_size = None + if meta_length: + file_size = int(meta_length[0]) + logger.info("Downloading: {0} Bytes: {1}".format(url, file_size)) + + file_size_dl = 0 + block_sz = 8192 + while True: + buffer = u.read(block_sz) + if not buffer: + break + + file_size_dl += len(buffer) + f.write(buffer) + + status = "{0:16}".format(file_size_dl) + if file_size: + status += " [{0:6.2f}%]".format(file_size_dl * 100 / file_size) + status += chr(13) + logger.info(status) + + with ZipFile(filename, mode='r') as z: + fw_path = z.extract('dstat-firmware.hex') + + return fw_path + +def test_firmware_version(current=None): + if current is None: + current = state.firmware_version + + temp_dir = mkdtemp() + logger.info("Temporary directory: {}".format(temp_dir)) + os.chdir(temp_dir) # Go to temporary directory + + command = "git clone http://microfluidics.utoronto.ca/gitlab/dstat/dstat-firmware.git" + logger.info('Cloning master.') + output = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) + logger.info(output) + + os.chdir("./dstat-firmware") + command = "git merge-base --is-ancestor master {}".format(current) + test = subprocess.call(command.split()) + + if test == '0': #already newest + logger.info('Firmware is latest available.') + return 'latest' + if test == '1': #old version + logger.info('Firmware is out of date.') + return 'old' + if test == '128': # newer or different branch + logger.info('Firmware is not on the master branch.') + return 'devel' + + +def dfu_program(path='./dstat-firmware.hex'): + """Tries to program DStat over USB with DFU with hex file at path.""" + try: + command = "dfu-programmer atxmega256a3u erase" + output = subprocess.check_output(command.split(), + stderr=subprocess.STDOUT) + logger.info(output) + command = "dfu-programmer atxmega256a3u flash {}".format(path) + output = subprocess.check_output(command.split(), + stderr=subprocess.STDOUT) + logger.info(output) + command = "dfu-programmer atxmega256a3u launch" + output = subprocess.check_output(command.split(), + stderr=subprocess.STDOUT) + logger.info(output) + except subprocess.CalledProcessError as e: + logger.error("{} failed with output:".format(" ".join(e.cmd))) + logger.error(e.output) + +def check_dstat(device): + ser = Serial(device, timeout=1) + ser.write("ck".encode('ascii')) + ser.flushInput() + ser.write('!SF'.encode('ascii')) + ser.close() + + logger.info("DFU command sent to {} at {}".format(port.description, port.device)) + +# class DFUWindow(object): +# def __init__(self): +# 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(orientation=Gtk.Orientation.VERTICAL) +# grid.set_column_homogeneous(False) +# +# entries = OrderedDict([ +# ('start', 'Start (Hz)'), +# ('stop', 'Stop (Hz)'), +# ('n_increments', 'Steps/Decade'), +# ('wait_ms', 'Settling time for first sample (ms)'), +# ('cycles', 'Number of settling cycles'), +# ('avg_samples', 'Samples to average'), +# ('output_mv', "mV p-p"), +# ('offset_mv', "Offset (mV)") +# ]) +# +# 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) +# +# row = len(entries) +# grid.attach(Gtk.Label(label="PGA Boost"), 0, row, 1, 1) +# self.pga_boost = Gtk.CheckButton.new_with_label('5x') +# grid.attach(self.pga_boost, 1, row, 1, 1) +# row += 1 +# +# #IV gain +# self.gain_settings = OrderedDict([ +# ('100 V/A', b'1'), +# ('3 kV/A', b'2'), +# ('30 kV/A', b'3'), +# ('300 kV/A', b'4'), +# ('3 MV/A', b'5'), +# ('30 MV/A', b'6'), +# ('100 MV/A', b'7'), +# ('*Bypass', b'0') +# ]) +# +# grid.attach(Gtk.Label(label="IV Gain"), 0, row, 1, 1) +# self.iv_gain = Gtk.ComboBoxText() +# for label, cmd in self.gain_settings.items(): +# self.iv_gain.append(id=cmd, text=label) +# grid.attach(self.iv_gain, 1, row, 1, 1) +# row += 1 +# +# #Output Range +# self.range_settings = OrderedDict([ +# ('1x', b'1'), +# ('1/2x', b'2'), +# ('1/5x', b'3'), +# ('1/10x', b'4') +# ]) +# +# grid.attach(Gtk.Label(label="AD5933 Output"), 0, row, 1, 1) +# self.output_range = Gtk.ComboBoxText() +# for label, cmd in self.range_settings.items(): +# self.output_range.append(id=cmd, text=label) +# grid.attach(self.output_range, 1, row, 1, 1) +# row += 1 +# +# #Reference +# self.ref_settings = OrderedDict([ +# ('RE Input', b'0'), +# ('Cal 1 (3 kΩ)', b'1'), +# ('Cal 2 (3 MΩ)', b'2'), +# ('Cal 1+2', b'3') +# ]) +# +# grid.attach(Gtk.Label(label="Reference Connection"), 0, row, 1, 1) +# self.ref_connection = Gtk.ComboBoxText() +# for label, cmd in self.ref_settings.items(): +# self.ref_connection.append(id=cmd, text=label) +# grid.attach(self.ref_connection, 1, row, 1, 1) +# row += 1 + + + +if __name__ == "__main__": + log_handler = logging.StreamHandler() + log_formatter = logging.Formatter( + fmt='%(asctime)s %(levelname)s: [%(name)s] %(message)s', + datefmt='%H:%M:%S' + ) + log_handler.setFormatter(log_formatter) + logger.setLevel(level=logging.INFO) + logger.addHandler(log_handler) + + check_dstat() + time.sleep(2) + dfu_program(sys.argv[1]) \ No newline at end of file diff --git a/dstat_interface/core/dstat/state.py b/dstat_interface/core/dstat/state.py index 7e3f195..785c553 100644 --- a/dstat_interface/core/dstat/state.py +++ b/dstat_interface/core/dstat/state.py @@ -2,4 +2,5 @@ from collections import OrderedDict settings = OrderedDict() ser = None -dstat_version = None \ No newline at end of file +dstat_version = None +firmware_version = None \ No newline at end of file diff --git a/dstat_interface/core/interface/dstatinterface.glade b/dstat_interface/core/interface/dstatinterface.glade index cabd94c..c9b7201 100644 --- a/dstat_interface/core/interface/dstatinterface.glade +++ b/dstat_interface/core/interface/dstatinterface.glade @@ -8,635 +8,15 @@ True dialog DStat-interface - 1.0.3 © Michael Dryden 2014 This software is licensed under the GNU GPL v3 http://microfluidics.utoronto.ca/dstat - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS + Wheeler Microfuidics Lab Michael Dryden Thanks to Christian Fobel for help with Dropbot Plugin image-missing + gpl-3-0 True @@ -689,6 +69,11 @@ Thanks to Christian Fobel for help with Dropbot Plugin False gtk-preferences + + True + False + image7 + True False @@ -779,6 +164,28 @@ Thanks to Christian Fobel for help with Dropbot Plugin + + + True + False + DStat + + + True + False + + + System Information + True + False + image7 + False + + + + + + True diff --git a/dstat_interface/core/interface/hw_info.py b/dstat_interface/core/interface/hw_info.py new file mode 100755 index 0000000..cb89367 --- /dev/null +++ b/dstat_interface/core/interface/hw_info.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +import logging + +logger = logging.getLogger(__name__) + +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 ..dstat import state, dfu + +class InfoDialog(object): + def __init__(self, parent, connect, signal='activate'): + self.parent = parent + connect.connect(signal, self.activate) + + def activate(self, object=None, data=None): + self.dialog = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.INFO, + Gtk.ButtonsType.OK, "DStat Info") + self.dialog.format_secondary_text( + "PCB Version: {}.{}\n".format( + state.dstat_version[0], state.dstat_version[1]) + + "Firmware Version:{}".format(state.firmware_version) + ) + + self.dialog.connect('response', self.destroy) + self.dialog.show() + + def destroy(self, object=None, data=None): + self.dialog.destroy() + \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index c556cf0..fe0c77d 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -53,7 +53,8 @@ from core.utils.version import getVersion import core.dstat as dstat from core.experiments import idle, pot from core import params, analysis -from core.interface import exp_window, adc_pot, plot_ui, data_view, save +from core.interface import (exp_window, adc_pot, plot_ui, data_view, + save, hw_info) from core.errors import InputError from core.plugin import DstatPlugin, get_hub_uri @@ -137,6 +138,13 @@ class Main(object): self.spinner = self.builder.get_object('spinner') self.mainwindow = self.builder.get_object('window1') + + self.menu_dstat_info = self.builder.get_object('menu_dstat_info') + self.menu_dstat_info.set_sensitive(False) + self.dstat_info_window = hw_info.InfoDialog( + parent=self.mainwindow, + connect=self.menu_dstat_info + ) # Set Version Strings try: @@ -154,7 +162,7 @@ class Main(object): self.connected = False self.pmt_mode = False - + self.menu_dropbot_connect = self.builder.get_object( 'menu_dropbot_connect') self.menu_dropbot_disconnect = self.builder.get_object( @@ -219,7 +227,7 @@ class Main(object): try: self.serial_connect.set_sensitive(False) self.serial_pmt_connect.set_sensitive(False) - dstat.state.dstat_version = dstat.comm.version_check( + dstat.comm.version_check( self.serial_liststore.get_value( self.serial_combobox.get_active_iter(), 0 ) @@ -294,6 +302,7 @@ class Main(object): self.serial_disconnect.set_sensitive(False) self.startbutton.set_sensitive(False) self.stopbutton.set_sensitive(False) + self.menu_dstat_info.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) def start_ocp(self, data=None): @@ -351,6 +360,7 @@ class Main(object): def ocp_assert(self): if self.ocp_is_running: self.startbutton.set_sensitive(True) + self.menu_dstat_info.set_sensitive(True) return False else: return True -- GitLab From b391f7a9daf5247c96872c457604466f60e1da31 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 20 Jun 2017 20:19:34 -0400 Subject: [PATCH 031/111] Fix subprocess logging. --- dstat_interface/core/dstat/comm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index ff5a4c2..ce0e89b 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -62,8 +62,7 @@ class TransmitError(Exception): "No reply received.") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): - ser_logger = logging.getLogger("dstat.comm._serial_process") - + ser_logger = logging.getLogger("{}._serial_process".format(__name__)) connected = False -- GitLab From 43bacd2b8055d133804179848c98c7cfd7cffa91 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 20 Jun 2017 20:23:13 -0400 Subject: [PATCH 032/111] Handle already disconnected serial during disconnect silently. --- dstat_interface/core/dstat/comm.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index ce0e89b..1d641be 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -102,7 +102,6 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser.write('!0 ') time.sleep(.1) - while True: # These can only be called when no experiment is running if ctrl_pipe.poll(): @@ -110,7 +109,10 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): if ctrl_buffer == ('a' or "DISCONNECT"): proc_pipe.send("ABORT") - ser.write('a') + try: + ser.write('a') + except serial.SerialException: + return 0 ser_logger.info("ABORT") if ctrl_buffer == "DISCONNECT": -- GitLab From 99557849a01aa1d296d8cfadf2306b3fdd318490 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 20 Jun 2017 20:31:39 -0400 Subject: [PATCH 033/111] Working Firmware update. (On Mac, at least) --- dstat_interface/core/dstat/dfu.py | 343 ++++++++++++------ .../core/interface/dstatinterface.glade | 14 + dstat_interface/core/interface/hw_info.py | 2 +- dstat_interface/main.py | 15 +- 4 files changed, 259 insertions(+), 115 deletions(-) diff --git a/dstat_interface/core/dstat/dfu.py b/dstat_interface/core/dstat/dfu.py index c24857f..c9c5c24 100755 --- a/dstat_interface/core/dstat/dfu.py +++ b/dstat_interface/core/dstat/dfu.py @@ -49,17 +49,160 @@ except ImportError: import serial from . import state +from .comm import dstat_logger, exp_logger -url = "http://microfluidics.utoronto.ca/gitlab/api/v4/projects/4/jobs/artifacts/master/download?job=1.2.3&private_token=zkgSx1FaaTP7yLyFKkX6" +fwurl = "http://microfluidics.utoronto.ca/gitlab/api/v4/projects/4/jobs/artifacts/master/download?job=1.2.3&private_token=zkgSx1FaaTP7yLyFKkX6" + +class FWDialog(object): + def __init__(self, parent, connect, stop_callback, disconnect_callback, signal='activate'): + self.parent = parent + self.stop = stop_callback + self.disconnect = disconnect_callback + connect.connect(signal, self.activate) + + def activate(self, widget=None, data=None): + for name, result in assert_deps().items(): + if result is not True: + logger.error("Can't use firmware update module.") + self.missing_deps() + return + + self.stop() # Stop OCP + version_result, master = test_firmware_version() + + if version_result is False: + self.git_error() + return + + if version_result == 'latest': + message = "Your firmware is already up to date." + secondary = "Click yes to reflash firmware anyways." + elif version_result == 'devel': + message = "Your firmware is not on the master branch." + secondary = "You may have a development version. " +\ + "Click yes to reflash firmware anyways." + elif version_result == 'old': + message = "Your firmware is out of date." + secondary = "Click yes to flash the latest firmware." + + dialog = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.INFO, + Gtk.ButtonsType.YES_NO, message) + dialog.format_secondary_text(secondary) + dialog.get_content_area().add( + Gtk.Label( + label="Installed version: {}".format(state.firmware_version))) + + dialog.get_content_area().add( + Gtk.Label(label="Latest version: {}".format(master))) + + dialog.show_all() + response = dialog.run() + + if response == Gtk.ResponseType.YES: + try: + download_fw() + except: + self.dl_error() + return + + dstat_enter_dfu() + + self.dfu_notice() + self.disconnect() + try: + dfu_program() + except: + self.dfu_error() + + dialog.destroy() + + else: + dialog.destroy() + + def missing_deps(self): + dialog = Gtk.MessageDialog( + self.parent, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, "Missing Dependencies") + + dialog.format_secondary_text('Check console for more info.') + + dialog.connect('response', self.destroy) + dialog.show() + + def git_error(self): + dialog = Gtk.MessageDialog( + self.parent, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, "Git Error") + + dialog.format_secondary_text('Check console for more info.') + + dialog.connect('response', self.destroy) + dialog.show() + + def dl_error(self): + dialog = Gtk.MessageDialog( + self.parent, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, "Download Error") + + dialog.format_secondary_text('Check console for more info.') + + dialog.connect('response', self.destroy) + dialog.show() + + def dfu_notice(self): + dialog = Gtk.MessageDialog( + self.parent, 0, Gtk.MessageType.INFO, + Gtk.ButtonsType.OK, "Note about DFU") + + dialog.format_secondary_text("Click OK once the DStat has connected in " + + 'DFU mode. (Check Devices) Windows may not enumerate the DStat in' + + ' DFU mode. Try holding down the reset button while plugging the' + + 'USB port in (No LEDs should be lit), then click OK. Make sure ' + + 'the DFU driver from the dfu-programmer directory is installed.') + + dialog.run() + dialog.destroy() + + def dfu_error(self): + dialog = Gtk.MessageDialog( + self.parent, 0, Gtk.MessageType.ERROR, + Gtk.ButtonsType.OK, "Could not update over DFU") + + dialog.format_secondary_text('Check console for more info.') + + dialog.connect('response', self.destroy) + dialog.show() + + def destroy(self, widget=None, data=None): + widget.destroy() + + +def assert_deps(): + deps = {'git' : 'git --version', + 'dfu-programmer' : 'dfu-programmer --version'} + + result = {} + + for key, command in deps.items(): + try: + output = subprocess.check_output(command.split(), + stderr=subprocess.STDOUT) + logger.info("%s\n%s", command, output) + result[key] = True + except subprocess.CalledProcessError: + logger.warning("{} is not available.".format(key)) + result[key] = False + + return result def download_fw(): # from https://stackoverflow.com/a/16518224 temp_dir = mkdtemp() logger.info("Temporary directory: {}".format(temp_dir)) os.chdir(temp_dir) # Go to temporary directory - u = urllib2.urlopen(url) + u = urllib2.urlopen(fwurl) - scheme, netloc, path, query, fragment = urlparse.urlsplit(url) + scheme, netloc, path, query, fragment = urlparse.urlsplit(fwurl) filename = os.path.basename(path) if not filename: filename = 'downloaded.file' @@ -71,7 +214,7 @@ def download_fw(): # from https://stackoverflow.com/a/16518224 file_size = None if meta_length: file_size = int(meta_length[0]) - logger.info("Downloading: {0} Bytes: {1}".format(url, file_size)) + logger.info("Downloading: {0} Bytes: {1}".format(fwurl, file_size)) file_size_dl = 0 block_sz = 8192 @@ -104,23 +247,36 @@ def test_firmware_version(current=None): command = "git clone http://microfluidics.utoronto.ca/gitlab/dstat/dstat-firmware.git" logger.info('Cloning master.') - output = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) + try: + output = subprocess.check_output(command.split(), + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + logger.error("git failed with error code {}".format(e.returncode)) + logger.error("Output: {}".format(e.output)) + return False, None logger.info(output) os.chdir("./dstat-firmware") + + command = "git rev-parse --short master" + master = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) + logger.info("Current master commit: {}".format(master)) + command = "git merge-base --is-ancestor master {}".format(current) test = subprocess.call(command.split()) - if test == '0': #already newest + if test == 0: # already newest logger.info('Firmware is latest available.') - return 'latest' - if test == '1': #old version + return 'latest', master + elif test == 1: # old version logger.info('Firmware is out of date.') - return 'old' - if test == '128': # newer or different branch + return 'old', master + elif test == 128: # newer or different branch logger.info('Firmware is not on the master branch.') - return 'devel' - + return 'devel', master + else: + logger.error('Unexpected git error. Git exited {}'.format(test)) + return False, None def dfu_program(path='./dstat-firmware.hex'): """Tries to program DStat over USB with DFU with hex file at path.""" @@ -128,117 +284,78 @@ def dfu_program(path='./dstat-firmware.hex'): command = "dfu-programmer atxmega256a3u erase" output = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) - logger.info(output) + logger.info("%s\n%s", command, output) command = "dfu-programmer atxmega256a3u flash {}".format(path) output = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) - logger.info(output) + logger.info("%s\n%s", command, output) command = "dfu-programmer atxmega256a3u launch" output = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) - logger.info(output) + logger.info("%s\n%s", command, output) except subprocess.CalledProcessError as e: logger.error("{} failed with output:".format(" ".join(e.cmd))) logger.error(e.output) - -def check_dstat(device): - ser = Serial(device, timeout=1) - ser.write("ck".encode('ascii')) - ser.flushInput() - ser.write('!SF'.encode('ascii')) - ser.close() - - logger.info("DFU command sent to {} at {}".format(port.description, port.device)) - -# class DFUWindow(object): -# def __init__(self): -# 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(orientation=Gtk.Orientation.VERTICAL) -# grid.set_column_homogeneous(False) -# -# entries = OrderedDict([ -# ('start', 'Start (Hz)'), -# ('stop', 'Stop (Hz)'), -# ('n_increments', 'Steps/Decade'), -# ('wait_ms', 'Settling time for first sample (ms)'), -# ('cycles', 'Number of settling cycles'), -# ('avg_samples', 'Samples to average'), -# ('output_mv', "mV p-p"), -# ('offset_mv', "Offset (mV)") -# ]) -# -# 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) -# -# row = len(entries) -# grid.attach(Gtk.Label(label="PGA Boost"), 0, row, 1, 1) -# self.pga_boost = Gtk.CheckButton.new_with_label('5x') -# grid.attach(self.pga_boost, 1, row, 1, 1) -# row += 1 -# -# #IV gain -# self.gain_settings = OrderedDict([ -# ('100 V/A', b'1'), -# ('3 kV/A', b'2'), -# ('30 kV/A', b'3'), -# ('300 kV/A', b'4'), -# ('3 MV/A', b'5'), -# ('30 MV/A', b'6'), -# ('100 MV/A', b'7'), -# ('*Bypass', b'0') -# ]) -# -# grid.attach(Gtk.Label(label="IV Gain"), 0, row, 1, 1) -# self.iv_gain = Gtk.ComboBoxText() -# for label, cmd in self.gain_settings.items(): -# self.iv_gain.append(id=cmd, text=label) -# grid.attach(self.iv_gain, 1, row, 1, 1) -# row += 1 -# -# #Output Range -# self.range_settings = OrderedDict([ -# ('1x', b'1'), -# ('1/2x', b'2'), -# ('1/5x', b'3'), -# ('1/10x', b'4') -# ]) -# -# grid.attach(Gtk.Label(label="AD5933 Output"), 0, row, 1, 1) -# self.output_range = Gtk.ComboBoxText() -# for label, cmd in self.range_settings.items(): -# self.output_range.append(id=cmd, text=label) -# grid.attach(self.output_range, 1, row, 1, 1) -# row += 1 -# -# #Reference -# self.ref_settings = OrderedDict([ -# ('RE Input', b'0'), -# ('Cal 1 (3 kΩ)', b'1'), -# ('Cal 2 (3 MΩ)', b'2'), -# ('Cal 1+2', b'3') -# ]) -# -# grid.attach(Gtk.Label(label="Reference Connection"), 0, row, 1, 1) -# self.ref_connection = Gtk.ComboBoxText() -# for label, cmd in self.ref_settings.items(): -# self.ref_connection.append(id=cmd, text=label) -# grid.attach(self.ref_connection, 1, row, 1, 1) -# row += 1 + raise + +def dstat_enter_dfu(): + """Tries to contact DStat and get version. Stores version in state. + If no response, returns False, otherwise True. + + Arguments: + ser_port -- address of serial port to use + """ + exp = DFUMode() + state.ser.start_exp(exp) + while True: + result = state.ser.get_proc(block=True) + if result in ('SERIAL_ERROR', 'DONE'): + break + logger.info(result) + # state.ser.disconnect() + + time.sleep(.1) + + return True + +class DFUMode(object): + def __init__(self): + pass + def run(self, ser, ctrl_pipe, data_pipe): + """Tries to contact DStat and get version. Returns a tuple of + (major, minor). If no response, returns empty tuple. + + Arguments: + ser_port -- address of serial port to use + """ + status = None + try: + ser.write(b'!2\n') + exp_logger.info('!2') + + for i in range(10): + if ser.readline().rstrip()==b"@ACK 2": + dstat_logger.info('@ACK 2') + ser.write(b'SF\n') + exp_logger.info('SF') + status = "DONE" + time.sleep(5) + break + else: + time.sleep(.5) + ser.reset_input_buffer() + ser.write(b'!2\n') + exp_logger.info('!2') + time.sleep(.1) + except UnboundLocalError as e: + status = "SERIAL_ERROR" + except serial.SerialException as e: + logger.error('SerialException: %s', e) + status = "SERIAL_ERROR" + finally: + return status if __name__ == "__main__": log_handler = logging.StreamHandler() @@ -250,6 +367,6 @@ if __name__ == "__main__": logger.setLevel(level=logging.INFO) logger.addHandler(log_handler) - check_dstat() + dstat_enter_dfu() time.sleep(2) dfu_program(sys.argv[1]) \ No newline at end of file diff --git a/dstat_interface/core/interface/dstatinterface.glade b/dstat_interface/core/interface/dstatinterface.glade index c9b7201..ed72fee 100644 --- a/dstat_interface/core/interface/dstatinterface.glade +++ b/dstat_interface/core/interface/dstatinterface.glade @@ -49,6 +49,11 @@ Thanks to Christian Fobel for help with Dropbot Plugin center gtk-missing-image + + True + False + image7 + True False @@ -182,6 +187,15 @@ Thanks to Christian Fobel for help with Dropbot Plugin False + + + Firmware… + True + False + image2 + False + + diff --git a/dstat_interface/core/interface/hw_info.py b/dstat_interface/core/interface/hw_info.py index cb89367..d8e6b82 100755 --- a/dstat_interface/core/interface/hw_info.py +++ b/dstat_interface/core/interface/hw_info.py @@ -27,7 +27,7 @@ class InfoDialog(object): self.dialog.format_secondary_text( "PCB Version: {}.{}\n".format( state.dstat_version[0], state.dstat_version[1]) + - "Firmware Version:{}".format(state.firmware_version) + "Firmware Version: {}".format(state.firmware_version) ) self.dialog.connect('response', self.destroy) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index fe0c77d..65113ab 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -145,7 +145,15 @@ class Main(object): parent=self.mainwindow, connect=self.menu_dstat_info ) - + self.menu_dstat_fw = self.builder.get_object('menu_dstat_fw') + self.menu_dstat_fw.set_sensitive(False) + self.dstat_fw_window = dstat.dfu.FWDialog( + parent=self.mainwindow, + connect=self.menu_dstat_fw, + stop_callback=self.stop_ocp, + disconnect_callback=self.on_serial_disconnect_clicked + ) + # Set Version Strings try: ver = getVersion() @@ -303,6 +311,7 @@ class Main(object): self.startbutton.set_sensitive(False) self.stopbutton.set_sensitive(False) self.menu_dstat_info.set_sensitive(False) + self.menu_dstat_fw.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) def start_ocp(self, data=None): @@ -353,6 +362,9 @@ class Main(object): pass self.ocp_disp.set_text("") self.ocp_is_running = False + self.startbutton.set_sensitive(True) + self.menu_dstat_info.set_sensitive(True) + self.menu_dstat_fw.set_sensitive(True) else: logger.error("OCP measurements not supported on v1.1 boards.") return @@ -361,6 +373,7 @@ class Main(object): if self.ocp_is_running: self.startbutton.set_sensitive(True) self.menu_dstat_info.set_sensitive(True) + self.menu_dstat_fw.set_sensitive(True) return False else: return True -- GitLab From 6ef02794e0596091923948686f084afe842f75b9 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 20 Jun 2017 20:31:55 -0400 Subject: [PATCH 034/111] dfu: remove unneeded import. --- dstat_interface/core/dstat/dfu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/dstat/dfu.py b/dstat_interface/core/dstat/dfu.py index c9c5c24..f55ce13 100755 --- a/dstat_interface/core/dstat/dfu.py +++ b/dstat_interface/core/dstat/dfu.py @@ -41,7 +41,7 @@ logger = logging.getLogger(__name__) try: import gi gi.require_version('Gtk', '3.0') - from gi.repository import Gtk, GObject + from gi.repository import Gtk except ImportError: print("ERR: GTK not available") sys.exit(1) -- GitLab From 76ff0d01ef928fb3c48ff7accb75f97fcfe78431 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 23 Jun 2017 18:53:54 -0400 Subject: [PATCH 035/111] =?UTF-8?q?Use=20proper=20version=20string=20compa?= =?UTF-8?q?risons.=20Needs=20=E2=80=98-=E2=80=98=20separator=20in=20versio?= =?UTF-8?q?n=20string=20added=20in=20dstat/dstat-firmware@c5f9701a3c2f297c?= =?UTF-8?q?987eb18b6fbdb8b0399a292b.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dstat_interface/core/dstat/comm.py | 38 ++++++++++--------- .../core/experiments/experiment_template.py | 17 ++++----- dstat_interface/core/interface/adc_pot.py | 27 ++++++++----- dstat_interface/core/interface/hw_info.py | 3 +- dstat_interface/main.py | 36 +++++++----------- 5 files changed, 61 insertions(+), 60 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 1d641be..12e24dc 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -25,6 +25,7 @@ import multiprocessing as mp from collections import OrderedDict import logging +from pkg_resources import parse_version try: import gi gi.require_version('Gtk', '3.0') @@ -259,29 +260,31 @@ class VersionCheck(object): ser.reset_input_buffer() break - parted = input.rstrip().split('.') + pcb, sep, firmware = input.strip().rpartition('-') - try: - logger.info( - "PCB Version: {}".format(".".join(parted[:2])) - ) + if pcb == "": + pcb = firmware + firmware = False + logger.info("Your firmware does not support version detection.") + + data_pipe.send((pcb, False)) + + else: logger.info( "Firmware Version: {}".format( - hex(int(parted[2])).lstrip('0x') + hex(int(firmware)).lstrip('0x') ) ) data_pipe.send(( - ( int(parted[0]), int(parted[1]) ), - hex(int(parted[2])).lstrip('0x') - )) - except IndexError: - logger.info("Your firmware does not support version detection.") - data_pipe.send(( - ( int(parted[0]), int(parted[1]) ), - False + pcb, + hex(int(firmware)).lstrip('0x') )) - finally: - status = "DONE" + + logger.info( + "PCB Version: {}".format(pcb) + ) + + status = "DONE" except UnboundLocalError as e: status = "SERIAL_ERROR" @@ -309,7 +312,8 @@ def version_check(ser_port): return False else: buffer = state.ser.get_data(block=True) - state.dstat_version, state.firmware_version = buffer + version, state.firmware_version = buffer + state.dstat_version = parse_version(version) logger.debug("version_check done") time.sleep(.1) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 24e3bfb..f4b143d 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -25,6 +25,8 @@ from copy import deepcopy from math import ceil import time +from pkg_resources import parse_version + try: import gi gi.require_version('Gtk', '3.0') @@ -70,17 +72,14 @@ class Experiment(object): self.scan = 0 self.time = 0 self.plots = [] - - major, minor = state.dstat_version - if major >= 1: - if minor == 1: + if state.dstat_version < parse_version('1.2'): 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'] + elif state.dstat_version >= parse_version('1.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.") diff --git a/dstat_interface/core/interface/adc_pot.py b/dstat_interface/core/interface/adc_pot.py index 2624946..56e73dd 100755 --- a/dstat_interface/core/interface/adc_pot.py +++ b/dstat_interface/core/interface/adc_pot.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os.path +from pkg_resources import parse_version try: import gi @@ -28,6 +29,7 @@ except ImportError: sys.exit(1) from ..errors import InputError, VarError +from ..dstat import state mod_dir = os.path.dirname(os.path.abspath(__file__)) @@ -138,15 +140,22 @@ class adc_pot(object): for i in self.ui: self.ui[i].set_active(self._params[i]) - def set_version(self, version): + def set_version(self): """ Sets menus for DStat version. """ - self.gain_liststore.clear() - if version[0] == 1: - if version[1] == 1: - for i in v1_1_gain: - self.gain_liststore.append(str(i)) - elif version[1] >= 2: - for i in v1_2_gain: - self.gain_liststore.append(i) + try: + if self.version == state.dstat_version: + return + except AttributeError: + pass + self.version = state.dstat_version + + self.gain_liststore.clear() + + if state.dstat_version < parse_version('1.2'): + for i in v1_1_gain: + self.gain_liststore.append(str(i)) + elif state.dstat_version >= parse_version('1.2'): + for i in v1_2_gain: + self.gain_liststore.append(i) \ No newline at end of file diff --git a/dstat_interface/core/interface/hw_info.py b/dstat_interface/core/interface/hw_info.py index cb89367..f0806dc 100755 --- a/dstat_interface/core/interface/hw_info.py +++ b/dstat_interface/core/interface/hw_info.py @@ -25,8 +25,7 @@ class InfoDialog(object): self.dialog = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, "DStat Info") self.dialog.format_secondary_text( - "PCB Version: {}.{}\n".format( - state.dstat_version[0], state.dstat_version[1]) + + "PCB Version: {}\n".format(state.dstat_version.base_version) + "Firmware Version:{}".format(state.firmware_version) ) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index fe0c77d..3a08c0e 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -31,6 +31,7 @@ from collections import OrderedDict from datetime import datetime import logging +from pkg_resources import parse_version try: import gi gi.require_version('Gtk', '3.0') @@ -235,26 +236,18 @@ class Main(object): self.statusbar.remove_all(self.error_context_id) - if not len(dstat.state.dstat_version) == 2: - self.statusbar.push(self.error_context_id, - "Communication Error") - return - - else: - self.adc_pot.set_version(dstat.state.dstat_version) - self.statusbar.push( - self.error_context_id, - "DStat version: {}.{}".format( - str(dstat.state.dstat_version[0]), - str(dstat.state.dstat_version[1]) - ) - ) + self.adc_pot.set_version() + self.statusbar.push( + self.message_context_id, + "DStat version: {}".format( + dstat.state.dstat_version.base_version) + ) - dstat.comm.read_settings() + dstat.comm.read_settings() - self.start_ocp() - self.connected = True - self.serial_disconnect.set_sensitive(True) + self.start_ocp() + self.connected = True + self.serial_disconnect.set_sensitive(True) except: self.serial_connect.set_sensitive(True) @@ -307,9 +300,7 @@ class Main(object): def start_ocp(self, data=None): """Start OCP measurements.""" - if (dstat.state.dstat_version[0] >= 1 and - dstat.state.dstat_version[1] >= 2): - + if dstat.state.dstat_version >= parse_version('1.2'): # Flush data pipe dstat.state.ser.flush_data() @@ -339,8 +330,7 @@ class Main(object): def stop_ocp(self, data=None): """Stop OCP measurements.""" - if (dstat.state.dstat_version[0] >= 1 and - dstat.state.dstat_version[1] >= 2): + if dstat.state.dstat_version >= parse_version('1.2'): if self.pmt_mode == True: logger.info("Stop PMT idle mode") else: -- GitLab From 1a70ce34849fdd8281a0609f0c1fa25596233e50 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 23 Jun 2017 18:55:10 -0400 Subject: [PATCH 036/111] Add setuptools to requirements. --- pavement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pavement.py b/pavement.py index 9a19d79..fc19855 100644 --- a/pavement.py +++ b/pavement.py @@ -18,7 +18,8 @@ setup(name='dstat_interface', license='GPLv3', packages=find_packages(), install_requires=['matplotlib', 'numpy', 'pyserial', 'pyzmq', - 'pyyaml','seaborn', 'zmq-plugin>=0.2.post2'], + 'pyyaml','seaborn', 'setuptools', + 'zmq-plugin>=0.2.post2'], # Install data listed in `MANIFEST.in` include_package_data=True) -- GitLab From 4b328a01eb18ea14cfa9c0fb5395813e33fb58aa Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 23 Jun 2017 18:55:55 -0400 Subject: [PATCH 037/111] =?UTF-8?q?Catch=20exception=20when=20using=20a=20?= =?UTF-8?q?last=5Fparameters.yml=20file=20with=20an=20experiment=20that=20?= =?UTF-8?q?doesn=E2=80=99t=20exist.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dstat_interface/core/interface/exp_window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dstat_interface/core/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py index 89b59ab..d50405a 100755 --- a/dstat_interface/core/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -117,4 +117,7 @@ class Experiments(GObject.Object): return self.classes[experiment].params def set_params(self, experiment, parameters): - self.classes[experiment].params = parameters \ No newline at end of file + try: + self.classes[experiment].params = parameters + except KeyError as e: + logger.warning("Tried to load inavlid experiment with id %s", e.args) \ No newline at end of file -- GitLab From fb0c852fe0dfeb9a8345c79c19075676d3aa311b Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Tue, 11 Jul 2017 19:40:06 -0400 Subject: [PATCH 038/111] comm: Only show devices with DSTAT in device identifier. --- dstat_interface/core/dstat/comm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 12e24dc..08b61b2 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -479,11 +479,11 @@ class SerialDevices(object): """Retrieves and stores list of serial devices in self.ports""" def __init__(self): try: - self.ports, _, _ = zip(*list_ports.comports()) + self.ports, _, _ = zip(*list_ports.grep("DSTAT")) except ValueError: self.ports = [] logger.error("No serial ports found") def refresh(self): """Refreshes list of ports.""" - self.ports, _, _ = zip(*list_ports.comports()) \ No newline at end of file + self.ports, _, _ = zip(*list_ports.grep("DSTAT")) \ No newline at end of file -- GitLab From ee37ee6c5d05eb339bc56b25cfc554ce8152c068 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:19:52 -0400 Subject: [PATCH 039/111] DFU: Update message for Windows. --- dstat_interface/core/dstat/dfu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/dfu.py b/dstat_interface/core/dstat/dfu.py index f55ce13..b81f399 100755 --- a/dstat_interface/core/dstat/dfu.py +++ b/dstat_interface/core/dstat/dfu.py @@ -155,8 +155,8 @@ class FWDialog(object): Gtk.ButtonsType.OK, "Note about DFU") dialog.format_secondary_text("Click OK once the DStat has connected in " - + 'DFU mode. (Check Devices) Windows may not enumerate the DStat in' - + ' DFU mode. Try holding down the reset button while plugging the' + + "DFU mode. Windows doesn't seem to like the automatic reboot. " + + "Try holding down the reset button while plugging the " + 'USB port in (No LEDs should be lit), then click OK. Make sure ' + 'the DFU driver from the dfu-programmer directory is installed.') -- GitLab From c8cbdfc62cbadcb2918361979b0fdd929f1e3599 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:20:35 -0400 Subject: [PATCH 040/111] Reorder imports. --- dstat_interface/core/dstat/comm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 08b61b2..d3e793c 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import serial -from serial.tools import list_ports import time import struct import multiprocessing as mp @@ -33,6 +31,8 @@ try: except ImportError: print "ERR: GTK not available" sys.exit(1) +import serial +from serial.tools import list_ports from ..errors import InputError, VarError -- GitLab From 16f3eefd1a5881e2968e9d6c13a6e60f871b77fa Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:21:24 -0400 Subject: [PATCH 041/111] comm: Manually set RTS and refresh DTR status to avoid bug with setting RTS in Windows. --- dstat_interface/core/dstat/comm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index d3e793c..9354c73 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -118,6 +118,9 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): if ctrl_buffer == "DISCONNECT": ser_logger.info("DISCONNECT") + ser.rts = False + ser._update_dtr_state() # Need DTR update on Windows + ser.close() proc_pipe.send("DISCONNECT") return 0 @@ -129,6 +132,9 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): return_code = proc_pipe.recv().run(ser, ctrl_pipe, data_pipe) except serial.SerialException: proc_pipe.send("DISCONNECT") + ser.rts = False + ser._update_dtr_state() # Need DTR update on Windows + ser.close() return 0 ser_logger.info('Return code: %s', str(return_code)) -- GitLab From 1344279d20e457889a4f83f162c7e454378009cb Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:23:29 -0400 Subject: [PATCH 042/111] comm: Send proper disconnect signal. --- dstat_interface/core/dstat/comm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 9354c73..cb22ef0 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -223,9 +223,9 @@ class SerialConnection(GObject.Object): self.ctrl_pipe_p.send(ctrl) def disconnect(self): - self.send_ctrl('a') - time.sleep(.2) - self.proc.terminate() + logger.info("Disconnecting") + self.send_ctrl('DISCONNECT') + self.proc.join() self.emit('disconnected') self.connected = False -- GitLab From c1fb6d626756636ab9a62961a775cb0687660c89 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:23:53 -0400 Subject: [PATCH 043/111] comm: Fix bad comparison. --- dstat_interface/core/dstat/comm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index cb22ef0..5c3031b 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -107,8 +107,7 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): # These can only be called when no experiment is running if ctrl_pipe.poll(): ctrl_buffer = ctrl_pipe.recv() - - if ctrl_buffer == ('a' or "DISCONNECT"): + if ctrl_buffer in ('a', "DISCONNECT"): proc_pipe.send("ABORT") try: ser.write('a') -- GitLab From edf12a7469f1b6733ff1d7c6f2401a91b5c02cf9 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:25:25 -0400 Subject: [PATCH 044/111] main: Remove redundant disconnect command. --- dstat_interface/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 4d0df7c..48bfbcc 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -286,7 +286,6 @@ class Main(object): self.stop_ocp() else: self.on_pot_stop_clicked() - dstat.state.ser.send_ctrl("DISCONNECT") dstat.state.ser.disconnect() except AttributeError as err: -- GitLab From 89d484855bd8a8480584077b93cac8e54120c5ba Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 15:31:29 -0400 Subject: [PATCH 045/111] Update changelog --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index b64fecf..293155e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Version 1.4.4 + -Make connection code more robust + -Execute button disabled until DStat is ready to start + -Supports new firmware version strings added in dstat-firmware@c5f9701 + -Experimental firmware upgrade tool (see DStat Menu) + -Fix many bugs Version 1.4.3 -Fix another critical bug with Windows multiprocessing -Allow normal exit even if DStat was never connected -- GitLab From 9fd6b3b2000b0e6016de9ddb124474028c382234 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Fri, 14 Jul 2017 16:15:12 -0400 Subject: [PATCH 046/111] Fix weird import problem with anaconda install. --- 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 48bfbcc..2720a3c 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -47,8 +47,8 @@ import zmq mod_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') -if __name__ == "__parents_main__": # Only runs for forking emulation on win - sys.path.append(mod_dir) +# Fix path in installed modules +sys.path.append(mod_dir) from core.utils.version import getVersion import core.dstat as dstat -- GitLab From e037bd928dcd6f9f63074842551673969a672bfb Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 24 Jul 2017 18:56:26 -0400 Subject: [PATCH 047/111] Experiments: Add post-scan processing. --- .../core/experiments/experiment_template.py | 3 ++ dstat_interface/main.py | 28 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index f4b143d..28af450 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -346,6 +346,9 @@ class Experiment(object): return data + def scan_process(self, line): + pass + def experiment_done(self): """Runs when experiment is finished (all data acquired)""" self.data_to_pandas() diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 2720a3c..93c97c9 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -559,10 +559,18 @@ class Main(object): self.line, data = incoming if self.line > self.lastdataline: newline = True + try: + logger.info("running scan_process()") + self.current_exp.scan_process(self.lastdataline) + except AttributeError: + pass self.lastdataline = self.line else: newline = False self.current_exp.store_data(incoming, newline) + + if newline: + self.experiment_running_plot() except TypeError: pass incoming = dstat.state.ser.get_data() @@ -615,12 +623,19 @@ class Main(object): removed from GTK's queue. """ 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.scan_refresh and self.line > self.lastline): + while self.line > self.lastline: + # make sure all of last line is added + plot.updateline(self.current_exp, self.lastline) + self.lastline += 1 + plot.updateline(self.current_exp, self.line) + plot.redraw() + else: + while self.line > self.lastline: + # make sure all of last line is added + plot.updateline(self.current_exp, self.lastline) + self.lastline += 1 + plot.updateline(self.current_exp, self.line) if plot.continuous_refresh is True or force_refresh is True: plot.redraw() @@ -631,6 +646,7 @@ class Main(object): copy data to raw data tab. Saves data if autosave enabled. """ try: + self.current_exp.scan_process(self.lastdataline) self.current_exp.experiment_done() GObject.source_remove(self.experiment_proc[0]) GObject.source_remove(self.plot_proc) # stop automatic plot update -- GitLab From a89d04e9ed01d926610a0a6e82692864168e08f8 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 24 Jul 2017 18:56:40 -0400 Subject: [PATCH 048/111] Cleanup --- dstat_interface/core/experiments/experiment_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 28af450..d39b6cc 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -74,7 +74,7 @@ class Experiment(object): self.plots = [] if state.dstat_version < parse_version('1.2'): - self.__gaintable = [1e2, 3e2, 3e3, 3e4, 3e5, 3e6, 3e7, 5e8] + self.__gaintable = [1e2, 3e2, 3e3, 3e4, 3e5, 3e6, 3e7, 5e8] elif state.dstat_version >= parse_version('1.2'): self.__gaintable = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] self.__gain_trim_table = ['r100_trim', 'r100_trim', 'r3k_trim', -- GitLab From ea6b2150a5a22abfa75980f74e1ac79840c69c21 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 26 Sep 2017 17:38:04 -0400 Subject: [PATCH 049/111] Experiments: Change pandas output functions to support different naming parameters. --- .../core/experiments/experiment_template.py | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index d39b6cc..e87ce21 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -375,15 +375,49 @@ class Experiment(object): self.df = OrderedDict() for name, data in self.data.items(): - df = DataFrame(columns=['Scan'] + self.columns) + try: + df = DataFrame( + columns=['Scan'] + list(self.plot_format[name]['labels'])) - for n, line in enumerate(data): - df = df.append(DataFrame( - OrderedDict(zip(['Scan'] + self.columns, - [n] + list(line)) - ) - ), ignore_index = True - ) + for n, line in enumerate(data): # Each scan + df = df.append( + DataFrame( + OrderedDict(zip( + ['Scan'] + list(self.plot_format[name]['labels']), + [n] + list(line)) + ) + ), ignore_index = True + ) + except (AttributeError, KeyError): + try: + df = DataFrame( + columns=['Scan'] + list(self.columns)) + + for n, line in enumerate(data): # Each scan + df = df.append( + DataFrame( + OrderedDict(zip( + ['Scan'] + list(self.columns), + [n] + list(line)) + ) + ), ignore_index = True + ) + except AttributeError as e: # Fallback if no self.columns + df = DataFrame( + columns=['Scan'] + ["{}{}".format(name, n) + for n in range(len(data))] + ) + + for n, line in enumerate(data): + df = df.append( + DataFrame( + OrderedDict(zip( + ['Scan'] + ["{}{}".format(name, n) + for n in range(len(data))], + [n] + list(line)) + ) + ), ignore_index = True + ) self.df[name] = df -- GitLab From 6fda800f7c6ddca070041fc9c0726ebb0b78ea65 Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 24 Jul 2017 19:03:34 -0400 Subject: [PATCH 050/111] Experiments: Add single scan refresh attribute. --- dstat_interface/core/experiments/experiment_template.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index e87ce21..c70b1c1 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -448,6 +448,7 @@ class PlotBox(object): """Initializes plots. self.box should be reparented.""" self.name = "Main" self.continuous_refresh = True + self.scan_refresh = False self.plotnames = plots self.subplots = {} -- GitLab From 9015b5302fcc7a916093c5bfc63853de5299d3bf Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Mon, 24 Jul 2017 19:04:03 -0400 Subject: [PATCH 051/111] Experiments: Deal with new scan data more robustly. --- .../core/experiments/experiment_template.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index c70b1c1..aa3602c 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -514,11 +514,19 @@ class PlotBox(object): 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]) - + while True: + try: + 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]) + except IndexError: + self.addline() + except KeyError: + pass + else: + break + # logger.warning("Tried to set line %s that doesn't exist.", line_number) 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. -- GitLab From 97884a7f38bf4299657c3495af53537b001dd3b7 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 28 Sep 2017 15:59:10 -0400 Subject: [PATCH 052/111] Fix package import bug. --- dstat_interface/core/dstat/__init__.py | 3 +++ dstat_interface/main.py | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/dstat_interface/core/dstat/__init__.py b/dstat_interface/core/dstat/__init__.py index e69de29..1fb1ae9 100644 --- a/dstat_interface/core/dstat/__init__.py +++ b/dstat_interface/core/dstat/__init__.py @@ -0,0 +1,3 @@ +from . import comm +from . import dfu +from . import state \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 93c97c9..7c2a9aa 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -47,18 +47,17 @@ import zmq mod_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') -# Fix path in installed modules -sys.path.append(mod_dir) - -from core.utils.version import getVersion -import core.dstat as dstat -from core.experiments import idle, pot -from core import params, analysis -from core.interface import (exp_window, adc_pot, plot_ui, data_view, +# if __name__ == "__parents_main__": # Only runs for forking emulation on win +# sys.path.append(mod_dir) + +from .core.utils.version import getVersion +from .core.experiments import idle, pot +from .core import params, analysis, dstat +from .core.interface import (exp_window, adc_pot, plot_ui, data_view, save, hw_info) -from core.errors import InputError -from core.plugin import DstatPlugin, get_hub_uri - +from .core.errors import InputError +from .core.plugin import DstatPlugin, get_hub_uri + # Setup Logging logger = logging.getLogger(__name__) core_logger = logging.getLogger("core") -- GitLab From 59532996f2696096762c712382d9551e0639d6d5 Mon Sep 17 00:00:00 2001 From: Unknown Date: Thu, 28 Sep 2017 16:00:20 -0400 Subject: [PATCH 053/111] Add state reset function on disconnect. --- dstat_interface/core/dstat/state.py | 7 +++++++ dstat_interface/main.py | 1 + 2 files changed, 8 insertions(+) diff --git a/dstat_interface/core/dstat/state.py b/dstat_interface/core/dstat/state.py index 785c553..4240ee0 100644 --- a/dstat_interface/core/dstat/state.py +++ b/dstat_interface/core/dstat/state.py @@ -1,5 +1,12 @@ from collections import OrderedDict +def reset(): + settings = OrderedDict() + ser = None + dstat_version = None + firmware_version = None + board_instance = None + settings = OrderedDict() ser = None dstat_version = None diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 7c2a9aa..d4f05e6 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -304,6 +304,7 @@ class Main(object): self.menu_dstat_info.set_sensitive(False) self.menu_dstat_fw.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) + dstat.state.reset() def start_ocp(self, data=None): """Start OCP measurements.""" -- GitLab From 72f39f0c31df7f4058bd1d9571b42735691fa70b Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 14:30:30 -0400 Subject: [PATCH 054/111] Ignore pycharm files. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 868139a..d2c8521 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ setup.py *.egg-info last_params last_params.yml + +\.idea/ -- GitLab From 45f79d6fa74903c31cd65c62b3666b223a282aa1 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:03:32 -0400 Subject: [PATCH 055/111] Added new board definition file --- dstat_interface/core/dstat/__init__.py | 1 + dstat_interface/core/dstat/boards.py | 141 ++++++++++ dstat_interface/core/dstat/state.py | 3 +- dstat_interface/core/experiments/cv.py | 46 +++- .../core/experiments/experiment_template.py | 32 +-- dstat_interface/core/experiments/idle.py | 6 +- dstat_interface/core/experiments/lsv.py | 32 ++- .../core/experiments/parameter_test.py | 242 ------------------ dstat_interface/core/experiments/pot.py | 3 +- dstat_interface/core/experiments/swv.py | 98 +++++-- dstat_interface/core/interface/adc_pot.py | 38 +-- dstat_interface/core/interface/exp_int.py | 4 +- dstat_interface/main.py | 3 + 13 files changed, 307 insertions(+), 342 deletions(-) create mode 100755 dstat_interface/core/dstat/boards.py delete mode 100755 dstat_interface/core/experiments/parameter_test.py diff --git a/dstat_interface/core/dstat/__init__.py b/dstat_interface/core/dstat/__init__.py index 1fb1ae9..d5a2029 100644 --- a/dstat_interface/core/dstat/__init__.py +++ b/dstat_interface/core/dstat/__init__.py @@ -1,3 +1,4 @@ +from . import boards from . import comm from . import dfu from . import state \ No newline at end of file diff --git a/dstat_interface/core/dstat/boards.py b/dstat_interface/core/dstat/boards.py new file mode 100755 index 0000000..c5d215b --- /dev/null +++ b/dstat_interface/core/dstat/boards.py @@ -0,0 +1,141 @@ +#!/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 . + +from __future__ import division, absolute_import, print_function, unicode_literals +import sys +import inspect +import logging + +from pkg_resources import parse_version, parse_requirements + +logger = logging.getLogger(__name__) + + +class BaseBoard(object): + pcb_version = 'x.x.x' + booster = False + + def __init__(self): + self.max_freq = 5000 + self.max_scans = 255 + self.max_time = 65535 + + self.setup() + assert len(self.gain) == self.gain_settings + assert len(self.gain_labels) == self.gain_settings + if self.gain_trim is not None: + assert len(self.gain_trim) == self.gain_settings + + def setup(self): + """Override in subclasses to provide correct numbers""" + self.gain = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] + self.gain_labels = ["Bypass", "100 Ω (15 mA FS)", "3 kΩ (500 µA FS)", + "30 kΩ (50 µA FS)", "300 kΩ (5 µA FS)", + "3 MΩ (500 nA FS)", "30 MΩ (50 nA FS)", + "100 MΩ (15 nA FS)" + ] + self.gain_trim = [None, 'r100_trim', 'r3k_trim', + 'r30k_trim', 'r300k_trim', 'r3M_trim', + 'r30M_trim', 'r100M_trim'] + self.gain_settings = len(self.gain) + self.gain_default_index = 2 + self.re_voltage_scale = 1 + + def test_mv(self, mv): + """Return true if voltage in mV is in range.""" + dac = float(mv)*self.re_voltage_scale/(3000./65536) + 32768 + + if 0 <= dac <= 65535: + return True + else: + return False + + def test_freq(self, hz): + """Return true if frequency in Hz is in range.""" + return 0 < float(hz) < self.max_freq + + def test_scans(self, n): + """Return true if number of scans is valid.""" + return 0 < int(n) < self.max_scans + + def test_s(self, s): + """Return true if time in integer seconds is valid.""" + return 0 < int(s) < self.max_time + +class V1_1Board(BaseBoard): + pcb_version = '1.1' + + def setup(self): + self.gain = [1e2, 3e2, 3e3, 3e4, 3e5, 3e6, 3e7, 5e8] + self.gain_labels = [None, "300 Ω (5 mA FS)", + "3 kΩ (500 µA FS)", "30 kΩ (50 µA FS)", + "300 kΩ (5 µA FS)", "3 MΩ (500 nA FS)", + "30 MΩ (50 nA FS)", "500 MΩ (3 nA FS)" + ] + self.gain_trim = None + self.gain_settings = len(self.gain) + self.gain_default_index = 2 + self.re_voltage_scale = 1 + + +class V1_2Board(BaseBoard): + pcb_version = '1.2' + + def setup(self): + self.gain = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] + self.gain_labels = ["Bypass", "100 Ω (15 mA FS)", "3 kΩ (500 µA FS)", + "30 kΩ (50 µA FS)", "300 kΩ (5 µA FS)", + "3 MΩ (500 nA FS)", "30 MΩ (50 nA FS)", + "100 MΩ (15 nA FS)" + ] + self.gain_trim = [None, 'r100_trim', 'r3k_trim', + 'r30k_trim', 'r300k_trim', 'r3M_trim', + 'r30M_trim', 'r100M_trim'] + self.gain_settings = len(self.gain) + self.gain_default_index = 2 + self.re_voltage_scale = 1 + + +def get_all_subclasses(cls): + all_subclasses = [] + + for subclass in cls.__subclasses__(): + all_subclasses.append(subclass) + all_subclasses.extend(get_all_subclasses(subclass)) + + return all_subclasses + + +def find_board(version, booster=False): + """Returns highest compatible board class or None if none available.""" + boards = get_all_subclasses(BaseBoard) + candidates = [] + for board in boards: + req = parse_requirements('dstat~={}'.format(board.pcb_version)).next() + if board.booster == booster and version in req: + candidates.append(board) + try: + picked = sorted(candidates, + key=lambda board: parse_version(board.pcb_version))[-1] + logger.info("Picked %s", picked) + return picked + except IndexError: + logger.warning("No matching board definition for ver: %s.", version) + return None diff --git a/dstat_interface/core/dstat/state.py b/dstat_interface/core/dstat/state.py index 4240ee0..12262c4 100644 --- a/dstat_interface/core/dstat/state.py +++ b/dstat_interface/core/dstat/state.py @@ -10,4 +10,5 @@ def reset(): settings = OrderedDict() ser = None dstat_version = None -firmware_version = None \ No newline at end of file +firmware_version = None +board_instance = None \ No newline at end of file diff --git a/dstat_interface/core/experiments/cv.py b/dstat_interface/core/experiments/cv.py index 475f11b..af828ea 100644 --- a/dstat_interface/core/experiments/cv.py +++ b/dstat_interface/core/experiments/cv.py @@ -23,19 +23,41 @@ class CVExp(Experiment): 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] += str(int( + int(self.parameters['clean_mV'])/ + self.re_voltage_scale* + (65536./3000)+32768 + )) + self.commands[2] += " " + self.commands[2] += str(int( + int(self.parameters['dep_mV'])/ + self.re_voltage_scale* + (65536./3000)+32768 + )) + self.commands[2] += " " + self.commands[2] += str(int( + int(self.parameters['v1'])/ + self.re_voltage_scale* + (65536./3000)+32768 + )) + self.commands[2] += " " + self.commands[2] += str(int( + int(self.parameters['v2'])/ + self.re_voltage_scale* + (65536./3000)+32768 + )) + self.commands[2] += " " + self.commands[2] += str(int( + int(self.parameters['start'])/ + self.re_voltage_scale* + (65536./3000)+32768 + )) self.commands[2] += " " self.commands[2] += str(self.parameters['scans']) self.commands[2] += " " - self.commands[2] += str(self.parameters['slope']) + self.commands[2] += str(int( + int(self.parameters['slope'])/ + self.re_voltage_scale* + (65536./3000) + )) self.commands[2] += " " \ No newline at end of file diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index aa3602c..959a9bf 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -73,22 +73,18 @@ class Experiment(object): self.time = 0 self.plots = [] - if state.dstat_version < parse_version('1.2'): - self.__gaintable = [1e2, 3e2, 3e3, 3e4, 3e5, 3e6, 3e7, 5e8] - elif state.dstat_version >= parse_version('1.2'): - self.__gaintable = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] - self.__gain_trim_table = ['r100_trim', 'r100_trim', 'r3k_trim', - 'r30k_trim', 'r300k_trim', 'r3M_trim', - 'r30M_trim', 'r100M_trim'] - else: - raise VarError(parameters['version'], "Invalid version parameter.") - - self.gain = self.__gaintable[int(self.parameters['gain'])] - self.gain_trim = int( - state.settings[ - self.__gain_trim_table[int(self.parameters['gain'])] - ][1] - ) + self.re_voltage_scale = state.board_instance.re_voltage_scale + + self.gain = state.board_instance.gain[int(self.parameters['gain'])] + + try: + self.gain_trim = int( + state.settings[ + state.board_instance.gain_trim[int(self.parameters['gain'])] + ][1] + ) + except AttributeError: + logger.debug("No gain trim table.") self.commands = ["EA", "EG"] @@ -318,8 +314,8 @@ class Experiment(object): scan, data = data_input voltage, 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 logging - -from errors import InputError - -logger = logging.getLogger(__name__) - -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: - if i in test_parameters: - parameters[i] = int(params[i]) - - if (parameters['clean_mV'] > 1499 or - parameters['clean_mV'] < -1500): - raise InputError(parameters['clean_mV'], - "Clean potential exceeds hardware limits.") - if (parameters['dep_mV'] > 1499 or - parameters['dep_mV'] < -1500): - raise InputError(parameters['dep_mV'], - "Deposition potential exceeds hardware limits.") - if (parameters['clean_s'] < 0): - raise InputError(parameters['clean_s'], - "Clean time cannot be negative.") - if (parameters['dep_s'] < 0): - raise InputError(parameters['dep_s'], - "Deposition time cannot be negative.") - if (parameters['start'] > 1499 or parameters['start'] < -1500): - raise InputError(parameters['start'], - "Start parameter exceeds hardware limits.") - if (parameters['stop'] > 1499 or parameters['stop'] < -1500): - raise InputError(parameters['stop'], - "Stop parameter exceeds hardware limits.") - if (parameters['slope'] > 2000 or parameters['slope'] < 1): - raise InputError(parameters['slope'], - "Slope parameter exceeds hardware limits.") - if parameters['start'] == parameters['stop']: - raise InputError(parameters['start'], - "Start cannot equal Stop.") - -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: - if i in test_parameters: - parameters[i] = int(params[i]) - - if (parameters['clean_mV'] > 1499 or - parameters['clean_mV'] < -1500): - raise InputError(parameters['clean_mV'], - "Clean potential exceeds hardware limits.") - if (parameters['dep_mV'] > 1499 or - parameters['dep_mV'] < -1500): - raise InputError(parameters['dep_mV'], - "Deposition potential exceeds hardware limits.") - if (parameters['clean_s'] < 0): - raise InputError(parameters['clean_s'], - "Clean time cannot be negative.") - if (parameters['dep_s'] < 0): - raise InputError(parameters['dep_s'], - "Deposition time cannot be negative.") - if (parameters['start'] > 1499 or parameters['start'] < -1500): - raise InputError(parameters['start'], - "Start parameter exceeds hardware limits.") - if (parameters['slope'] > 2000 or parameters['slope'] < 1): - raise InputError(parameters['slope'], - "Slope parameter exceeds hardware limits.") - if (parameters['v1'] > 1499 or parameters['v1'] < -1500): - raise InputError(parameters['v1'], - "Vertex 1 parameter exceeds hardware limits.") - if (parameters['v2'] > 1499 or parameters['v2'] < -1500): - raise InputError(parameters['v2'], - "Vertex 2 parameter exceeds hardware limits.") - if (parameters['scans'] < 1 or parameters['scans'] > 255): - raise InputError(parameters['scans'], - "Scans parameter outside limits.") - if parameters['v1'] == parameters['v2']: - raise InputError(parameters['v1'], - "Vertex 1 cannot equal Vertex 2.") - -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: - if i in test_parameters: - parameters[i] = int(params[i]) - - if params['cyclic_true'] : - if int(params['scans']) < 1: - raise InputError(params['scans'], - "Must have at least one scan.") - else: - params['scans'] = 0 - - # check parameters are within hardware limits (doesn't - # check if pulse will go out of bounds, but instrument - # checks this (I think)) - if (parameters['clean_mV'] > 1499 or - parameters['clean_mV'] < -1500): - raise InputError(parameters['clean_mV'], - "Clean potential exceeds hardware limits.") - if (parameters['dep_mV'] > 1499 or - parameters['dep_mV'] < -1500): - raise InputError(parameters['dep_mV'], - "Deposition potential exceeds hardware limits.") - if (parameters['clean_s'] < 0): - raise InputError(parameters['clean_s'], - "Clean time cannot be negative.") - if (parameters['dep_s'] < 0): - raise InputError(parameters['dep_s'], - "Deposition time cannot be negative.") - if (parameters['start'] > 1499 or parameters['start'] < -1500): - raise InputError(parameters['start'], - "Start parameter exceeds hardware limits.") - if (parameters['step'] > 200 or parameters['step'] < 1): - raise InputError(parameters['step'], - "Step height parameter exceeds hardware limits.") - if (parameters['stop'] > 1499 or parameters['stop'] < -1500): - raise InputError(parameters['stop'], - "Stop parameter exceeds hardware limits.") - if (parameters['pulse'] > 150 or parameters['pulse'] < 1): - raise InputError(parameters['pulse'], - "Pulse height parameter exceeds hardware limits.") - if (parameters['freq'] < 1 or parameters['freq'] > 1000): - raise InputError(parameters['freq'], - "Frequency parameter outside limits.") - if parameters['start'] == parameters['stop']: - raise InputError(parameters['start'], - "Start cannot equal Stop.") - -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: - if i in test_parameters: - parameters[i] = int(params[i]) - - if (parameters['clean_mV'] > 1499 or - parameters['clean_mV'] < -1500): - raise InputError(parameters['clean_mV'], - "Clean potential exceeds hardware limits.") - if (parameters['dep_mV'] > 1499 or - parameters['dep_mV'] < -1500): - raise InputError(parameters['dep_mV'], - "Deposition potential exceeds hardware limits.") - if (parameters['clean_s'] < 0): - raise InputError(parameters['clean_s'], - "Clean time cannot be negative.") - if (parameters['dep_s'] < 0): - raise InputError(parameters['dep_s'], - "Deposition time cannot be negative.") - if (parameters['start'] > 1499 or parameters['start'] < -1500): - raise InputError(parameters['start'], - "Start parameter exceeds hardware limits.") - if (parameters['step'] > 200 or parameters['step'] < 1): - raise InputError(parameters['step'], - "Step height parameter exceeds hardware limits.") - if (parameters['stop'] > 1499 or parameters['stop'] < -1500): - raise InputError(parameters['stop'], - "Stop parameter exceeds hardware limits.") - if (parameters['pulse'] > 150 or parameters['pulse'] < 1): - raise InputError(parameters['pulse'], - "Pulse height parameter exceeds hardware limits.") - if (parameters['period'] < 1 or parameters['period'] > 1000): - raise InputError(parameters['period'], - "Period parameter outside limits.") - if (parameters['width'] < 1 or parameters['width'] > 1000): - raise InputError(parameters['width'], - "Width parameter outside limits.") - if parameters['period'] <= parameters['width']: - raise InputError(parameters['width'], - "Width must be less than period.") - if parameters['start'] == parameters['stop']: - raise InputError(parameters['start'], - "Start cannot equal Stop.") - -def pd_test(parameters): - """Test PD parameters for sanity""" - - if (int(parameters['time']) <= 0): - raise InputError(parameters['time'], - "Time must be greater than zero.") - if (int(parameters['time']) > 65535): - raise InputError(parameters['time'], - "Time must fit in 16-bit counter.") - if (parameters['sync_true'] and parameters['shutter_true']): - if (float(parameters['sync_freq']) > 30 or - float(parameters['sync_freq']) <= 0): - raise InputError(parameters['sync_freq'], - "Frequency must be between 0 and 30 Hz.") - if (float(parameters['fft_start']) < 0 or - float(parameters['fft_start']) > float(parameters['time'])-1): - raise InputError(parameters['fft_start'], - "FFT must start between 0 and time-1.") - if float(parameters['fft_int']) < 0: - raise InputError( - parameters['fft_int'], - "Integral bandwidth must be greater than 0" - ) - -def pot_test(params): - """Test POT parameters for sanity""" - test_parameters = ['time'] - parameters = {} - for i in params: - if i in test_parameters: - parameters[i] = int(params[i]) - - if (int(parameters['time']) <= 0): - raise InputError(parameters['time'], - "Time must be greater than zero.") - if (int(parameters['time']) > 65535): - raise InputError(parameters['time'], - "Time must fit in 16-bit counter.") \ No newline at end of file diff --git a/dstat_interface/core/experiments/pot.py b/dstat_interface/core/experiments/pot.py index a14c928..cbf8232 100644 --- a/dstat_interface/core/experiments/pot.py +++ b/dstat_interface/core/experiments/pot.py @@ -44,7 +44,8 @@ class PotExp(Experiment): # 2*uint16 + int32 seconds, milliseconds, voltage = struct.unpack('= parse_version('1.2'): - for i in v1_2_gain: - self.gain_liststore.append(i) - \ No newline at end of file + for n, i in enumerate(self.version.gain_labels): + self.gain_liststore.append((n, i, str(n))) + + self.ui['gain_index'].set_active(self.version.gain_default_index) \ No newline at end of file diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index 6b271a0..8c3ecc8 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -146,9 +146,9 @@ class Chronoamp(ExpInterface): self.builder.get_object('potential_entry').get_text()) time = int(self.builder.get_object('time_entry').get_text()) - if (potential > 1499 or potential < -1500): + if not state.board_instance.test_mv(potential): raise ValueError("Potential out of range") - if (time < 1 or time > 65535): + if not state.board_instance.test_s(time): raise ValueError("Time out of range") self.model.append([potential, time]) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index d4f05e6..b648ed8 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -241,6 +241,9 @@ class Main(object): ) ) + dstat.state.board_instance = dstat.boards.find_board( + dstat.state.dstat_version, self.boost_mode)() + self.statusbar.remove_all(self.error_context_id) self.adc_pot.set_version() -- GitLab From 3a845054f8009d43ec9643c98d3cdda20d6edf8d Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:04:06 -0400 Subject: [PATCH 056/111] Fixed settings writing. --- dstat_interface/core/dstat/comm.py | 130 +++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 24 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 5c3031b..e721527 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -376,9 +376,106 @@ class Settings(object): for i in range(len(parted)): settings[parted[i].split('.')[0]] = [i, parted[i].split('.')[1]] - + return settings + def write_command(self, cmd, params=None, retry=5): + """Write command to serial with optional number of retries.""" + def get_reply(retries = 3): + while True: + reply = self.ser.readline().rstrip() + if reply.startswith('#'): + dstat_logger.info(reply) + elif reply == "": + retries -= 1 + if retries <= 0: + raise TransmitError + else: + return reply + + n = len(cmd) + if params is not None: + n_params = len(params) + + for _ in range(retry): + tries = 5 + while True: + time.sleep(0.2) + self.ser.reset_input_buffer() + self.ser.write('!{}\n'.format(n)) + time.sleep(.1) + + try: + reply = get_reply() + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + + if reply != "@ACK {}".format(n): + logger.warning("Expected ACK got: {}".format(reply)) + continue + + tries = 5 + + while True: + self.ser.write('{}\n'.format(cmd)) + try: + reply = get_reply() + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + + if reply != "@RCV {}".format(n): + logger.warning("Expected RCV got: {}".format(reply)) + continue + + if params is None: + return True + + tries = 5 + + while True: + try: + reply = get_reply() + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + + if reply != "@RQP {}".format(n_params): + logger.warning("Expected RQP got: {}".format(reply)) + continue + + tries = 5 + + for i in params: + while True: + self.ser.write(i + " ") + try: + reply = get_reply() + if reply == "@RCVC {}".format(i): + break + except TransmitError: + if tries <= 0: + continue + tries -= 1 + pass + else: + break + return True + return False + def write(self): write_buffer = range(len(self.settings)) @@ -387,28 +484,10 @@ class Settings(object): to_write = " ".join(write_buffer) + " " n = len(to_write) + logger.debug("to_write = %s", to_write) - 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 + if not self.write_command('SW' + to_write): + logger.error("Could not write command.") def read_settings(): """Tries to contact DStat and get settings. Returns dict of @@ -418,7 +497,8 @@ def read_settings(): state.ser.flush_data() state.ser.start_exp(Settings(task='r')) state.settings = state.ser.get_data(block=True) - + + logger.info("Read settings from DStat") logger.debug("read_settings: %s", state.ser.get_proc(block=True)) return @@ -427,9 +507,11 @@ def write_settings(): """Tries to write settings to DStat from global settings var. """ + logger.debug("Settings to write: %s", state.settings) + state.ser.flush_data() state.ser.start_exp(Settings(task='w', settings=state.settings)) - + logger.info("Wrote settings to DStat") logger.debug("write_settings: %s", state.ser.get_proc(block=True)) return -- GitLab From 811daad28d4525bc2db29aa70c5c85858838e29c Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:05:48 -0400 Subject: [PATCH 057/111] Formatting --- dstat_interface/core/experiments/experiment_template.py | 5 +++-- dstat_interface/core/interface/exp_int.py | 2 +- dstat_interface/main.py | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 959a9bf..ba316f8 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -34,7 +34,8 @@ try: 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 @@ -101,7 +102,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 = { diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index 8c3ecc8..2b00948 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -353,7 +353,7 @@ class PD(ExpInterface): self.bool[i].set_active(self._params[i]) self.builder.get_object('voltage_adjustment').set_value( - self._params['voltage'] ) + self._params['voltage']) def on_light_button_clicked(self, data=None): __main__.MAIN.on_pot_stop_clicked() diff --git a/dstat_interface/main.py b/dstat_interface/main.py index b648ed8..9001281 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -113,12 +113,12 @@ class Main(object): self.data_view = data_view.DataPage(self.main_notebook) self.info_page = data_view.InfoPage(self.main_notebook) - #fill adc_pot_box + # 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') self.adc_pot_container.reparent(self.adc_pot_box) - #fill serial + # fill serial self.serial_connect = self.builder.get_object('serial_connect') self.serial_pmt_connect = self.builder.get_object('pmt_mode') self.serial_disconnect = self.builder.get_object('serial_disconnect') @@ -126,7 +126,6 @@ class Main(object): self.serial_combobox = self.builder.get_object('serial_combobox') self.serial_combobox.pack_start(self.cell, True) self.serial_combobox.add_attribute(self.cell, 'text', 0) - self.serial_liststore = self.builder.get_object('serial_liststore') self.serial_devices = dstat.comm.SerialDevices() @@ -202,7 +201,6 @@ class Main(object): try: params.save_params(self, os.path.join(conf_path, 'last_params.yml')) - self.on_serial_disconnect_clicked() except KeyError: pass @@ -277,7 +275,7 @@ class Main(object): ) except IOError: logger.info("No previous parameters found.") - + def on_serial_disconnect_clicked(self, data=None): """Disconnect from DStat.""" if self.connected == False: @@ -307,6 +305,7 @@ class Main(object): self.menu_dstat_info.set_sensitive(False) self.menu_dstat_fw.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) + dstat.state.reset() def start_ocp(self, data=None): -- GitLab From 6cf1a873918fba87660d640399c1c7bbe9dbee51 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:07:26 -0400 Subject: [PATCH 058/111] Remove relative imports from main. --- dstat_interface/main.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 9001281..fcf6e43 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -30,8 +30,20 @@ from copy import deepcopy from collections import OrderedDict from datetime import datetime import logging - from pkg_resources import parse_version + +from serial import SerialException +import zmq + +import core +from core.utils.version import getVersion +from core.experiments import idle, pot +from core import params, analysis, dstat +from core.interface import (exp_window, adc_pot, plot_ui, data_view, + save, hw_info) +from core.errors import InputError +from core.plugin import DstatPlugin, get_hub_uri + try: import gi gi.require_version('Gtk', '3.0') @@ -40,24 +52,12 @@ except ImportError: print("ERR: GTK not available") sys.exit(1) -from serial import SerialException - -import zmq - mod_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') # if __name__ == "__parents_main__": # Only runs for forking emulation on win # sys.path.append(mod_dir) -from .core.utils.version import getVersion -from .core.experiments import idle, pot -from .core import params, analysis, dstat -from .core.interface import (exp_window, adc_pot, plot_ui, data_view, - save, hw_info) -from .core.errors import InputError -from .core.plugin import DstatPlugin, get_hub_uri - # Setup Logging logger = logging.getLogger(__name__) core_logger = logging.getLogger("core") -- GitLab From 9a020880fc6e98152ce0ef1943be6b18177f7e10 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:07:41 -0400 Subject: [PATCH 059/111] Formatting --- dstat_interface/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index fcf6e43..5a4655a 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -70,9 +70,10 @@ log_formatter = logging.Formatter( ) log_handler.setFormatter(log_formatter) -for i in loggers: - i.setLevel(level=logging.INFO) - i.addHandler(log_handler) +for log in loggers: + log.setLevel(level=logging.INFO) + log.addHandler(log_handler) + class Main(object): """Main program """ -- GitLab From 8169991a8a81e5c9248b050772315e38b379564c Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:09:24 -0400 Subject: [PATCH 060/111] Remove redundant variable. --- 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 5a4655a..d2ffdd6 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -210,7 +210,7 @@ class Main(object): def on_gtk_about_activate(self, menuitem, data=None): """Display the about window.""" - self.response = self.aboutdialog.run() # waits for user to click close + self.aboutdialog.run() # waits for user to click close self.aboutdialog.hide() def on_menu_analysis_options_activate(self, menuitem, data=None): -- GitLab From f9d91e9beb1b8d5815647e2ca06f2b6bcd9d0d02 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 11 Oct 2017 18:19:08 -0400 Subject: [PATCH 061/111] Handle exception when no DStats found on refreshing device list. --- dstat_interface/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index d2ffdd6..6d296f5 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -218,8 +218,11 @@ class Main(object): def on_serial_refresh_clicked(self, data=None): """Refresh list of serial devices.""" - self.serial_devices.refresh() - self.serial_liststore.clear() + try: + self.serial_devices.refresh() + self.serial_liststore.clear() + except ValueError: + logger.warning("No DStats found") for i in self.serial_devices.ports: self.serial_liststore.append([i]) -- GitLab From d9fed596e1ebf6757f4bbdaf4b93890751a7d217 Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 16 Oct 2017 14:17:30 -0400 Subject: [PATCH 062/111] Make sure DAC units are set on DStat. --- dstat_interface/main.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 6d296f5..926bf7a 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -257,6 +257,20 @@ class Main(object): dstat.comm.read_settings() + try: + if dstat.state.settings['dac_units_true'][1] != b'1': + dstat.state.settings['dac_units_true'][1] = b'1' + dstat.comm.write_settings() + except KeyError: + logger.warning("Connected DStat does not support sending DAC units.") + dialog = Gtk.MessageDialog( + self.window, 0, Gtk.MessageType.WARNING, + Gtk.ButtonsType.OK, "Connected DStat does not support sending DAC units." + + "Update firmware or set potentials will be incorrect!") + + dialog.run() + dialog.destroy() + self.start_ocp() self.connected = True self.serial_disconnect.set_sensitive(True) -- GitLab From a8cabc25a242ba6eb690a1ca69bb8487520dc769 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 13 Oct 2017 22:43:26 -0400 Subject: [PATCH 063/111] utility: Create experiment loop class --- .../core/utils/experiment_loops.py | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 dstat_interface/core/utils/experiment_loops.py diff --git a/dstat_interface/core/utils/experiment_loops.py b/dstat_interface/core/utils/experiment_loops.py new file mode 100644 index 0000000..2099fd5 --- /dev/null +++ b/dstat_interface/core/utils/experiment_loops.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# 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 . + +from __future__ import division, absolute_import, print_function, unicode_literals + +from ..dstat import state + +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) + +logger = logging.getLogger(__name__) + + +class BaseLoop(GObject.GObject): + __gsignals__ = { + b'experiment_done': (GObject.SIGNAL_RUN_FIRST, None, tuple()), + b'progress_update': (GObject.SIGNAL_RUN_FIRST, None, (float,)) + } + + def __init__(self, experiment): + GObject.GObject.__init__(self) + self.line = None + self.lastdataline = 0 + self.current_exp = experiment + self.experiment_proc = None + + def run(self): + self.experiment_proc = ( + GObject.idle_add(self.experiment_running_data), + GObject.idle_add(self.experiment_running_proc), + GObject.timeout_add(100, self.update_progress) + ) + + def experiment_running_data(self): + """Receive data from experiment process and add to + current_exp.data['data]. + Run in GTK main loop. + + Returns: + True -- when experiment is continuing to keep function in GTK's queue. + False -- when experiment process signals EOFError or IOError to remove + function from GTK's queue. + """ + try: + incoming = state.ser.get_data() + while incoming is not None: + try: + self.line, data = incoming + if self.line > self.lastdataline: + newline = True + try: + logger.info("running scan_process()") + self.current_exp.scan_process(self.lastdataline) + except AttributeError: + pass + 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: + logger.error(err) + self.experiment_done() + return False + except IOError as err: + logger.error(err) + self.experiment_done() + return False + + def experiment_running_proc(self): + """Receive proc signals from experiment process. + Run in GTK main loop. + + Returns: + True -- when experiment is continuing to keep function in GTK's queue. + False -- when experiment process signals EOFError or IOError to remove + function from GTK's queue. + """ + try: + 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": + self.on_serial_disconnect_clicked() + + else: + logger.warning("Unrecognized experiment return code: %s", + proc_buffer) + return False + return True + + except EOFError as err: + logger.warning("EOFError: %s", err) + self.experiment_done() + return False + except IOError as err: + logger.warning("IOError: %s", err) + self.experiment_done() + return False + + def experiment_done(self): + logger.info("Experiment done") + self.current_exp.experiment_done() + for proc in self.experiment_proc: + GObject.source_remove(proc) + self.emit("experiment_done") + + def update_progress(self): + progress = self.current_exp.get_progress() + self.emit("progress_update", progress) + return True -- GitLab From 0ed7b16e004650a72de2ccc45889b8e1978bd827 Mon Sep 17 00:00:00 2001 From: Unknown Date: Fri, 13 Oct 2017 22:44:07 -0400 Subject: [PATCH 064/111] Fix circular import. --- dstat_interface/core/dstat/__init__.py | 1 - dstat_interface/main.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/dstat_interface/core/dstat/__init__.py b/dstat_interface/core/dstat/__init__.py index d5a2029..1fb1ae9 100644 --- a/dstat_interface/core/dstat/__init__.py +++ b/dstat_interface/core/dstat/__init__.py @@ -1,4 +1,3 @@ -from . import boards from . import comm from . import dfu from . import state \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 926bf7a..efccfe4 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -35,10 +35,10 @@ from pkg_resources import parse_version from serial import SerialException import zmq -import core from core.utils.version import getVersion from core.experiments import idle, pot from core import params, analysis, dstat +from core.dstat import boards from core.interface import (exp_window, adc_pot, plot_ui, data_view, save, hw_info) from core.errors import InputError @@ -243,7 +243,7 @@ class Main(object): ) ) - dstat.state.board_instance = dstat.boards.find_board( + dstat.state.board_instance = boards.find_board( dstat.state.dstat_version, self.boost_mode)() self.statusbar.remove_all(self.error_context_id) -- GitLab From c1ecbea433def12dfa0d22609b655ba7ad4aec39 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 8 Nov 2017 17:58:35 -0500 Subject: [PATCH 065/111] Formatting --- dstat_interface/core/dstat/boards.py | 5 +++-- dstat_interface/core/experiments/idle.py | 12 +++++++----- dstat_interface/core/params.py | 7 +++++-- dstat_interface/main.py | 1 - 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/dstat_interface/core/dstat/boards.py b/dstat_interface/core/dstat/boards.py index c5d215b..f596028 100755 --- a/dstat_interface/core/dstat/boards.py +++ b/dstat_interface/core/dstat/boards.py @@ -42,7 +42,7 @@ class BaseBoard(object): assert len(self.gain_labels) == self.gain_settings if self.gain_trim is not None: assert len(self.gain_trim) == self.gain_settings - + def setup(self): """Override in subclasses to provide correct numbers""" self.gain = [1, 1e2, 3e3, 3e4, 3e5, 3e6, 3e7, 1e8] @@ -79,6 +79,7 @@ class BaseBoard(object): """Return true if time in integer seconds is valid.""" return 0 < int(s) < self.max_time + class V1_1Board(BaseBoard): pcb_version = '1.1' @@ -88,7 +89,7 @@ class V1_1Board(BaseBoard): "3 kΩ (500 µA FS)", "30 kΩ (50 µA FS)", "300 kΩ (5 µA FS)", "3 MΩ (500 nA FS)", "30 MΩ (50 nA FS)", "500 MΩ (3 nA FS)" - ] + ] self.gain_trim = None self.gain_settings = len(self.gain) self.gain_default_index = 2 diff --git a/dstat_interface/core/experiments/idle.py b/dstat_interface/core/experiments/idle.py index 02c3ef8..b8caf4d 100644 --- a/dstat_interface/core/experiments/idle.py +++ b/dstat_interface/core/experiments/idle.py @@ -7,6 +7,7 @@ from ..dstat import state class OCPExp(Experiment): """Open circuit potential measumement in statusbar.""" id = 'ocp' + def __init__(self): self.re_voltage_scale = state.board_instance.re_voltage_scale self.databytes = 8 @@ -16,18 +17,19 @@ class OCPExp(Experiment): self.commands[0] += "2 " # input buffer self.commands[0] += "3 " # 2.5 Hz sample rate self.commands[0] += "1 " # 2x PGA - - + def data_handler(self, data_input): """Overrides Experiment method to only send ADC values.""" scan, data = data_input # 2*uint16 + int32 seconds, milliseconds, voltage = struct.unpack(' Date: Wed, 8 Nov 2017 17:59:06 -0500 Subject: [PATCH 066/111] boards: hide internal function --- dstat_interface/core/dstat/boards.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/boards.py b/dstat_interface/core/dstat/boards.py index f596028..ae61e3f 100755 --- a/dstat_interface/core/dstat/boards.py +++ b/dstat_interface/core/dstat/boards.py @@ -119,14 +119,14 @@ def get_all_subclasses(cls): for subclass in cls.__subclasses__(): all_subclasses.append(subclass) - all_subclasses.extend(get_all_subclasses(subclass)) + all_subclasses.extend(__get_all_subclasses(subclass)) return all_subclasses def find_board(version, booster=False): """Returns highest compatible board class or None if none available.""" - boards = get_all_subclasses(BaseBoard) + boards = __get_all_subclasses(BaseBoard) candidates = [] for board in boards: req = parse_requirements('dstat~={}'.format(board.pcb_version)).next() -- GitLab From ab93e5a5ddca2fbce72539efbbfa2f7688b2d6db Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 9 Nov 2017 16:15:44 -0500 Subject: [PATCH 067/111] Fixed bad current conversion. --- dstat_interface/core/experiments/experiment_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index ba316f8..88dfa04 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -316,7 +316,7 @@ class Experiment(object): voltage, current = struct.unpack(' Date: Thu, 9 Nov 2017 16:17:04 -0500 Subject: [PATCH 068/111] Fix invalid attribute reference --- 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 4fc7e5c..f925e6f 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -243,7 +243,7 @@ class Main(object): ) dstat.state.board_instance = boards.find_board( - dstat.state.dstat_version, self.boost_mode)() + dstat.state.dstat_version)() self.statusbar.remove_all(self.error_context_id) -- GitLab From 62838c4931959da291503362fb48f5b399ac3f6a Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 9 Nov 2017 16:17:17 -0500 Subject: [PATCH 069/111] Missed function rename. --- dstat_interface/core/dstat/boards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/dstat/boards.py b/dstat_interface/core/dstat/boards.py index ae61e3f..da4927f 100755 --- a/dstat_interface/core/dstat/boards.py +++ b/dstat_interface/core/dstat/boards.py @@ -114,7 +114,7 @@ class V1_2Board(BaseBoard): self.re_voltage_scale = 1 -def get_all_subclasses(cls): +def __get_all_subclasses(cls): all_subclasses = [] for subclass in cls.__subclasses__(): -- GitLab From 2516ade4e66d2d10bc2aab717d4d072a35d1bbfc Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 19:56:49 -0500 Subject: [PATCH 070/111] Formatting --- dstat_interface/core/dstat/dfu.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dstat_interface/core/dstat/dfu.py b/dstat_interface/core/dstat/dfu.py index b81f399..3b56288 100755 --- a/dstat_interface/core/dstat/dfu.py +++ b/dstat_interface/core/dstat/dfu.py @@ -298,6 +298,7 @@ def dfu_program(path='./dstat-firmware.hex'): logger.error(e.output) raise + def dstat_enter_dfu(): """Tries to contact DStat and get version. Stores version in state. If no response, returns False, otherwise True. @@ -318,6 +319,7 @@ def dstat_enter_dfu(): return True + class DFUMode(object): def __init__(self): pass @@ -335,7 +337,7 @@ class DFUMode(object): exp_logger.info('!2') for i in range(10): - if ser.readline().rstrip()==b"@ACK 2": + if ser.readline().rstrip() == b"@ACK 2": dstat_logger.info('@ACK 2') ser.write(b'SF\n') exp_logger.info('SF') @@ -357,6 +359,7 @@ class DFUMode(object): finally: return status + if __name__ == "__main__": log_handler = logging.StreamHandler() log_formatter = logging.Formatter( -- GitLab From 3350f46ed349200d6dd6186d30a8e715711cccdb Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 19:57:15 -0500 Subject: [PATCH 071/111] Fix port refresh when no DStat connected. --- dstat_interface/core/dstat/comm.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index e721527..116c34d 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -565,12 +565,13 @@ def read_light_sensor(): class SerialDevices(object): """Retrieves and stores list of serial devices in self.ports""" def __init__(self): + self.ports = [] + self.refresh() + + def refresh(self): + """Refreshes list of ports.""" try: self.ports, _, _ = zip(*list_ports.grep("DSTAT")) except ValueError: self.ports = [] - logger.error("No serial ports found") - - def refresh(self): - """Refreshes list of ports.""" - self.ports, _, _ = zip(*list_ports.grep("DSTAT")) \ No newline at end of file + logger.error("No serial ports found") \ No newline at end of file -- GitLab From a7ea02263c71918c6c9c0b0592e7a278c7f7c40d Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 19:57:31 -0500 Subject: [PATCH 072/111] Fix PD mode --- dstat_interface/core/experiments/chronoamp.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/dstat_interface/core/experiments/chronoamp.py b/dstat_interface/core/experiments/chronoamp.py index eae24dd..fc234bf 100644 --- a/dstat_interface/core/experiments/chronoamp.py +++ b/dstat_interface/core/experiments/chronoamp.py @@ -83,29 +83,32 @@ class PDExp(Chronoamp): 'xlims' : (0, int(self.parameters['time'])) } } - + if self.parameters['shutter_true']: if self.parameters['sync_true']: self.commands.append("EZ") self.commands[-1] += str(self.parameters['sync_freq']) self.commands[-1] += " " else: - self.commands.append("E2") - - self.commands.append(("ER1 ", [])) - + self.commands.append(("E2", [])) + + command = "ER1 " + params = [] + if self.parameters['interlock_true']: - self.commands[-1][0] += "1 " + command += "1 " else: - self.commands[-1][0] += "0 " + command += "0 " if self.parameters['voltage'] == 0: # Special case where V=0 - self.commands[-1][1].append("65535") + params.append("65535") else: - self.commands[-1][1].append(str(int( + params.append(str(int( 65535-(self.parameters['voltage']*(65536./3000)))) ) - self.commands[-1][1].append(str(self.parameters['time'])) + params.append(str(self.parameters['time'])) + + self.commands.append((command, params)) if self.parameters['shutter_true']: if self.parameters['sync_true']: -- GitLab From f8296f4f44ed5c0e577476308dfa3881587fb5ff Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 19:57:48 -0500 Subject: [PATCH 073/111] Fix DPV parameters --- dstat_interface/core/experiments/swv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/experiments/swv.py b/dstat_interface/core/experiments/swv.py index 115d146..d0ff85a 100644 --- a/dstat_interface/core/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -182,13 +182,13 @@ class DPVExp(SWVExp): self.commands[2] += str(int( int(self.parameters['step'])/ self.re_voltage_scale* - (65536./3000)+32768 + (65536./3000) )) self.commands[2] += " " self.commands[2] += str(int( int(self.parameters['pulse'])/ self.re_voltage_scale* - (65536./3000)+32768 + (65536./3000) )) self.commands[2] += " " self.commands[2] += str(self.parameters['period']) -- GitLab From ba698f48c980a6d064bd228119d5c77f4424294e Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 19:58:00 -0500 Subject: [PATCH 074/111] Fix SWV parameters calculation. --- dstat_interface/core/experiments/swv.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dstat_interface/core/experiments/swv.py b/dstat_interface/core/experiments/swv.py index d0ff85a..17202d4 100644 --- a/dstat_interface/core/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -53,31 +53,31 @@ class SWVExp(Experiment): self.commands[2] += str(self.parameters['dep_s']) self.commands[2] += " " self.commands[2] += str(int( - int(self.parameters['clean_mV']). + int(self.parameters['clean_mV'])/ self.re_voltage_scale* (65536./3000)+32768 )) self.commands[2] += " " self.commands[2] += str(int( - int(self.parameters['dep_mV']). + int(self.parameters['dep_mV'])/ self.re_voltage_scale* (65536./3000)+32768 )) self.commands[2] += " " self.commands[2] += str(int( - int(self.parameters['start']). + int(self.parameters['start'])/ self.re_voltage_scale* (65536./3000)+32768 )) self.commands[2] += " " self.commands[2] += str(int( - int(self.parameters['stop']). + int(self.parameters['stop'])/ self.re_voltage_scale* (65536./3000)+32768 )) self.commands[2] += " " self.commands[2] += str(int( - int(self.parameters['step']). + int(self.parameters['step'])/ self.re_voltage_scale* (65536./3000) )) -- GitLab From 040e8693fc716d00622eb16047cb2a5fa0403e05 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 19:58:16 -0500 Subject: [PATCH 075/111] Add EEPROM reset option. --- .../core/interface/dstatinterface.glade | 14 ++++ dstat_interface/core/interface/hw_info.py | 79 ++++++++++++++++++- dstat_interface/main.py | 11 +++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/dstat_interface/core/interface/dstatinterface.glade b/dstat_interface/core/interface/dstatinterface.glade index ed72fee..635e737 100644 --- a/dstat_interface/core/interface/dstatinterface.glade +++ b/dstat_interface/core/interface/dstatinterface.glade @@ -84,6 +84,11 @@ Thanks to Christian Fobel for help with Dropbot Plugin False gtk-save-as + + True + False + image7 + @@ -196,6 +201,15 @@ Thanks to Christian Fobel for help with Dropbot Plugin False + + + Reset EEPROM + True + False + image9 + False + + diff --git a/dstat_interface/core/interface/hw_info.py b/dstat_interface/core/interface/hw_info.py index 8063ef7..3a8228b 100755 --- a/dstat_interface/core/interface/hw_info.py +++ b/dstat_interface/core/interface/hw_info.py @@ -1,8 +1,15 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- from __future__ import (absolute_import, division, print_function, unicode_literals) + +from ..dstat import state, dfu +from ..dstat.comm import dstat_logger, exp_logger + import logging +import time +import serial logger = logging.getLogger(__name__) @@ -14,8 +21,6 @@ except ImportError: print("ERR: GTK not available") sys.exit(1) -from ..dstat import state, dfu - class InfoDialog(object): def __init__(self, parent, connect, signal='activate'): self.parent = parent @@ -34,4 +39,72 @@ class InfoDialog(object): def destroy(self, object=None, data=None): self.dialog.destroy() - \ No newline at end of file + + +class ResetDialog(object): + def __init__(self, parent, connect, stop_callback, disconnect_callback, signal='activate'): + self.parent = parent + self.stop = stop_callback + self.disconnect = disconnect_callback + connect.connect(signal, self.activate) + + def activate(self, object=None, data=None): + dialog = Gtk.MessageDialog(self.parent, 0, Gtk.MessageType.WARNING, + Gtk.ButtonsType.OK_CANCEL, "EEPROM Reset") + dialog.format_secondary_text("This will reset the DStat's EEPROM settings, then disconneect." + ) + + response = dialog.run() + if response == Gtk.ResponseType.OK: + self.dstat_reset_eeprom() + dialog.destroy() + + def dstat_reset_eeprom(self): + """Tries to contact DStat and resets EEPROM. + If no response, returns False, otherwise True. + """ + self.stop() + exp = EEPROMReset() + state.ser.start_exp(exp) + logger.info("Resetting DStat EEPROM…") + while True: + result = state.ser.get_proc(block=True) + if result in ('SERIAL_ERROR', 'DONE', 'ABORT'): + break + logger.info(result) + + self.disconnect() + + +class EEPROMReset(object): + def __init__(self): + pass + + def run(self, ser, ctrl_pipe, data_pipe): + status = None + try: + ser.write(b'!2\n') + exp_logger.info('!2') + + for i in range(10): + if ser.readline().rstrip() == b"@ACK 2": + dstat_logger.info('@ACK 2') + ser.write(b'SD\n') + exp_logger.info('SD') + status = "DONE" + time.sleep(5) + break + else: + time.sleep(.5) + ser.reset_input_buffer() + ser.write(b'!2\n') + exp_logger.info('!2') + time.sleep(.1) + + except UnboundLocalError as e: + status = "SERIAL_ERROR" + except serial.SerialException as e: + logger.error('SerialException: %s', e) + status = "SERIAL_ERROR" + finally: + return status \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index f925e6f..ceba0d1 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -152,6 +152,14 @@ class Main(object): stop_callback=self.stop_ocp, disconnect_callback=self.on_serial_disconnect_clicked ) + self.menu_dstat_reset = self.builder.get_object('menu_dstat_reset') + self.menu_dstat_reset.set_sensitive(False) + self.menu_dstat_reset_window = hw_info.ResetDialog( + parent=self.mainwindow, + connect=self.menu_dstat_reset, + stop_callback=self.stop_ocp, + disconnect_callback=self.on_serial_disconnect_clicked + ) # Set Version Strings try: @@ -321,6 +329,7 @@ class Main(object): self.stopbutton.set_sensitive(False) self.menu_dstat_info.set_sensitive(False) self.menu_dstat_fw.set_sensitive(False) + self.menu_dstat_reset.set_sensitive(False) self.adc_pot.ui['short_true'].set_sensitive(True) dstat.state.reset() @@ -373,6 +382,7 @@ class Main(object): self.startbutton.set_sensitive(True) self.menu_dstat_info.set_sensitive(True) self.menu_dstat_fw.set_sensitive(True) + self.menu_dstat_reset.set_sensitive(True) else: logger.error("OCP measurements not supported on v1.1 boards.") return @@ -382,6 +392,7 @@ class Main(object): self.startbutton.set_sensitive(True) self.menu_dstat_info.set_sensitive(True) self.menu_dstat_fw.set_sensitive(True) + self.menu_dstat_reset.set_sensitive(True) return False else: return True -- GitLab From f45d92af091d904451e0a01f74fde7603d07bc63 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 14 Nov 2017 20:06:40 -0500 Subject: [PATCH 076/111] Update changelog --- CHANGELOG | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 293155e..b54a7d4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,15 +1,23 @@ +Version 1.4.5 + -Made board definitions modular + -Fix several bugs with experiment parameters + -Uses DAC unit based parameters now (**REQUIRES dstat-firmware@9e4a9f or higher**) + Version 1.4.4 -Make connection code more robust -Execute button disabled until DStat is ready to start -Supports new firmware version strings added in dstat-firmware@c5f9701 -Experimental firmware upgrade tool (see DStat Menu) -Fix many bugs + Version 1.4.3 -Fix another critical bug with Windows multiprocessing -Allow normal exit even if DStat was never connected -Store last parameters in user folder + Version 1.4.2 -Refactor to fix critical bug preventing running packaged versions. + Version 1.4.1 -Fixed voltage axis orientation for LSV, CV, SWV, and DPV (Thanks to Dan Bizzotto @ UBC) -Tweaked paver files to make version detection work without git. -- GitLab From a80f73fc2930cf5e560d164af1bab1e3b736bbec Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 15 Nov 2017 16:52:47 -0500 Subject: [PATCH 077/111] Change imports in main to relative imports (main needs to be launched as module). --- dstat_interface/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index ceba0d1..fa6e637 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -34,14 +34,14 @@ from pkg_resources import parse_version from serial import SerialException import zmq -from core.utils.version import getVersion -from core.experiments import idle, pot -from core import params, analysis, dstat -from core.dstat import boards -from core.interface import (exp_window, adc_pot, plot_ui, data_view, +from .core.utils.version import getVersion +from .core.experiments import idle, pot +from .core import params, analysis, dstat +from .core.dstat import boards +from .core.interface import (exp_window, adc_pot, plot_ui, data_view, save, hw_info) -from core.errors import InputError -from core.plugin import DstatPlugin, get_hub_uri +from .core.errors import InputError +from .core.plugin import DstatPlugin, get_hub_uri try: import gi -- GitLab From b5c65d40fe3b643881c7fcb0afacd6fd3225b25f Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 15 Nov 2017 19:14:06 -0500 Subject: [PATCH 078/111] Workaround for weird Gtk+3 redrawing bug on Windows --- dstat_interface/main.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index fa6e637..e181a93 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -24,6 +24,7 @@ from __future__ import division, absolute_import, print_function, unicode_litera import sys import os +import platform import multiprocessing import uuid from collections import OrderedDict @@ -761,6 +762,13 @@ class Main(object): self.completed_experiment_ids[self.active_experiment_id] =\ datetime.utcnow() + # Temporary fix for weird drawing bug on windows Gtk+3 + if platform.system() == 'Windows': + position = self.window.get_position() + self.window.hide() + self.window.show() + self.window.move(*position) + def on_pot_stop_clicked(self, data=None): """Stop current experiment. Signals experiment process to stop.""" try: -- GitLab From a418c7a07075a01deac0b59d6fcb68f415064d49 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 15 Nov 2017 19:14:38 -0500 Subject: [PATCH 079/111] Change package import structure again. Need to execute as module now. --- dstat_interface/main.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index e181a93..00df052 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -35,14 +35,14 @@ from pkg_resources import parse_version from serial import SerialException import zmq -from .core.utils.version import getVersion -from .core.experiments import idle, pot -from .core import params, analysis, dstat -from .core.dstat import boards -from .core.interface import (exp_window, adc_pot, plot_ui, data_view, +from dstat_interface.core.utils.version import getVersion +from dstat_interface.core.experiments import idle, pot +from dstat_interface.core import params, analysis, dstat +from dstat_interface.core.dstat import boards +from dstat_interface.core.interface import (exp_window, adc_pot, plot_ui, data_view, save, hw_info) -from .core.errors import InputError -from .core.plugin import DstatPlugin, get_hub_uri +from dstat_interface.core.errors import InputError +from dstat_interface.core.plugin import DstatPlugin, get_hub_uri try: import gi @@ -60,7 +60,7 @@ conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') # Setup Logging logger = logging.getLogger(__name__) -core_logger = logging.getLogger("core") +core_logger = logging.getLogger("dstat_interface.core") loggers = [logger, core_logger] log_handler = logging.StreamHandler() -- GitLab From c6b44352f9d36fe7610eb3371217deafe304cfe2 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 15 Nov 2017 19:14:45 -0500 Subject: [PATCH 080/111] Update changelog --- CHANGELOG | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index b54a7d4..066648f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ Version 1.4.5 -Made board definitions modular -Fix several bugs with experiment parameters -Uses DAC unit based parameters now (**REQUIRES dstat-firmware@9e4a9f or higher**) + -Change package import structure again (main must always be run as module now) + -Workaround for weird Gtk+3 redrawing bug on Windows Version 1.4.4 -Make connection code more robust @@ -17,7 +19,7 @@ Version 1.4.3 Version 1.4.2 -Refactor to fix critical bug preventing running packaged versions. - + Version 1.4.1 -Fixed voltage axis orientation for LSV, CV, SWV, and DPV (Thanks to Dan Bizzotto @ UBC) -Tweaked paver files to make version detection work without git. -- GitLab From c791df98bf6d6598daff9fde62cb7d6b91b96447 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 22 Nov 2017 13:54:11 -0500 Subject: [PATCH 081/111] experiment_template: Refactor ctrl pipe check. --- .../core/experiments/experiment_template.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 88dfa04..0f0c0e2 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -183,7 +183,7 @@ class Experiment(object): reply = get_reply() except TransmitError: if tries <= 0: - continue + break tries -= 1 pass else: @@ -262,24 +262,27 @@ class Experiment(object): data to self.data_pipe as result of self.data_handler). """ scan = 0 + + def check_ctrl(): + if self.ctrl_pipe.poll(): + input = self.ctrl_pipe.recv() + logger.info("serial_handler: %s", input) + if input == "DISCONNECT": + self.serial.write('a') + self.serial.reset_input_buffer() + logger.info("serial_handler: ABORT pressed!") + time.sleep(.3) + return False + elif input == 'a': + self.serial.write('a') + else: + self.serial.write(input) + try: while True: - if self.ctrl_pipe.poll(): - input = self.ctrl_pipe.recv() - logger.debug("serial_handler: %s", input) - if input == "DISCONNECT": - self.serial.write('a') - self.serial.reset_input_buffer() - logger.info("serial_handler: ABORT pressed!") - time.sleep(.3) - return False - elif input == 'a': - self.serial.write('a') - + check_ctrl() for line in self.serial: - if self.ctrl_pipe.poll(): - if self.ctrl_pipe.recv() == 'a': - self.serial.write('a') + check_ctrl() if line.startswith('B'): data = self.data_handler( -- GitLab From e5214c8c06927a7e68bb17be02631208f4ad40a7 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 22 Nov 2017 13:55:27 -0500 Subject: [PATCH 082/111] Make sure last experiment is deleted before starting new one. --- dstat_interface/main.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 00df052..93ebf39 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -511,7 +511,12 @@ class Main(object): self.startbutton.set_sensitive(False) self.stopbutton.set_sensitive(True) self.statusbar.remove_all(self.error_context_id) - + + try: + del self.current_exp + except AttributeError: + pass + self.current_exp = self.exp_window.setup_exp(parameters) plot_ui.replace_notebook_exp( -- GitLab From c8f51ea0f950b8a4d10361db51b2b420ddf54d7a Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 22 Nov 2017 13:58:43 -0500 Subject: [PATCH 083/111] PEP-8 stuff --- dstat_interface/core/dstat/comm.py | 5 +++-- dstat_interface/core/experiments/cal.py | 2 ++ dstat_interface/core/experiments/chronoamp.py | 2 +- .../core/experiments/experiment_template.py | 4 ++-- dstat_interface/core/interface/exp_int.py | 11 +++++++---- dstat_interface/main.py | 3 ++- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 116c34d..4f1c351 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -62,16 +62,17 @@ class TransmitError(Exception): super(TransmitError, self).__init__(self, "No reply received.") + def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger = logging.getLogger("{}._serial_process".format(__name__)) connected = False - - + for i in range(5): time.sleep(1) # Give OS time to enumerate try: ser = serial.Serial(ser_port, timeout=1) + # ser = serial.Serial(ser_port, timeout=1) ser_logger.info("Connecting") time.sleep(.5) connected = True diff --git a/dstat_interface/core/experiments/cal.py b/dstat_interface/core/experiments/cal.py index 3f1aff2..ed702c5 100755 --- a/dstat_interface/core/experiments/cal.py +++ b/dstat_interface/core/experiments/cal.py @@ -29,6 +29,7 @@ from ..errors import InputError, VarError from ..dstat import state from ..experiments.experiment_template import Experiment, dstat_logger + def measure_offset(time): gain_trim_table = [None, 'r100_trim', 'r3k_trim', 'r30k_trim', 'r300k_trim', 'r3M_trim', 'r30M_trim', 'r100M_trim'] @@ -46,6 +47,7 @@ def measure_offset(time): return gain_offset + class CALExp(Experiment): id = 'cal' """Offset calibration experiment""" diff --git a/dstat_interface/core/experiments/chronoamp.py b/dstat_interface/core/experiments/chronoamp.py index fc234bf..9cd519c 100644 --- a/dstat_interface/core/experiments/chronoamp.py +++ b/dstat_interface/core/experiments/chronoamp.py @@ -16,7 +16,7 @@ class ChronoampBox(PlotBox): subplot.ticklabel_format(style='sci', scilimits=(0, 3), useOffset=False, axis='y') subplot.plot([],[]) - + class Chronoamp(Experiment): id = 'cae' """Chronoamperometry experiment""" diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 0f0c0e2..78c7a74 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -57,6 +57,7 @@ logger = logging.getLogger(__name__) dstat_logger = logging.getLogger("{}.DSTAT".format(comm.__name__)) exp_logger = logging.getLogger("{}.Experiment".format(__name__)) + class Experiment(object): """Store and acquire a potentiostat experiment. Meant to be subclassed to by different experiment types and not used instanced directly. Subclass @@ -214,8 +215,7 @@ class Experiment(object): 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. + then sends self.commands. """ self.serial = ser self.ctrl_pipe = ctrl_pipe diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index 2b00948..7235400 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -40,6 +40,7 @@ logger = logging.getLogger(__name__) mod_dir = os.path.dirname(os.path.abspath(__file__)) + class ExpInterface(GObject.Object): """Generic experiment interface class. Should be subclassed to implement experiment interfaces by populating self.entry. Override class attributes @@ -67,7 +68,7 @@ class ExpInterface(GObject.Object): @property def params(self): """Dict of parameters""" - if self._params == None: + if self._params is None: self._fill_params() self._get_params() return self._params @@ -79,7 +80,7 @@ class ExpInterface(GObject.Object): @params.setter def params(self, params): - if self._params == None: + if self._params is None: self._fill_params() for i in self._params: try: @@ -99,7 +100,8 @@ class ExpInterface(GObject.Object): 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. @@ -489,7 +491,7 @@ class CAL(ExpInterface): 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): + if int(self.entry['time'].get_text()) <= 0 or int(self.entry['time'].get_text()) > 65535: logger.error("ERR: Time out of range") return @@ -526,6 +528,7 @@ class CAL(ExpInterface): GObject.timeout_add(700, restore_buttons, self.buttons) __main__.MAIN.spinner.stop() + def restore_buttons(buttons): """ Should be called with GObject callback """ for i in buttons: diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 93ebf39..b4a5b32 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -35,12 +35,13 @@ from pkg_resources import parse_version from serial import SerialException import zmq + from dstat_interface.core.utils.version import getVersion from dstat_interface.core.experiments import idle, pot from dstat_interface.core import params, analysis, dstat from dstat_interface.core.dstat import boards from dstat_interface.core.interface import (exp_window, adc_pot, plot_ui, data_view, - save, hw_info) + save, hw_info) from dstat_interface.core.errors import InputError from dstat_interface.core.plugin import DstatPlugin, get_hub_uri -- GitLab From ac6b99bd123a574dbb5cfd341a45a3b7d166f86b Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 22 Nov 2017 14:07:37 -0500 Subject: [PATCH 084/111] Fix calibration --- dstat_interface/core/experiments/cal.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/dstat_interface/core/experiments/cal.py b/dstat_interface/core/experiments/cal.py index ed702c5..3da9280 100755 --- a/dstat_interface/core/experiments/cal.py +++ b/dstat_interface/core/experiments/cal.py @@ -57,15 +57,16 @@ class CALExp(Experiment): self.scan = 0 self.data = [] - self.commands = ["EA2 3 1 ", "EG", "ER"] + self.commands = ["EA2 3 1 ", "EG"] 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 + + self.commands.append( + ("ER1 0", ["32768", str(self.parameters['time'])]) + ) + def serial_handler(self): """Handles incoming serial transmissions from DStat. Returns False @@ -100,6 +101,7 @@ class CALExp(Experiment): elif line.lstrip().startswith("@DONE"): dstat_logger.debug(line.lstrip().rstrip()) self.serial.flushInput() + self.experiment_done() return True except serial.SerialException: @@ -114,13 +116,16 @@ class CALExp(Experiment): seconds, milliseconds, current = struct.unpack(' Date: Wed, 22 Nov 2017 14:08:47 -0500 Subject: [PATCH 085/111] Remove redundant connection check. --- dstat_interface/core/dstat/comm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 4f1c351..b415087 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -186,7 +186,6 @@ class SerialConnection(GObject.Object): self.proc_pipe_p.send(exp) def stop_exp(self): - self.assert_connected() self.send_ctrl('a') def get_proc(self, block=False): -- GitLab From b3901db3d0f86920edd5d920c535aeadd0b4d847 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 22 Nov 2017 14:12:03 -0500 Subject: [PATCH 086/111] Allow sending arbitrary commands through ctrl pipe. --- dstat_interface/core/dstat/comm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index b415087..1d2c05b 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -124,6 +124,8 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser.close() proc_pipe.send("DISCONNECT") return 0 + else: + ser.write(ctrl_buffer) elif proc_pipe.poll(): while ctrl_pipe.poll(): -- GitLab From 18b6c4cc99203666c54d30a9ae1fea2163cd649e Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Wed, 22 Nov 2017 14:14:27 -0500 Subject: [PATCH 087/111] Fix missing imports. --- dstat_interface/core/experiments/chronoamp.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dstat_interface/core/experiments/chronoamp.py b/dstat_interface/core/experiments/chronoamp.py index 9cd519c..5998178 100644 --- a/dstat_interface/core/experiments/chronoamp.py +++ b/dstat_interface/core/experiments/chronoamp.py @@ -2,6 +2,7 @@ import time import struct import numpy as np import serial +from ..interface.plot import mean, plotSpectrum, findBounds from .experiment_template import PlotBox, Experiment, exp_logger -- GitLab From 56f40569bbf001459f4075458e6250491330ad91 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 22 Nov 2017 19:10:15 -0500 Subject: [PATCH 088/111] comm: Add ctrl pipe check method. --- dstat_interface/core/dstat/comm.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 1d2c05b..0128652 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -200,7 +200,18 @@ class SerialConnection(GObject.Object): return self.proc_pipe_p.recv() else: return None - + + def get_ctrl(self, block=False): + self.assert_connected() + + if block is True: + return self.ctrl_pipe_p.recv() + else: + if self.ctrl_pipe_p.poll() is True: + return self.ctrl_pipe_p.recv() + else: + return None + def get_data(self, block=False): self.assert_connected() -- GitLab From b7b440e2940635271ea1434a2a8ac0c0803917ab Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 22 Nov 2017 19:13:53 -0500 Subject: [PATCH 089/111] Fix logging bug when binary data gets grabbed. --- dstat_interface/core/dstat/comm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 0128652..d78ea58 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -211,7 +211,7 @@ class SerialConnection(GObject.Object): return self.ctrl_pipe_p.recv() else: return None - + def get_data(self, block=False): self.assert_connected() @@ -268,7 +268,7 @@ class VersionCheck(object): time.sleep(.1) for line in ser: - dstat_logger.info(line) + dstat_logger.info(line.decode('utf-8')) if line.startswith('V'): input = line.lstrip('V') elif line.startswith("#"): -- GitLab From 695256883448279df301613b66236981ac15b831 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 22 Nov 2017 19:20:11 -0500 Subject: [PATCH 090/111] Make experiment classes GObjects --- dstat_interface/core/experiments/experiment_template.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 78c7a74..bd36ce5 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -58,16 +58,20 @@ dstat_logger = logging.getLogger("{}.DSTAT".format(comm.__name__)) exp_logger = logging.getLogger("{}.Experiment".format(__name__)) -class Experiment(object): +class Experiment(GObject.Object): """Store and acquire a potentiostat experiment. Meant to be subclassed to by different experiment types and not used instanced directly. Subclass must instantiate self.plotbox as the PlotBox class to use and define id as a class attribute. """ id = None - + __gsignals__ = { + b'exp_ready': (GObject.SIGNAL_RUN_FIRST, None, ()), + b'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()) + } def __init__(self, parameters): """Adds commands for gain and ADC.""" + super(Experiment, self).__init__() self.parameters = parameters self.databytes = 8 self.datapoint = 0 -- GitLab From 45498fe626768d0a06e4338f7dfab216e1d1a7a0 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 22 Nov 2017 19:25:13 -0500 Subject: [PATCH 091/111] Add experiment fetching method --- dstat_interface/core/interface/exp_int.py | 5 ++++- dstat_interface/core/interface/exp_window.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index 7235400..a4aba0e 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -61,7 +61,10 @@ class ExpInterface(GObject.Object): self.builder.connect_signals(self) self.entry = {} # to be used only for str parameters self._params = None - + + def get_experiment(self, parameters): + return self.__class__.experiment(parameters) + def _fill_params(self): self._params = dict.fromkeys(self.entry.keys()) diff --git a/dstat_interface/core/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py index d50405a..d7fca1e 100755 --- a/dstat_interface/core/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -94,8 +94,8 @@ class Experiments(GObject.Object): "Experiment {} has no defined parameter test.".format( exp.name) ) - return exp.experiment(parameters) - + return exp.get_experiment(parameters) + def hide_exps(self): for key in self.containers: self.containers[key].hide() -- GitLab From 4d76ce080311c1562addd58ada27a3c30693e633 Mon Sep 17 00:00:00 2001 From: Unknown Date: Wed, 22 Nov 2017 19:28:54 -0500 Subject: [PATCH 092/111] Check for ctrl commands from experiment processes. --- dstat_interface/main.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index b4a5b32..2581191 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -632,6 +632,13 @@ class Main(object): function from GTK's queue. """ try: + ctrl_buffer = dstat.state.ser.get_ctrl() + try: + if ctrl_buffer is not None: + self.current_exp.ctrl_loop(ctrl_buffer) + except AttributeError: + pass + proc_buffer = dstat.state.ser.get_proc() if proc_buffer is not None: if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: -- GitLab From d8f6a2021333363fb772177303dd266bb69f2d5d Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 27 Nov 2017 21:19:09 -0500 Subject: [PATCH 093/111] Move loops out of main into experiments. Add progress bar. --- .../experiment_loops.py | 70 +++++++++++++++++-- .../core/experiments/experiment_template.py | 7 ++ .../core/interface/dstatinterface.glade | 21 +++++- dstat_interface/main.py | 58 +++++---------- 4 files changed, 111 insertions(+), 45 deletions(-) rename dstat_interface/core/{utils => experiments}/experiment_loops.py (66%) diff --git a/dstat_interface/core/utils/experiment_loops.py b/dstat_interface/core/experiments/experiment_loops.py similarity index 66% rename from dstat_interface/core/utils/experiment_loops.py rename to dstat_interface/core/experiments/experiment_loops.py index 2099fd5..d64b09f 100644 --- a/dstat_interface/core/utils/experiment_loops.py +++ b/dstat_interface/core/experiments/experiment_loops.py @@ -41,19 +41,25 @@ class BaseLoop(GObject.GObject): b'progress_update': (GObject.SIGNAL_RUN_FIRST, None, (float,)) } - def __init__(self, experiment): + def __init__(self, experiment, callbacks=None): GObject.GObject.__init__(self) self.line = None self.lastdataline = 0 self.current_exp = experiment self.experiment_proc = None + for signal, cb in callbacks.items(): + try: + self.connect(signal, cb) + except TypeError: + logger.warning("Invalid signal %s", signal) + def run(self): - self.experiment_proc = ( + self.experiment_proc = [ GObject.idle_add(self.experiment_running_data), GObject.idle_add(self.experiment_running_proc), GObject.timeout_add(100, self.update_progress) - ) + ] def experiment_running_data(self): """Receive data from experiment process and add to @@ -105,6 +111,13 @@ class BaseLoop(GObject.GObject): function from GTK's queue. """ try: + ctrl_buffer = state.ser.get_ctrl() + try: + if ctrl_buffer is not None: + self.current_exp.ctrl_loop(ctrl_buffer) + except AttributeError: + pass + proc_buffer = state.ser.get_proc() if proc_buffer is not None: if proc_buffer in ["DONE", "SERIAL_ERROR", "ABORT"]: @@ -129,12 +142,59 @@ class BaseLoop(GObject.GObject): def experiment_done(self): logger.info("Experiment done") - self.current_exp.experiment_done() for proc in self.experiment_proc: GObject.source_remove(proc) + self.current_exp.scan_process(self.lastdataline) + self.current_exp.experiment_done() self.emit("experiment_done") def update_progress(self): - progress = self.current_exp.get_progress() + try: + progress = self.current_exp.get_progress() + except AttributeError: + progress = -1 self.emit("progress_update", progress) return True + + +class PlotLoop(BaseLoop): + 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 is None: + return True + for plot in self.current_exp.plots: + if (plot.scan_refresh and self.line > self.lastdataline): + while self.line > self.lastline: + # make sure all of last line is added + plot.updateline(self.current_exp, self.lastdataline) + self.lastdataline += 1 + plot.updateline(self.current_exp, self.line) + plot.redraw() + else: + while self.line > self.lastdataline: + # make sure all of last line is added + plot.updateline(self.current_exp, self.lastdataline) + self.lastdataline += 1 + plot.updateline(self.current_exp, self.line) + + if plot.continuous_refresh is True or force_refresh is True: + plot.redraw() + return True + + def run(self): + super(PlotLoop, self).run() + self.experiment_proc.append( + GObject.timeout_add(200, self.experiment_running_plot) + ) + + def experiment_done(self): + logger.info("Experiment done") + for proc in self.experiment_proc: + GObject.source_remove(proc) + self.current_exp.scan_process(self.lastdataline) + self.current_exp.experiment_done() + self.experiment_running_plot(force_refresh=True) + self.emit("experiment_done") \ No newline at end of file diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index bd36ce5..5aecd55 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -52,6 +52,7 @@ import serial from ..errors import InputError, VarError from ..dstat import state, comm from ..dstat.comm import TransmitError +from . import experiment_loops logger = logging.getLogger(__name__) dstat_logger = logging.getLogger("{}.DSTAT".format(comm.__name__)) @@ -65,10 +66,12 @@ class Experiment(GObject.Object): a class attribute. """ id = None + Loops = experiment_loops.PlotLoop __gsignals__ = { b'exp_ready': (GObject.SIGNAL_RUN_FIRST, None, ()), b'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()) } + def __init__(self, parameters): """Adds commands for gain and ADC.""" super(Experiment, self).__init__() @@ -105,6 +108,10 @@ class Experiment(GObject.Object): self.setup() self.time = [datetime.utcnow()] + + def setup_loops(self, callbacks): + self.loops = self.__class__.Loops(self, callbacks) + self.loops.run() def setup(self): self.data = OrderedDict(current_voltage=[([], [])]) diff --git a/dstat_interface/core/interface/dstatinterface.glade b/dstat_interface/core/interface/dstatinterface.glade index 635e737..889e1d6 100644 --- a/dstat_interface/core/interface/dstatinterface.glade +++ b/dstat_interface/core/interface/dstatinterface.glade @@ -349,6 +349,9 @@ Thanks to Christian Fobel for help with Dropbot Plugin 1 + + + True @@ -357,7 +360,7 @@ Thanks to Christian Fobel for help with Dropbot Plugin False True - 2 + 3 @@ -435,6 +438,22 @@ Thanks to Christian Fobel for help with Dropbot Plugin 2 + + + True + False + True + 10 + 15 + 5 + 2 + + + False + True + 3 + + False diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 2581191..db426bb 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -76,9 +76,15 @@ for log in loggers: log.addHandler(log_handler) -class Main(object): +class Main(GObject.Object): """Main program """ + __gsignals__ = { + b'exp_start': (GObject.SIGNAL_RUN_FIRST, None, ()), + b'exp_stop': (GObject.SIGNAL_RUN_FIRST, None, ()) + } + def __init__(self): + super(Main, self).__init__() self.builder = Gtk.Builder() self.builder.add_from_file( os.path.join(mod_dir, 'core/interface/dstatinterface.glade')) @@ -93,6 +99,7 @@ class Main(object): self.stopbutton = self.builder.get_object('pot_stop') self.startbutton = self.builder.get_object('pot_start') self.startbutton.set_sensitive(False) + self.exp_progressbar = self.builder.get_object('exp_progressbar') self.adc_pot = adc_pot.adc_pot() self.error_context_id = self.statusbar.get_context_id("error") @@ -518,6 +525,9 @@ class Main(object): except AttributeError: pass + callbacks = {'experiment_done' : self.experiment_done, + 'progress_update' : self.progress_update} + self.current_exp = self.exp_window.setup_exp(parameters) plot_ui.replace_notebook_exp( @@ -534,14 +544,7 @@ class Main(object): # Flush data pipe dstat.state.ser.flush_data() - - 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.current_exp.setup_loops(callbacks) return experiment_id except ValueError as i: @@ -579,6 +582,12 @@ class Main(object): exceptions() raise + def progress_update(self, widget, progress): + if 0 <= progress <= 1: + self.exp_progressbar.set_fraction(progress) + else: + self.exp_progressbar.pulse() + def experiment_running_data(self): """Receive data from experiment process and add to current_exp.data['data]. @@ -685,43 +694,14 @@ class Main(object): plot.redraw() return True - def experiment_done(self): + def experiment_done(self, widget=None): """Clean up after data acquisition is complete. Update plot and copy data to raw data tab. Saves data if autosave enabled. """ try: - self.current_exp.scan_process(self.lastdataline) - self.current_exp.experiment_done() - GObject.source_remove(self.experiment_proc[0]) - GObject.source_remove(self.plot_proc) # stop automatic plot update - self.experiment_running_plot(force_refresh=True) - - - # # Shutter stuff - # if (self.current_exp.parameters['shutter_true'] and - # self.current_exp.parameters['sync_true']): - # self.ft_plot.updateline(self.current_exp, 0) - # self.ft_plot.redraw() - # - # line_buffer = [] - # - # for scan in self.current_exp.data['ft']: - # for dimension in scan: - # for i in range(len(dimension)): - # try: - # line_buffer[i] += "%s " % dimension[i] - # except IndexError: - # line_buffer.append("") - # line_buffer[i] += "%s " % dimension[i] - # - # for i in line_buffer: - # self.databuffer.insert_at_cursor("%s\n" % i) - # Run Analysis analysis.do_analysis(self.current_exp) - self.current_exp.experiment_done() - # Write DStat commands self.info_page.set_text(self.current_exp.get_info_text()) -- GitLab From 8624c8f3b17cfbfe2bcc86271e93ce9ffc20ff31 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 28 Nov 2017 16:00:29 -0500 Subject: [PATCH 094/111] Add non-pulse progressbars for most experiments. --- dstat_interface/core/experiments/chronoamp.py | 12 +++++-- dstat_interface/core/experiments/cv.py | 5 ++- dstat_interface/core/experiments/lsv.py | 13 ++++++-- dstat_interface/core/experiments/pot.py | 11 +++++-- dstat_interface/core/experiments/swv.py | 32 +++++++++++++++++-- dstat_interface/main.py | 1 + 6 files changed, 65 insertions(+), 9 deletions(-) diff --git a/dstat_interface/core/experiments/chronoamp.py b/dstat_interface/core/experiments/chronoamp.py index 5998178..a795630 100644 --- a/dstat_interface/core/experiments/chronoamp.py +++ b/dstat_interface/core/experiments/chronoamp.py @@ -29,10 +29,11 @@ class Chronoamp(Experiment): self.databytes = 8 self.data = {'current_time' : [([],[])]} self.columns = ['Time (s)', 'Current (A)'] + self.total_time = sum(self.parameters['time']) self.plot_format = { 'current_time' : { 'labels' : self.columns, - 'xlims' : (0, sum(self.parameters['time'])) + 'xlims' : (0, self.total_time) } } @@ -66,7 +67,13 @@ class Chronoamp(Experiment): for i, item in enumerate(self.data['current_time'][line]): item.append(data[i]) - + + def get_progress(self): + try: + return self.data['current_time'][-1][0][-1]/self.total_time + except IndexError: + return 0 + class PDExp(Chronoamp): """Photodiode/PMT experiment""" id = 'pde' @@ -84,6 +91,7 @@ class PDExp(Chronoamp): 'xlims' : (0, int(self.parameters['time'])) } } + self.total_time = int(self.parameters['time']) if self.parameters['shutter_true']: if self.parameters['sync_true']: diff --git a/dstat_interface/core/experiments/cv.py b/dstat_interface/core/experiments/cv.py index af828ea..80bc3a5 100644 --- a/dstat_interface/core/experiments/cv.py +++ b/dstat_interface/core/experiments/cv.py @@ -60,4 +60,7 @@ class CVExp(Experiment): self.re_voltage_scale* (65536./3000) )) - self.commands[2] += " " \ No newline at end of file + self.commands[2] += " " + + def get_progress(self): + return (len(self.data['current_voltage'])-1) / float(self.parameters['scans']) diff --git a/dstat_interface/core/experiments/lsv.py b/dstat_interface/core/experiments/lsv.py index 8b6f65d..aeed95d 100644 --- a/dstat_interface/core/experiments/lsv.py +++ b/dstat_interface/core/experiments/lsv.py @@ -18,7 +18,10 @@ class LSVExp(Experiment): int(self.parameters['stop'])) ) ) - + + self.stop_mv = int(self.parameters['stop']) + self.max_mv = abs(int(self.parameters['start'])-int(self.parameters['stop'])) + self.commands += "E" self.commands[2] += "L" self.commands[2] += str(self.parameters['clean_s']) @@ -54,4 +57,10 @@ class LSVExp(Experiment): self.re_voltage_scale* (65536./3000) )) - self.commands[2] += " " \ No newline at end of file + self.commands[2] += " " + + def get_progress(self): + try: + return 1 - (abs(self.stop_mv - self.data['current_voltage'][-1][0][-1])/self.max_mv) + except IndexError: + return 0 \ No newline at end of file diff --git a/dstat_interface/core/experiments/pot.py b/dstat_interface/core/experiments/pot.py index cbf8232..f16a372 100644 --- a/dstat_interface/core/experiments/pot.py +++ b/dstat_interface/core/experiments/pot.py @@ -32,7 +32,8 @@ class PotExp(Experiment): 'xlims' : (0, int(self.parameters['time'])) } } - + self.total_time = int(self.parameters['time']) + self.commands += "E" self.commands[2] += "P" self.commands[2] += str(self.parameters['time']) @@ -58,4 +59,10 @@ class PotExp(Experiment): 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 + item.append(data[i]) + + def get_progress(self): + try: + return self.data['voltage_time'][-1][0][-1]/self.total_time + except IndexError: + return 0 \ No newline at end of file diff --git a/dstat_interface/core/experiments/swv.py b/dstat_interface/core/experiments/swv.py index 17202d4..19bd91e 100644 --- a/dstat_interface/core/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -46,6 +46,10 @@ class SWVExp(Experiment): } } + self.stop_mv = int(self.parameters['stop']) + self.max_mv = abs(int(self.parameters['start']) - int(self.parameters['stop'])) + self.scan_points = self.max_mv * 2 / float(self.parameters['step']) + self.commands += "E" self.commands[2] += "S" self.commands[2] += str(self.parameters['clean_s']) @@ -120,6 +124,21 @@ class SWVExp(Experiment): for i, item in enumerate(self.data['swv'][line]): item.append(data[i]) + def get_progress(self): + try: + if int(self.parameters['scans']) != 0: + scans_prog = (len(self.data['swv'])-1) / float(self.parameters['scans']) + scan_prog = (len(self.data['swv'][-1][0])-1) / self.scan_points / float(self.parameters['scans']) + prog = scans_prog + scan_prog + if prog > 1: + prog = 1 + return prog + else: + return 1 - (abs(self.stop_mv - self.data['swv'][-1][0][-1])/self.max_mv) + except IndexError: + return 0 + + class DPVExp(SWVExp): """Diffential Pulse Voltammetry experiment.""" id = 'dpv' @@ -148,7 +167,10 @@ class DPVExp(SWVExp): ) } } - + + self.stop_mv = int(self.parameters['stop']) + self.max_mv = abs(int(self.parameters['start']) - int(self.parameters['stop'])) + self.commands += "E" self.commands[2] += "D" self.commands[2] += str(self.parameters['clean_s']) @@ -194,4 +216,10 @@ class DPVExp(SWVExp): self.commands[2] += str(self.parameters['period']) self.commands[2] += " " self.commands[2] += str(self.parameters['width']) - self.commands[2] += " " \ No newline at end of file + self.commands[2] += " " + + def get_progress(self): + try: + return 1 - (abs(self.stop_mv - self.data['swv'][-1][0][-1])/self.max_mv) + except IndexError: + return 0 \ No newline at end of file diff --git a/dstat_interface/main.py b/dstat_interface/main.py index db426bb..09e3517 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -749,6 +749,7 @@ class Main(GObject.Object): self.metadata = None # Reset metadata self.spinner.stop() + self.exp_progressbar.set_fraction(0) self.stopbutton.set_sensitive(False) self.start_ocp() -- GitLab From bbaa80d5cebf5e75a2162ecfb7f7272e18a65acf Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 28 Nov 2017 16:00:47 -0500 Subject: [PATCH 095/111] Handle no DStat plugged in gracefully. --- dstat_interface/main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 09e3517..837576e 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -245,6 +245,10 @@ class Main(GObject.Object): def on_serial_connect_clicked(self, widget): """Connect and retrieve DStat version.""" + selection = self.serial_combobox.get_active_iter() + if selection is None: + return + if widget is self.serial_pmt_connect: self.pmt_mode = True self.adc_pot.ui['short_true'].set_active(True) @@ -254,9 +258,7 @@ class Main(GObject.Object): self.serial_connect.set_sensitive(False) self.serial_pmt_connect.set_sensitive(False) dstat.comm.version_check( - self.serial_liststore.get_value( - self.serial_combobox.get_active_iter(), 0 - ) + self.serial_liststore.get_value(selection, 0) ) dstat.state.board_instance = boards.find_board( -- GitLab From 2f21e781a6dddc09bc12e3d68a0790df021cacef Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 28 Nov 2017 16:01:05 -0500 Subject: [PATCH 096/111] PEP8 --- dstat_interface/core/experiments/experiment_template.py | 8 ++------ dstat_interface/core/experiments/swv.py | 1 + dstat_interface/core/interface/exp_window.py | 8 +++++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 5aecd55..325903b 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -19,13 +19,11 @@ # along with this program. If not, see . import logging import struct -from datetime import datetime +import time from collections import OrderedDict from copy import deepcopy +from datetime import datetime from math import ceil -import time - -from pkg_resources import parse_version try: import gi @@ -38,7 +36,6 @@ except ImportError: from matplotlib.figure import Figure import matplotlib.gridspec as gridspec -import matplotlib.pyplot as plt from matplotlib.backends.backend_gtk3agg \ import FigureCanvasGTK3Agg as FigureCanvas @@ -49,7 +46,6 @@ sns.set(context='paper', style='darkgrid') import serial -from ..errors import InputError, VarError from ..dstat import state, comm from ..dstat.comm import TransmitError from . import experiment_loops diff --git a/dstat_interface/core/experiments/swv.py b/dstat_interface/core/experiments/swv.py index 19bd91e..5743ec3 100644 --- a/dstat_interface/core/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -20,6 +20,7 @@ class SWVBox(PlotBox): class SWVExp(Experiment): """Square Wave Voltammetry experiment""" id = 'swv' + def setup(self): self.plots.append(SWVBox(['swv'])) diff --git a/dstat_interface/core/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py index d7fca1e..61906b5 100755 --- a/dstat_interface/core/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -33,11 +33,13 @@ from . import exp_int logger = logging.getLogger(__name__) + 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 @@ -52,7 +54,8 @@ class Experiments(GObject.Object): } self.classes = OrderedDict(sorted(classes.items())) - #fill exp_section + + # fill exp_section exp_section = self.builder.get_object('exp_section_box') self.containers = {} @@ -86,6 +89,9 @@ class Experiments(GObject.Object): self.set_exp(self.expcombobox.get_active_id()) def setup_exp(self, parameters): + """Takes parameters. + Returns experiment instance. + """ exp = self.classes[self.expcombobox.get_active_id()] try: exp.param_test(parameters) -- GitLab From 3d61e6f9366ef71003b9cabc44b93a59d45644cd Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 11 Dec 2017 03:03:56 -0500 Subject: [PATCH 097/111] Refactor experiment plotting --- dstat_interface/core/experiments/chronoamp.py | 30 ++++--- dstat_interface/core/experiments/cv.py | 8 +- .../core/experiments/experiment_loops.py | 3 +- .../core/experiments/experiment_template.py | 84 +++++++++++-------- dstat_interface/core/experiments/lsv.py | 13 +-- dstat_interface/core/experiments/pot.py | 28 +++++-- dstat_interface/core/experiments/swv.py | 62 ++++++++------ dstat_interface/main.py | 3 - 8 files changed, 136 insertions(+), 95 deletions(-) diff --git a/dstat_interface/core/experiments/chronoamp.py b/dstat_interface/core/experiments/chronoamp.py index a795630..94c7484 100644 --- a/dstat_interface/core/experiments/chronoamp.py +++ b/dstat_interface/core/experiments/chronoamp.py @@ -6,45 +6,55 @@ from ..interface.plot import mean, plotSpectrum, findBounds from .experiment_template import PlotBox, Experiment, exp_logger + class ChronoampBox(PlotBox): + def setup(self): + self.plot_format = { + 'current_time': {'xlabel': "Time (s)", + 'ylabel': "Current (A)" + } + } + def format_plots(self): """ Creates and formats subplots needed. Overrides superclass. """ - self.subplots = {'current_time' : self.figure.add_subplot(111)} + 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([],[]) + subplot.set_xlabel(self.plot_format[key]['xlabel']) + subplot.set_ylabel(self.plot_format[key]['ylabel']) + class Chronoamp(Experiment): id = 'cae' """Chronoamperometry experiment""" def setup(self): - self.plots.append(ChronoampBox('current_time')) - self.datatype = "linearData" self.datalength = 2 self.databytes = 8 self.data = {'current_time' : [([],[])]} self.columns = ['Time (s)', 'Current (A)'] self.total_time = sum(self.parameters['time']) - self.plot_format = { - 'current_time' : { - 'labels' : self.columns, - 'xlims' : (0, self.total_time) - } - } + self.plotlims = {'current_time': {'xlims': (0, self.total_time)} + } self.commands.append( - ("ER"+ str(len(self.parameters['potential'])) + " 0 ", []) + ("ER" + str(len(self.parameters['potential'])) + " 0 ", []) ) for i in self.parameters['potential']: self.commands[-1][1].append(str(int(i*(65536./3000)+32768))) for i in self.parameters['time']: self.commands[-1][1].append(str(i)) + + plot = ChronoampBox(['current_time']) + plot.setlims('current_time', **self.plotlims['current_time']) + + self.plots.append(plot) def data_handler(self, data_input): """Overrides Experiment method to not convert x axis to mV.""" diff --git a/dstat_interface/core/experiments/cv.py b/dstat_interface/core/experiments/cv.py index 80bc3a5..c08e610 100644 --- a/dstat_interface/core/experiments/cv.py +++ b/dstat_interface/core/experiments/cv.py @@ -3,19 +3,21 @@ import struct from .experiment_template import PlotBox, Experiment + class CVExp(Experiment): id = 'cve' """Cyclic Voltammetry experiment""" def setup(self): + self.plotlims['current_voltage']['xlims'] = tuple( + sorted((int(self.parameters['v1']), int(self.parameters['v2']))) + ) super(CVExp, self).setup() + self.datatype = "CVData" self.xlabel = "Voltage (mV)" self.ylabel = "Current (A)" self.datalength = 2 * self.parameters['scans'] # x and y for each scan self.databytes = 6 # uint16 + int32 - self.plot_format['current_voltage']['xlims'] = tuple( - sorted((int(self.parameters['v1']), int(self.parameters['v2']))) - ) self.commands += "E" self.commands[2] += "C" diff --git a/dstat_interface/core/experiments/experiment_loops.py b/dstat_interface/core/experiments/experiment_loops.py index d64b09f..a350de8 100644 --- a/dstat_interface/core/experiments/experiment_loops.py +++ b/dstat_interface/core/experiments/experiment_loops.py @@ -75,7 +75,7 @@ class BaseLoop(GObject.GObject): incoming = state.ser.get_data() while incoming is not None: try: - self.line, data = incoming + self.line = incoming[0] if self.line > self.lastdataline: newline = True try: @@ -89,6 +89,7 @@ class BaseLoop(GObject.GObject): self.current_exp.store_data(incoming, newline) except TypeError: pass + incoming = state.ser.get_data() return True diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 325903b..05a17a1 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -65,12 +65,13 @@ class Experiment(GObject.Object): Loops = experiment_loops.PlotLoop __gsignals__ = { b'exp_ready': (GObject.SIGNAL_RUN_FIRST, None, ()), - b'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()) + b'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, parameters): """Adds commands for gain and ADC.""" super(Experiment, self).__init__() + self.current_command = None self.parameters = parameters self.databytes = 8 self.datapoint = 0 @@ -93,7 +94,7 @@ class Experiment(GObject.Object): self.commands = ["EA", "EG"] - if self.parameters['buffer_true']: + if self.parameters['buffer_true']: self.commands[0] += "2" else: self.commands[0] += "0" @@ -101,31 +102,32 @@ class Experiment(GObject.Object): p=self.parameters) self.commands[1] += "{p[gain]} {p[short_true]:d} ".format( p=self.parameters) - + + self.plotlims = {'current_voltage' : {'xlims' : (0, 1)} + } + self.setup() self.time = [datetime.utcnow()] def setup_loops(self, callbacks): self.loops = self.__class__.Loops(self, callbacks) self.loops.run() - + def setup(self): self.data = OrderedDict(current_voltage=[([], [])]) - self.columns = ['Voltage (mV)', 'Current (A)'] - self.plot_format = { - 'current_voltage' : {'labels' : self.columns, - 'xlims' : (0, 1) - } - } + # list of scans, tuple of dimensions, list of data self.line_data = ([], []) - - self.plots.append(PlotBox(['current_voltage'])) + + plot = PlotBox(['current_voltage']) + plot.setlims('current_voltage', **self.plotlims['current_voltage']) + + self.plots.append(plot) def write_command(self, cmd, params=None, retry=5): """Write command to serial with optional number of retries.""" - def get_reply(retries = 3): + def get_reply(retries=3): while True: reply = self.serial.readline().rstrip() if reply.startswith('#'): @@ -232,6 +234,7 @@ class Experiment(GObject.Object): try: for i in self.commands: + self.current_command = i status = "DONE" if isinstance(i, (str, unicode)): logger.info("Command: %s", i) @@ -315,8 +318,7 @@ class Experiment(GObject.Object): except serial.SerialException: return False - - + def data_handler(self, data_input): """Takes data_input as tuple -- (scan, data). Returns: @@ -448,23 +450,28 @@ class Experiment(GObject.Object): ) return buf - + + class PlotBox(object): """Contains data plot and associated methods.""" - def __init__(self, plots): + def __init__(self, plots=None): """Initializes plots. self.box should be reparented.""" self.name = "Main" self.continuous_refresh = True self.scan_refresh = False - - self.plotnames = plots + + if plots is not None: + self.plotnames = plots + else: + self.plotnames = [] self.subplots = {} self.figure = Figure() # self.figure.subplots_adjust(left=0.07, bottom=0.07, # right=0.96, top=0.96) - - self.format_plots() # Should be overriden by subclass + + self.setup() + self.format_plots() # Should be overriden by subclass self.figure.set_tight_layout(True) @@ -473,7 +480,14 @@ class PlotBox(object): self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) self.box.pack_start(self.canvas, expand=True, fill=True, padding=0) - + + def setup(self): + self.plot_format = { + 'current_voltage': {'xlabel': "Voltage (mV)", + 'ylabel': "Current (A)" + } + } + def format_plots(self): """ Creates and formats subplots needed. Should be overriden by subclass @@ -487,10 +501,12 @@ class PlotBox(object): for n, i in enumerate(self.plotnames): self.subplots[i] = self.figure.add_subplot(gs[n]) - for subplot in self.subplots.values(): + for key, subplot in self.subplots.items(): subplot.ticklabel_format(style='sci', scilimits=(0, 3), useOffset=False, axis='y') - subplot.plot([],[]) + subplot.plot([], []) + subplot.set_xlabel(self.plot_format[key]['xlabel']) + subplot.set_ylabel(self.plot_format[key]['ylabel']) def clearall(self): """Remove all lines on plot. """ @@ -534,20 +550,14 @@ class PlotBox(object): else: break # logger.warning("Tried to set line %s that doesn't exist.", line_number) - def changetype(self, Experiment): - """Change plot type. Set axis labels and x bounds to those stored - in the Experiment instance. Stores class instance in Experiment. - """ - - for name, subplot in self.subplots.items(): - subplot.set_xlabel(Experiment.plot_format[name]['labels'][0]) - subplot.set_ylabel(Experiment.plot_format[name]['labels'][1]) - subplot.set_xlim(Experiment.plot_format[name]['xlims']) - for name, subplot in Experiment.plot_format.items(): - self.subplots[name].set_xlabel(subplot['labels'][0]) - self.subplots[name].set_ylabel(subplot['labels'][1]) - self.subplots[name].set_xlim(subplot['xlims']) + def setlims(self, plot, xlims=None, ylims=None): + """Sets x and y limits. + """ + if xlims is not None: + self.subplots[plot].set_xlim(xlims) + if ylims is not None: + self.subplots[plot].set_ylim(ylims) self.figure.canvas.draw() diff --git a/dstat_interface/core/experiments/lsv.py b/dstat_interface/core/experiments/lsv.py index aeed95d..ce2a2e1 100644 --- a/dstat_interface/core/experiments/lsv.py +++ b/dstat_interface/core/experiments/lsv.py @@ -7,18 +7,19 @@ class LSVExp(Experiment): """Linear Scan Voltammetry experiment""" id = 'lsv' def setup(self): - super(LSVExp, self).setup() - - self.datatype = "linearData" - self.datalength = 2 - self.databytes = 6 # uint16 + int32 - self.plot_format['current_voltage']['xlims'] = tuple( + self.plotlims['current_voltage']['xlims'] = tuple( sorted( (int(self.parameters['start']), int(self.parameters['stop'])) ) ) + super(LSVExp, self).setup() + + self.datatype = "linearData" + self.datalength = 2 + self.databytes = 6 # uint16 + int32 + self.stop_mv = int(self.parameters['stop']) self.max_mv = abs(int(self.parameters['start'])-int(self.parameters['stop'])) diff --git a/dstat_interface/core/experiments/pot.py b/dstat_interface/core/experiments/pot.py index f16a372..fa9b4be 100644 --- a/dstat_interface/core/experiments/pot.py +++ b/dstat_interface/core/experiments/pot.py @@ -3,18 +3,29 @@ import struct from .experiment_template import PlotBox, Experiment + class PotBox(PlotBox): + def setup(self): + self.plot_format = { + 'voltage_time': {'xlabel': "Time (s)", + 'ylabel': "Voltage (V)" + } + } + def format_plots(self): """ Creates and formats subplots needed. Overrides superclass. """ - self.subplots = {'voltage_time' : self.figure.add_subplot(111)} + self.subplots = {'voltage_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([],[]) - + subplot.set_xlabel(self.plot_format[key]['xlabel']) + subplot.set_ylabel(self.plot_format[key]['ylabel']) + + class PotExp(Experiment): id = 'pot' """Potentiometry experiment""" @@ -26,18 +37,17 @@ class PotExp(Experiment): self.databytes = 8 self.data = {'voltage_time' : [([],[])]} self.columns = ['Time (s)', 'Voltage (V)'] - self.plot_format = { - 'voltage_time' : { - 'labels' : self.columns, - 'xlims' : (0, int(self.parameters['time'])) - } - } + self.plotlims = { + 'voltage_time': { + 'xlims': (0, int(self.parameters['time'])) + } + } self.total_time = int(self.parameters['time']) self.commands += "E" self.commands[2] += "P" self.commands[2] += str(self.parameters['time']) - self.commands[2] += " 1 " #potentiometry mode + self.commands[2] += " 1 " # potentiometry mode def data_handler(self, data_input): """Overrides Experiment method to not convert x axis to mV.""" diff --git a/dstat_interface/core/experiments/swv.py b/dstat_interface/core/experiments/swv.py index 5743ec3..d60c447 100644 --- a/dstat_interface/core/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -4,26 +4,33 @@ from copy import deepcopy from .experiment_template import PlotBox, Experiment + class SWVBox(PlotBox): + def setup(self): + self.plot_format = { + 'swv': {'xlabel': "Voltage (mV)", + 'ylabel': "Current (A)" + } + } def format_plots(self): """ Creates and formats subplots needed. Overrides superclass. """ - - self.subplots = {'swv' : self.figure.add_subplot(111)} + self.subplots = {'swv': 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([],[]) + subplot.set_xlabel(self.plot_format[key]['xlabel']) + subplot.set_ylabel(self.plot_format[key]['ylabel']) + class SWVExp(Experiment): """Square Wave Voltammetry experiment""" id = 'swv' def setup(self): - self.plots.append(SWVBox(['swv'])) - self.datatype = "SWVData" self.xlabel = "Voltage (mV)" self.ylabel = "Current (A)" @@ -35,17 +42,19 @@ class SWVExp(Experiment): 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)' - ), - 'xlims' : tuple(sorted( - (int(self.parameters['start']), - int(self.parameters['stop'])) - ) - ) - } + self.plotlims = { + 'swv': { + 'xlims': tuple(sorted( + (int(self.parameters['start']), + int(self.parameters['stop'])) + ) + ) } + } + + plot = SWVBox() + plot.setlims('swv', **self.plotlims['swv']) + self.plots.append(plot) self.stop_mv = int(self.parameters['stop']) self.max_mv = abs(int(self.parameters['start']) - int(self.parameters['stop'])) @@ -144,8 +153,6 @@ class DPVExp(SWVExp): """Diffential Pulse Voltammetry experiment.""" id = 'dpv' def setup(self): - self.plots.append(SWVBox(['swv'])) - self.datatype = "SWVData" self.xlabel = "Voltage (mV)" self.ylabel = "Current (A)" @@ -157,17 +164,20 @@ class DPVExp(SWVExp): 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)' - ), - 'xlims' : tuple(sorted( - (int(self.parameters['start']), - int(self.parameters['stop'])) - ) - ) - } + + self.plotlims = { + 'swv': { + 'xlims': tuple(sorted( + (int(self.parameters['start']), + int(self.parameters['stop'])) + ) + ) } + } + + plot = SWVBox() + plot.setlims('swv', **self.plotlims['swv']) + self.plots.append(plot) self.stop_mv = int(self.parameters['stop']) self.max_mv = abs(int(self.parameters['start']) - int(self.parameters['stop'])) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 837576e..32caa5d 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -535,9 +535,6 @@ class Main(GObject.Object): plot_ui.replace_notebook_exp( self.plot_notebook, self.current_exp, self.window ) - - for plot in self.current_exp.plots: - plot.changetype(self.current_exp) self.data_view.clear_exps() self.info_page.clear() -- GitLab From 87003c573f7b91fe65d567e77883ff9db6e189c1 Mon Sep 17 00:00:00 2001 From: Unknown Date: Mon, 11 Dec 2017 03:16:00 -0500 Subject: [PATCH 098/111] Fix potentiometry plot limits. --- dstat_interface/core/experiments/pot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dstat_interface/core/experiments/pot.py b/dstat_interface/core/experiments/pot.py index fa9b4be..0f5971c 100644 --- a/dstat_interface/core/experiments/pot.py +++ b/dstat_interface/core/experiments/pot.py @@ -42,6 +42,8 @@ class PotExp(Experiment): 'xlims': (0, int(self.parameters['time'])) } } + self.plots[-1].setlims('voltage_time', **self.plotlims['voltage_time']) + self.total_time = int(self.parameters['time']) self.commands += "E" -- GitLab From e6653d00cd412a724cb3db0d141ee1ec23a0e49f Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Thu, 29 Mar 2018 14:38:36 -0400 Subject: [PATCH 099/111] Update changelog --- CHANGELOG | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 066648f..49d3422 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +Version 1.4.6 + -Fixed data output for SWV/DPV forward/reverse current + -Working progress bars + -Refactored a lot of plotting code + -Calibration should work properly now + Version 1.4.5 -Made board definitions modular -Fix several bugs with experiment parameters -- GitLab From 9cc6cc2d9c7931c7ce593d61a94f6e646c6a6708 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Mon, 11 Jun 2018 16:26:41 -0400 Subject: [PATCH 100/111] Update documentation to add environment file --- README.markdown | 6 ++- conda-env.yml | 137 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 conda-env.yml diff --git a/README.markdown b/README.markdown index ed2fdb5..e4c2f22 100644 --- a/README.markdown +++ b/README.markdown @@ -31,8 +31,10 @@ dstat-interface has moved to gtk+3 and we now recommend Anaconda/Miniconda for i 1. [Install Miniconda](https://repo.continuum.io/miniconda/Miniconda2-latest-MacOSX-x86_64.sh) It doesn't matter if you pick Python 2.7 or 3.5—this just sets Miniconda's default Python. (Skip if Miniconda or Anaconda are already installed) - 2. Create a new environment for dstat. In a terminal type: -````conda create -n dstat -c mdryden python=2.7 dstat-interface dstat-interface-deps```` + 2. Download the [conda env file](conda-env.yml). + + 3. In the terminal, create the dstat environment (replacing with the actual path to the file on your computer): +````conda env create -f ```` 3. Then to run dstat-interface: ````source activate dstat diff --git a/conda-env.yml b/conda-env.yml new file mode 100644 index 0000000..8bce2bc --- /dev/null +++ b/conda-env.yml @@ -0,0 +1,137 @@ +name: dstat +channels: + - conda-forge + - mdryden + - defaults +dependencies: + - ca-certificates=2018.4.16=0 + - certifi=2018.4.16=py27_0 + - nb_conda_kernels=2.1.0=py27_0 + - openssl=1.0.2o=0 + - appnope=0.1.0=py27_0 + - backports=1.0=py27_0 + - backports_abc=0.5=py27_0 + - bleach=1.5.0=py27_0 + - configparser=3.5.0=py27_0 + - curl=7.54.1=0 + - cycler=0.10.0=py27_0 + - decorator=4.1.2=py27_0 + - entrypoints=0.2.3=py27_0 + - enum34=1.1.6=py27_0 + - expat=2.1.0=0 + - freetype=2.5.5=2 + - funcsigs=1.0.2=py27hb9f6266_0 + - functools32=3.2.3.2=py27_0 + - get_terminal_size=1.0.0=py27_0 + - gettext=0.19.8=1 + - git=2.11.1=0 + - html5lib=0.9999999=py27_0 + - icu=54.1=0 + - intel-openmp=2018.0.0=h8158457_8 + - ipykernel=4.6.1=py27_0 + - ipython=5.3.0=py27_0 + - ipython-notebook=4.0.4=py27_0 + - ipython_genutils=0.2.0=py27_0 + - jbig=2.1=0 + - jinja2=2.9.6=py27_0 + - jpeg=9b=0 + - jsonschema=2.6.0=py27_0 + - jupyter_client=5.1.0=py27_0 + - jupyter_core=4.3.0=py27_0 + - krb5=1.13.2=0 + - libcxx=4.0.1=h579ed51_0 + - libcxxabi=4.0.1=hebd6815_0 + - libffi=3.2.1=1 + - libgfortran=3.0.1=h93005f0_2 + - libiconv=1.14=0 + - libpng=1.6.30=1 + - libssh2=1.8.0=0 + - libtiff=4.0.6=3 + - llvmlite=0.21.0=py27hac8ee23_0 + - markupsafe=1.0=py27_0 + - matplotlib=2.0.2=np113py27_0 + - mistune=0.7.4=py27_0 + - mkl=2018.0.1=hfbd8650_4 + - nbconvert=5.2.1=py27_0 + - nbformat=4.4.0=py27_0 + - notebook=5.0.0=py27_0 + - numba=0.36.2=np113py27h7c931aa_0 + - numpy=1.13.3=py27h62f9060_0 + - pandas=0.20.3=py27_0 + - pandocfilters=1.4.2=py27_0 + - path.py=10.3.1=py27_0 + - pathlib2=2.3.0=py27_0 + - patsy=0.4.1=py27_0 + - pcre=8.39=1 + - pexpect=4.2.1=py27_0 + - pickleshare=0.7.4=py27_0 + - pip=9.0.1=py27_1 + - prompt_toolkit=1.0.15=py27_0 + - ptyprocess=0.5.2=py27_0 + - pygments=2.2.0=py27_0 + - pyparsing=2.2.0=py27_0 + - pyqt=5.6.0=py27_2 + - python=2.7.13=0 + - python-dateutil=2.6.1=py27_0 + - pytz=2017.2=py27_0 + - pyyaml=3.12=py27_0 + - pyzmq=16.0.2=py27_0 + - qt=5.6.2=2 + - readline=6.2=2 + - scandir=1.5=py27_0 + - scipy=1.0.0=py27h793f721_0 + - seaborn=0.8=py27_0 + - setuptools=36.4.0=py27_0 + - simplegeneric=0.8.1=py27_1 + - singledispatch=3.4.0.3=py27_0 + - sip=4.18=py27_0 + - six=1.10.0=py27_0 + - sqlite=3.13.0=0 + - ssl_match_hostname=3.5.0.1=py27_0 + - statsmodels=0.8.0=np113py27_0 + - subprocess32=3.2.7=py27_0 + - terminado=0.6=py27_0 + - testpath=0.3.1=py27_0 + - tk=8.5.18=0 + - tornado=4.5.2=py27_0 + - traitlets=4.3.2=py27_0 + - wcwidth=0.1.7=py27_0 + - wheel=0.29.0=py27_0 + - xz=5.2.3=0 + - yaml=0.1.6=0 + - zlib=1.2.11=0 + - adwaita-icon-theme=3.24.0=1 + - arrow=0.10.0=py27_0 + - at-spi2-atk=2.24.1=2 + - at-spi2-core=2.24.1=2 + - atk=2.24.0=3 + - cairo-gobject=1.14.8=8 + - dbus-client=1.10.18=0 + - dfu-programmer=0.7.2=2 + - dstat-interface=1.4.6=py27_0 + - dstat-interface-deps=1.0=0 + - gdk-pixbuf=2.36.6=2 + - glib=2.52.2=5 + - gobject-introspection=1.52.1=2 + - gtk3=3.22.15=4 + - harfbuzz=1.4.6=3 + - libepoxy=1.4.2=5 + - libusb=1.0.21=0 + - pango=1.40.6=2 + - pixman=0.34.0=1 + - py2cairo=1.10.0=py27_0 + - pygobject3=3.24.2=py27_3 + - pyserial=3.3=py27_0 + - zmq-plugin=0.2.post14=py27_0 + - pip: + - backports.shutil-get-terminal-size==1.0.0 + - backports.shutil-which==3.5.1 + - backports.ssl-match-hostname==3.5.0.1 + - chardet==3.0.4 + - colorama==0.3.9 + - idna==2.6 + - paver==1.2.4 + - pygobject==3.24.1 + - requests==2.18.4 + - urllib3==1.22 + - vmprof==0.4.10 -- GitLab From 86048afb85deb66fbaf689d50474de94e5589c7e Mon Sep 17 00:00:00 2001 From: Michael DM Dryden Date: Thu, 28 Jan 2021 15:15:50 -0500 Subject: [PATCH 101/111] Update README.markdown --- README.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.markdown b/README.markdown index e4c2f22..573ea72 100644 --- a/README.markdown +++ b/README.markdown @@ -1,6 +1,8 @@ ##### _DStat is described in detail in [Dryden MDM, Wheeler AR (2015) DStat: A Versatile, Open-Source Potentiostat for Electroanalysis and Integration. PLoS ONE 10(10): e0140349. doi: 10.1371/journal.pone.0140349](http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0140349) If you use this information in published work, please cite accordingly._ --- +## Python 2.7 is now discontinued and gtk has always been a pain for cross-platform use, so I am in the process of writing a new interface in Python 3 and Qt that I hope will be working before too long. + This is the documentation for the DStat interface software. 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. -- GitLab From a582788d8d163028e0a92bf0fc97160e78604146 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Fri, 15 Nov 2019 14:00:49 -0500 Subject: [PATCH 102/111] Ignore _pycache_ --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d2c8521..a9d44d8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ last_params last_params.yml \.idea/ + +dstat_interface/core/experiments/__pycache__/ -- GitLab From 002437e22d17a4f37ad3acb59d20ecabbd7764ef Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Fri, 15 Nov 2019 14:01:13 -0500 Subject: [PATCH 103/111] Code cleanup --- dstat_interface/core/dstat/comm.py | 5 +++-- .../core/experiments/experiment_template.py | 11 +++++------ dstat_interface/core/interface/save.py | 5 ++++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index d78ea58..54628b8 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -528,8 +528,9 @@ def write_settings(): logger.debug("write_settings: %s", state.ser.get_proc(block=True)) return - -class LightSensor: + + +class LightSensor(object): def __init__(self): pass diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 05a17a1..45c14fa 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -395,7 +395,7 @@ class Experiment(GObject.Object): ['Scan'] + list(self.plot_format[name]['labels']), [n] + list(line)) ) - ), ignore_index = True + ), ignore_index=True ) except (AttributeError, KeyError): try: @@ -531,10 +531,9 @@ class PlotBox(object): def updateline(self, Experiment, line_number): """Update a line specified with new data. - - Arguments: - Experiment -- Experiment instance - line_number -- line number to update + + :param Experiment: Experiment instance + :param line_number: line number to update """ for subplot in Experiment.data: while True: @@ -565,7 +564,7 @@ class PlotBox(object): """Autoscale and refresh the plot.""" for name, plot in self.subplots.items(): plot.relim() - plot.autoscale(True, axis = 'y') + plot.autoscale(True, axis='y') self.figure.canvas.draw() return True \ No newline at end of file diff --git a/dstat_interface/core/interface/save.py b/dstat_interface/core/interface/save.py index 44a7884..f9a6d3d 100755 --- a/dstat_interface/core/interface/save.py +++ b/dstat_interface/core/interface/save.py @@ -164,6 +164,7 @@ def man_param_load(window): elif response == Gtk.ResponseType.CANCEL: fcd.destroy() + def autoSave(exp, path, name): if name == "": name = "file" @@ -173,6 +174,7 @@ def autoSave(exp, path, name): save_text(exp, path) + def autoPlot(exp, path, name): if name == "": name = "file" @@ -202,7 +204,8 @@ def save_text(exp, path): 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. If no file extension or unknown, uses pdf. -- GitLab From d47f73df324c74d3917d5d2039715b1420ccce54 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Fri, 22 Nov 2019 14:45:30 -0500 Subject: [PATCH 104/111] py3: Convert custom gsignals keys to str --- dstat_interface/core/experiments/experiment_loops.py | 4 ++-- dstat_interface/core/experiments/experiment_template.py | 4 ++-- dstat_interface/core/interface/exp_int.py | 4 ++-- dstat_interface/main.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dstat_interface/core/experiments/experiment_loops.py b/dstat_interface/core/experiments/experiment_loops.py index a350de8..67fce8e 100644 --- a/dstat_interface/core/experiments/experiment_loops.py +++ b/dstat_interface/core/experiments/experiment_loops.py @@ -37,8 +37,8 @@ logger = logging.getLogger(__name__) class BaseLoop(GObject.GObject): __gsignals__ = { - b'experiment_done': (GObject.SIGNAL_RUN_FIRST, None, tuple()), - b'progress_update': (GObject.SIGNAL_RUN_FIRST, None, (float,)) + 'experiment_done': (GObject.SIGNAL_RUN_FIRST, None, tuple()), + 'progress_update': (GObject.SIGNAL_RUN_FIRST, None, (float,)) } def __init__(self, experiment, callbacks=None): diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 45c14fa..8c3476c 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -64,8 +64,8 @@ class Experiment(GObject.Object): id = None Loops = experiment_loops.PlotLoop __gsignals__ = { - b'exp_ready': (GObject.SIGNAL_RUN_FIRST, None, ()), - b'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'exp_ready': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()), } def __init__(self, parameters): diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index a4aba0e..36f157f 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -47,8 +47,8 @@ class ExpInterface(GObject.Object): 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, ()) + 'run_utility': (GObject.SIGNAL_RUN_FIRST, None, ()), + 'done_utility': (GObject.SIGNAL_RUN_FIRST, None, ()) } id = None diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 32caa5d..9f622fd 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -79,8 +79,8 @@ for log in loggers: class Main(GObject.Object): """Main program """ __gsignals__ = { - b'exp_start': (GObject.SIGNAL_RUN_FIRST, None, ()), - b'exp_stop': (GObject.SIGNAL_RUN_FIRST, None, ()) + 'exp_start': (GObject.SignalFlags.RUN_FIRST, None, ()), + 'exp_stop': (GObject.SignalFlags.RUN_FIRST, None, ()) } def __init__(self): -- GitLab From c219b47c75fc3fda309192fceeab064b50e05e64 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Fri, 22 Nov 2019 14:59:38 -0500 Subject: [PATCH 105/111] py3: Fix print statements and sys imports --- dstat_interface/core/dstat/comm.py | 4 +++- dstat_interface/core/experiments/experiment_template.py | 5 ++++- dstat_interface/core/interface/adc_pot.py | 3 ++- dstat_interface/core/interface/exp_int.py | 1 + dstat_interface/core/interface/exp_window.py | 3 ++- dstat_interface/core/interface/plot.py | 3 ++- dstat_interface/core/interface/plot_ui.py | 3 ++- dstat_interface/core/interface/save.py | 1 + dstat_interface/core/utils/version.py | 3 ++- 9 files changed, 19 insertions(+), 7 deletions(-) diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 54628b8..017c0fc 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.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 time import struct @@ -29,7 +30,8 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: - print "ERR: GTK not available" + import sys + print("ERR: GTK not available") sys.exit(1) import serial from serial.tools import list_ports diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index 8c3476c..a82114a 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -17,6 +17,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 logging import struct import time @@ -30,7 +32,8 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: - print "ERR: GTK not available" + import sys + print("ERR: GTK not available") sys.exit(1) diff --git a/dstat_interface/core/interface/adc_pot.py b/dstat_interface/core/interface/adc_pot.py index 77efe5e..770ee7f 100755 --- a/dstat_interface/core/interface/adc_pot.py +++ b/dstat_interface/core/interface/adc_pot.py @@ -25,7 +25,8 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk except ImportError: - print "ERR: GTK not available" + import sys + print("ERR: GTK not available") sys.exit(1) from ..errors import InputError, VarError diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index 36f157f..e4dc40d 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -27,6 +27,7 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: + import sys print("ERR: GTK not available") sys.exit(1) diff --git a/dstat_interface/core/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py index 61906b5..8df980c 100755 --- a/dstat_interface/core/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -26,7 +26,8 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: - print "ERR: GTK not available" + import sys + print("ERR: GTK not available") sys.exit(1) from . import exp_int diff --git a/dstat_interface/core/interface/plot.py b/dstat_interface/core/interface/plot.py index 7e87c41..cc0d89b 100755 --- a/dstat_interface/core/interface/plot.py +++ b/dstat_interface/core/interface/plot.py @@ -25,7 +25,8 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk except ImportError: - print "ERR: GTK not available" + import sys + print("ERR: GTK not available") sys.exit(1) from matplotlib.figure import Figure diff --git a/dstat_interface/core/interface/plot_ui.py b/dstat_interface/core/interface/plot_ui.py index b0013e8..5976273 100644 --- a/dstat_interface/core/interface/plot_ui.py +++ b/dstat_interface/core/interface/plot_ui.py @@ -6,7 +6,8 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk except ImportError: - print "ERR: GTK not available" + import sys + print("ERR: GTK not available") sys.exit(1) from matplotlib.backends.backend_gtk3 \ diff --git a/dstat_interface/core/interface/save.py b/dstat_interface/core/interface/save.py index f9a6d3d..92e6d16 100755 --- a/dstat_interface/core/interface/save.py +++ b/dstat_interface/core/interface/save.py @@ -31,6 +31,7 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk except ImportError: + import sys print("ERR: GTK not available") sys.exit(1) import numpy as np diff --git a/dstat_interface/core/utils/version.py b/dstat_interface/core/utils/version.py index ae2f3d2..bc9c9ed 100644 --- a/dstat_interface/core/utils/version.py +++ b/dstat_interface/core/utils/version.py @@ -35,6 +35,7 @@ With that setup, a new release can be labelled by simply invoking: git tag -s v1.0 """ +from __future__ import division, absolute_import, print_function, unicode_literals __author__ = ('Douglas Creager ', 'Michal Nazarewicz ') @@ -133,5 +134,5 @@ def getVersion(): if __name__ == '__main__': - print getVersion() + print(getVersion()) -- GitLab From 5b2bdda7c4464e5b382c0706f233a84325631a23 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Sun, 22 Dec 2019 01:03:09 -0600 Subject: [PATCH 106/111] version: use logging module --- dstat_interface/core/utils/version.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/dstat_interface/core/utils/version.py b/dstat_interface/core/utils/version.py index bc9c9ed..f20cefc 100644 --- a/dstat_interface/core/utils/version.py +++ b/dstat_interface/core/utils/version.py @@ -51,6 +51,9 @@ import subprocess import sys import os.path import inspect +import logging + +logger = logging.getLogger(__name__) RELEASE_VERSION_FILE = '{}/RELEASE-VERSION'.format( os.path.dirname(os.path.abspath(inspect.stack()[0][1]))) @@ -83,9 +86,9 @@ def readGitVersion(): if not ver: return None m = re.search(_GIT_DESCRIPTION_RE, ver) + if not m: - sys.stderr.write('version: git description (%s) is invalid, ' - 'ignoring\n' % ver) + logger.warning('Git description (%s) is invalid, ignoring', ver) return None commits = int(m.group('commits')) @@ -110,29 +113,28 @@ def readReleaseVersion(): finally: fd.close() if not re.search(_PEP386_VERSION_RE, ver): - sys.stderr.write('version: release version (%s) is invalid, ' - 'will use it anyway\n' % ver) + logger.warning('release version (%s) is invalid, will use it anyways', ver) return ver except: return None -def writeReleaseVersion(version): +def write_release_version(version): fd = open(RELEASE_VERSION_FILE, 'w') fd.write('%s\n' % version) fd.close() -def getVersion(): - release_version = readReleaseVersion() - version = readGitVersion() or release_version +def get_version(): + release_version = read_release_version() + version = read_git_version() or release_version if not version: raise ValueError('Cannot find the version number') if version != release_version: - writeReleaseVersion(version) + write_release_version(version) return version if __name__ == '__main__': - print(getVersion()) + print(get_version()) -- GitLab From 76853097321c920b0b1bdde06c9f68a073bc6b5a Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Sun, 22 Dec 2019 01:03:32 -0600 Subject: [PATCH 107/111] version: PEP-8 function names --- dstat_interface/core/utils/version.py | 4 ++-- dstat_interface/main.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dstat_interface/core/utils/version.py b/dstat_interface/core/utils/version.py index f20cefc..b47620b 100644 --- a/dstat_interface/core/utils/version.py +++ b/dstat_interface/core/utils/version.py @@ -66,7 +66,7 @@ _GIT_DESCRIPTION_RE = r'^v(?P%s)-(?P\d+)-g(?P[\da-f]+)$' % ( _PEP386_SHORT_VERSION_RE) -def readGitVersion(): +def read_git_version(): try: proc = subprocess.Popen(('git', 'describe', '--long', '--match', 'v[0-9]*.*'), @@ -105,7 +105,7 @@ def readGitVersion(): return version -def readReleaseVersion(): +def read_release_version(): try: fd = open(RELEASE_VERSION_FILE) try: diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 9f622fd..039f729 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -36,7 +36,7 @@ from serial import SerialException import zmq -from dstat_interface.core.utils.version import getVersion +from dstat_interface.core.utils.version import get_version from dstat_interface.core.experiments import idle, pot from dstat_interface.core import params, analysis, dstat from dstat_interface.core.dstat import boards @@ -172,7 +172,7 @@ class Main(GObject.Object): # Set Version Strings try: - ver = getVersion() + ver = get_version() except ValueError: ver = "1.x" logger.warning("Could not fetch version number") -- GitLab From 5b01d805fc215eecf8c19d4363e989279721538f Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Sun, 22 Dec 2019 01:06:15 -0600 Subject: [PATCH 108/111] version: fix unicode regex matching --- dstat_interface/core/utils/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/utils/version.py b/dstat_interface/core/utils/version.py index b47620b..0bc434b 100644 --- a/dstat_interface/core/utils/version.py +++ b/dstat_interface/core/utils/version.py @@ -74,7 +74,7 @@ def read_git_version(): data, _ = proc.communicate() if proc.returncode: return None - ver = data.splitlines()[0].strip() + ver = data.splitlines()[0].strip().decode('utf-8') proc = subprocess.Popen(('git', 'rev-parse', '--abbrev-ref', 'HEAD'), stdout=subprocess.PIPE, stderr=subprocess.PIPE) branch, _ = proc.communicate() -- GitLab From 714bde8c9de4dc3c8d9de24b8b20e94075288eba Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Mon, 23 Dec 2019 01:21:51 -0600 Subject: [PATCH 109/111] main: replace gtk3 deprecated functions --- dstat_interface/main.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/dstat_interface/main.py b/dstat_interface/main.py index 039f729..d13d843 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -48,7 +48,7 @@ from dstat_interface.core.plugin import DstatPlugin, get_hub_uri try: import gi gi.require_version('Gtk', '3.0') - from gi.repository import Gtk, GObject + from gi.repository import Gtk, GObject, GLib except ImportError: print("ERR: GTK not available") sys.exit(1) @@ -355,7 +355,7 @@ class Main(GObject.Object): logger.info("Start PMT idle mode") dstat.state.ser.start_exp(idle.PMTIdle()) self.ocp_is_running = True - self.ocp_proc = (GObject.timeout_add(250, self.ocp_running_proc) + self.ocp_proc = (GLib.timeout_add(250, self.ocp_running_proc) , ) @@ -363,12 +363,12 @@ class Main(GObject.Object): logger.info("Start OCP") dstat.state.ser.start_exp(idle.OCPExp()) - self.ocp_proc = (GObject.timeout_add(300, self.ocp_running_data), - GObject.timeout_add(250, self.ocp_running_proc) + self.ocp_proc = (GLib.timeout_add(300, self.ocp_running_data), + GLib.timeout_add(250, self.ocp_running_proc) ) self.ocp_is_running = False - GObject.timeout_add(100, self.ocp_assert) # Check if getting data + GLib.timeout_add(100, self.ocp_assert) # Check if getting data else: logger.info("OCP measurements not supported on v1.1 boards.") @@ -385,7 +385,7 @@ class Main(GObject.Object): dstat.state.ser.send_ctrl('a') for i in self.ocp_proc: - GObject.source_remove(i) + GLib.source_remove(i) while self.ocp_running_proc(): pass self.ocp_disp.set_text("") @@ -844,9 +844,8 @@ class Main(GObject.Object): if __name__ == "__main__": multiprocessing.freeze_support() - GObject.threads_init() MAIN = Main() - mainloop = GObject.MainLoop() + mainloop = GLib.MainLoop() try: mainloop.run() except KeyboardInterrupt: -- GitLab From 95fb78b3c86cecb2ca65db38dd5f85e49a483a88 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Mon, 23 Dec 2019 01:22:49 -0600 Subject: [PATCH 110/111] params: Set yaml loader explicitly --- dstat_interface/core/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dstat_interface/core/params.py b/dstat_interface/core/params.py index 63c1ae3..b60f871 100755 --- a/dstat_interface/core/params.py +++ b/dstat_interface/core/params.py @@ -67,7 +67,7 @@ def load_params(window, path): pass with open(path, 'r') as f: - params = yaml.load(f) + params = yaml.load(f, Loader=yaml.SafeLoader) set_params(window, params) -- GitLab From 867338bfd831a46e4bc94bb799c78fcfc44acd27 Mon Sep 17 00:00:00 2001 From: "Michael D. M. Dryden" Date: Tue, 15 Feb 2022 14:12:33 -0500 Subject: [PATCH 111/111] Change to Python 3 --- dstat_interface/core/dstat/boards.py | 2 +- dstat_interface/core/dstat/comm.py | 351 ++++++++++-------- dstat_interface/core/dstat/dfu.py | 33 +- dstat_interface/core/dstat/state.py | 17 +- .../core/experiments/experiment_loops.py | 1 + .../core/experiments/experiment_template.py | 58 +-- dstat_interface/core/experiments/idle.py | 19 +- dstat_interface/core/experiments/lsv.py | 6 +- dstat_interface/core/experiments/swv.py | 2 +- dstat_interface/core/interface/adc_pot.py | 1 + dstat_interface/core/interface/exp_int.py | 56 +-- dstat_interface/core/interface/exp_window.py | 4 +- dstat_interface/core/interface/save.py | 2 - dstat_interface/core/params.py | 2 +- dstat_interface/core/plugin.py | 8 +- dstat_interface/main.py | 49 ++- 16 files changed, 336 insertions(+), 275 deletions(-) diff --git a/dstat_interface/core/dstat/boards.py b/dstat_interface/core/dstat/boards.py index da4927f..421e02c 100755 --- a/dstat_interface/core/dstat/boards.py +++ b/dstat_interface/core/dstat/boards.py @@ -129,7 +129,7 @@ def find_board(version, booster=False): boards = __get_all_subclasses(BaseBoard) candidates = [] for board in boards: - req = parse_requirements('dstat~={}'.format(board.pcb_version)).next() + req = next(parse_requirements('dstat~={}'.format(board.pcb_version))) if board.booster == booster and version in req: candidates.append(board) try: diff --git a/dstat_interface/core/dstat/comm.py b/dstat_interface/core/dstat/comm.py index 017c0fc..605961d 100755 --- a/dstat_interface/core/dstat/comm.py +++ b/dstat_interface/core/dstat/comm.py @@ -25,44 +25,50 @@ from collections import OrderedDict import logging from pkg_resources import parse_version + try: import gi + gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: import sys + print("ERR: GTK not available") sys.exit(1) import serial from serial.tools import list_ports +from . import state from ..errors import InputError, VarError logger = logging.getLogger(__name__) dstat_logger = logging.getLogger("{}.DSTAT".format(__name__)) exp_logger = logging.getLogger("{}.Experiment".format(__name__)) -from . import state class AlreadyConnectedError(Exception): def __init__(self): super(AlreadyConnectedError, self).__init__(self, - "Serial instance already connected.") + "Serial instance already connected.") + class NotConnectedError(Exception): def __init__(self): super(NotConnectedError, self).__init__(self, - "Serial instance not connected.") - + "Serial instance not connected.") + + class ConnectionError(Exception): def __init__(self): super(ConnectionError, self).__init__(self, - "Could not connect.") + "Could not connect.") + class TransmitError(Exception): def __init__(self): super(TransmitError, self).__init__(self, - "No reply received.") + "No reply received.") def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): @@ -70,20 +76,19 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): connected = False for i in range(5): - time.sleep(1) # Give OS time to enumerate - + time.sleep(1) # Give OS time to enumerate + try: ser = serial.Serial(ser_port, timeout=1) - # ser = serial.Serial(ser_port, timeout=1) ser_logger.info("Connecting") time.sleep(.5) connected = True except serial.SerialException: pass - + if connected is True: break - + try: if ser.isOpen() is False: ser_logger.info("Connection Error") @@ -93,47 +98,51 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger.info("Connection Error") proc_pipe.send("SERIAL_ERROR") return 1 - - ser.write('!0 ') - + + ser.write(b'!0 ') + for i in range(10): - if ser.readline().rstrip()=="@ACK 0": - if ser.readline().rstrip()=="@RCV 0": + if ser.readline().rstrip() == b"@ACK 0": + if ser.readline().rstrip() == b"@RCV 0": break else: time.sleep(.5) ser.reset_input_buffer() - ser.write('!0 ') + ser.write(b'!0 ') time.sleep(.1) while True: # These can only be called when no experiment is running - if ctrl_pipe.poll(): + if ctrl_pipe.poll(): ctrl_buffer = ctrl_pipe.recv() if ctrl_buffer in ('a', "DISCONNECT"): proc_pipe.send("ABORT") try: - ser.write('a') + ser.write(b'a') except serial.SerialException: return 0 ser_logger.info("ABORT") - + if ctrl_buffer == "DISCONNECT": ser_logger.info("DISCONNECT") ser.rts = False ser._update_dtr_state() # Need DTR update on Windows - + ser.close() proc_pipe.send("DISCONNECT") return 0 else: - ser.write(ctrl_buffer) - + ser.write(ctrl_buffer.encode('ascii')) + elif proc_pipe.poll(): while ctrl_pipe.poll(): ctrl_pipe.recv() try: - return_code = proc_pipe.recv().run(ser, ctrl_pipe, data_pipe) + proc, params, state = proc_pipe.recv() + if params is None: + return_code = proc(state=state).run(ser, ctrl_pipe, data_pipe) + else: + return_code = proc(params, state).run(ser, ctrl_pipe, data_pipe) except serial.SerialException: proc_pipe.send("DISCONNECT") ser.rts = False @@ -143,10 +152,9 @@ def _serial_process(ser_port, proc_pipe, ctrl_pipe, data_pipe): ser_logger.info('Return code: %s', str(return_code)) proc_pipe.send(return_code) - + else: time.sleep(.1) - class SerialConnection(GObject.Object): @@ -154,20 +162,20 @@ class SerialConnection(GObject.Object): '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_pipe_c, self.ctrl_pipe_c, + self.data_pipe_c)) self.proc.start() time.sleep(2) if self.proc.is_alive() is False: @@ -179,22 +187,29 @@ class SerialConnection(GObject.Object): 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) - + clean_state = exp.state + try: + clean_state['dstat_version'] = None # Remove unpickleable Version class instance + except (TypeError, KeyError): + pass + try: + self.proc_pipe_p.send((type(exp), exp.parameters, clean_state)) + except AttributeError: + self.proc_pipe_p.send((type(exp), None, clean_state)) + def stop_exp(self): self.send_ctrl('a') - + def get_proc(self, block=False): self.assert_connected() - + if block is True: return self.proc_pipe_p.recv() else: @@ -216,7 +231,7 @@ class SerialConnection(GObject.Object): def get_data(self, block=False): self.assert_connected() - + if block is True: return self.data_pipe_p.recv() else: @@ -224,18 +239,18 @@ class SerialConnection(GObject.Object): return self.data_pipe_p.recv() else: return None - + def flush_data(self): self.assert_connected() - + while self.data_pipe_p.poll() is True: self.data_pipe_p.recv() - + def send_ctrl(self, ctrl): self.assert_connected() - + self.ctrl_pipe_p.send(ctrl) - + def disconnect(self): logger.info("Disconnecting") self.send_ctrl('DISCONNECT') @@ -243,10 +258,12 @@ class SerialConnection(GObject.Object): self.emit('disconnected') self.connected = False + class VersionCheck(object): - def __init__(self): - pass - + def __init__(self, parameters=None, state=None): + self.parameters = parameters + self.state = state + def run(self, ser, ctrl_pipe, data_pipe): """Tries to contact DStat and get version. Returns a tuple of (major, minor). If no response, returns empty tuple. @@ -256,39 +273,40 @@ class VersionCheck(object): """ try: ser.reset_input_buffer() - ser.write('!1\n') - + ser.write('!1\n'.encode('ascii')) + for i in range(10): - if ser.readline().rstrip()=="@ACK 1": - ser.write('V\n') - if ser.readline().rstrip()=="@RCV 1": + if ser.readline().rstrip() == b"@ACK 1": + ser.write(b'V\n') + if ser.readline().rstrip() == b"@RCV 1": break else: time.sleep(.5) ser.reset_input_buffer() - ser.write('!1\n') + ser.write(b'!1\n') time.sleep(.1) - + for line in ser: - dstat_logger.info(line.decode('utf-8')) + line = line.decode('ascii') + dstat_logger.info(line) if line.startswith('V'): input = line.lstrip('V') elif line.startswith("#"): - dstat_logger.info(line.lstrip().rstrip()) + dstat_logger.info(line.strip()) elif line.lstrip().startswith("@DONE"): - dstat_logger.debug(line.lstrip().rstrip()) + dstat_logger.debug(line.strip()) ser.reset_input_buffer() break - + pcb, sep, firmware = input.strip().rpartition('-') - + if pcb == "": pcb = firmware firmware = False logger.info("Your firmware does not support version detection.") - + data_pipe.send((pcb, False)) - + else: logger.info( "Firmware Version: {}".format( @@ -296,33 +314,34 @@ class VersionCheck(object): ) ) data_pipe.send(( - pcb, - hex(int(firmware)).lstrip('0x') - )) - + pcb, + hex(int(firmware)).lstrip('0x') + )) + logger.info( "PCB Version: {}".format(pcb) ) status = "DONE" - + except UnboundLocalError as e: status = "SERIAL_ERROR" - except SerialException as e: + except serial.SerialException as e: logger.error('SerialException: %s', e) status = "SERIAL_ERROR" finally: return status + def version_check(ser_port): """Tries to contact DStat and get version. Stores version in state. If no response, returns False, otherwise True. Arguments: ser_port -- address of serial port to use - """ + """ state.ser = SerialConnection() - + state.ser.connect(ser_port) state.ser.start_exp(VersionCheck()) result = state.ser.get_proc(block=True) @@ -336,90 +355,95 @@ def version_check(ser_port): state.dstat_version = parse_version(version) logger.debug("version_check done") time.sleep(.1) - + return True + class Settings(object): - def __init__(self, task, settings=None): - self.task = task - self.settings = settings - + def __init__(self, parameters=None, state=None): + self.parameters = parameters + self.state = state + self.task = parameters['task'] + try: + self.settings = parameters['settings'] + except KeyError: + pass + def run(self, ser, ctrl_pipe, data_pipe): """Tries to contact DStat and get settings. Returns dict of settings. """ - + self.ser = ser - - if 'w' in self.task: + + if self.task == 'w': self.write() - - if 'r' in self.task: + + if self.task == 'r': data_pipe.send(self.read()) - + status = "DONE" - + return status - + def read(self): - settings = OrderedDict() self.ser.reset_input_buffer() - self.ser.write('!2\n') + self.ser.write('!2\n'.encode('ascii')) for i in range(10): - if self.ser.readline().rstrip()=="@ACK 2": - self.ser.write('SR\n') - if self.ser.readline().rstrip()=="@RCV 2": + if self.ser.readline().rstrip() == b"@ACK 2": + self.ser.write('SR\n'.encode('ascii')) + if self.ser.readline().rstrip() == b"@RCV 2": break else: time.sleep(.5) self.ser.reset_input_buffer() - self.ser.write('!2\n') + self.ser.write('!2\n'.encode('ascii')) time.sleep(.1) - + 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("@DONE"): - dstat_logger.debug(line.lstrip().rstrip()) + line_in = line.decode('ascii') + if line_in.lstrip().startswith('S'): + input = line_in.lstrip().lstrip('S') + elif line_in.lstrip().startswith("#"): + dstat_logger.info(line_in.strip()) + elif line_in.lstrip().startswith("@DONE"): + dstat_logger.debug(line_in.strip()) self.ser.reset_input_buffer() break - + parted = input.rstrip().split(':') - - for i in range(len(parted)): - settings[parted[i].split('.')[0]] = [i, parted[i].split('.')[1]] + settings = OrderedDict((setting.split('.')[0], setting.split('.')[1]) for setting in parted) return settings - + def write_command(self, cmd, params=None, retry=5): """Write command to serial with optional number of retries.""" - def get_reply(retries = 3): + + def get_reply(retries=3): while True: reply = self.ser.readline().rstrip() - if reply.startswith('#'): + if reply.startswith(b'#'): dstat_logger.info(reply) - elif reply == "": + elif reply == b"": retries -= 1 if retries <= 0: raise TransmitError else: return reply - + n = len(cmd) if params is not None: n_params = len(params) - + for _ in range(retry): tries = 5 while True: time.sleep(0.2) self.ser.reset_input_buffer() - self.ser.write('!{}\n'.format(n)) + self.ser.write('!{}\n'.format(n).encode('ascii')) time.sleep(.1) - + try: reply = get_reply() except TransmitError: @@ -429,15 +453,15 @@ class Settings(object): pass else: break - - if reply != "@ACK {}".format(n): + + if reply != f"@ACK {n}".encode('ascii'): logger.warning("Expected ACK got: {}".format(reply)) continue - + tries = 5 - + while True: - self.ser.write('{}\n'.format(cmd)) + self.ser.write('{}\n'.format(cmd).encode('ascii')) try: reply = get_reply() except TransmitError: @@ -447,16 +471,16 @@ class Settings(object): pass else: break - - if reply != "@RCV {}".format(n): + + if reply != f"@RCV {n}".encode('ascii'): logger.warning("Expected RCV got: {}".format(reply)) continue if params is None: return True - + tries = 5 - + while True: try: reply = get_reply() @@ -467,19 +491,19 @@ class Settings(object): pass else: break - - if reply != "@RQP {}".format(n_params): + + if reply != f"@RQP {n_params}".encode('ascii'): logger.warning("Expected RQP got: {}".format(reply)) continue - + tries = 5 - + for i in params: while True: - self.ser.write(i + " ") + self.ser.write(i + " ".encode('ascii')) try: reply = get_reply() - if reply == "@RCVC {}".format(i): + if reply == f"@RCVC {i}".encode('ascii'): break except TransmitError: if tries <= 0: @@ -490,104 +514,109 @@ class Settings(object): break return True return False - + def write(self): - 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] - - to_write = " ".join(write_buffer) + " " - n = len(to_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] + + to_write = " ".join(self.settings.values()) + " " + # n = len(to_write) logger.debug("to_write = %s", to_write) - + if not self.write_command('SW' + to_write): logger.error("Could not write command.") - + + def read_settings(): """Tries to contact DStat and get settings. Returns dict of settings. """ - + state.ser.flush_data() - state.ser.start_exp(Settings(task='r')) + state.ser.start_exp(Settings(parameters={'task': 'r'})) state.settings = state.ser.get_data(block=True) logger.info("Read settings from DStat") logger.debug("read_settings: %s", state.ser.get_proc(block=True)) - + return - + + def write_settings(): """Tries to write settings to DStat from global settings var. """ - + logger.debug("Settings to write: %s", state.settings) - + state.ser.flush_data() - state.ser.start_exp(Settings(task='w', settings=state.settings)) + state.ser.start_exp(Settings(parameters={'task': 'w', 'settings': state.settings})) logger.info("Wrote settings to DStat") logger.debug("write_settings: %s", state.ser.get_proc(block=True)) - + return class LightSensor(object): def __init__(self): pass - + def run(self, ser, ctrl_pipe, data_pipe): """Tries to contact DStat and get light sensor reading. Returns uint of light sensor clear channel. """ - + ser.reset_input_buffer() - ser.write('!') - - while not ser.read()=="@": + ser.write('!'.encode('ascii')) + + while not ser.read() == b"@": self.ser.reset_input_buffer() - ser.write('!') - - ser.write('T') + ser.write('!'.encode('ascii')) + + ser.write('T'.encode('ascii')) for line in ser: - if line.lstrip().startswith('T'): - input = line.lstrip().lstrip('T') - elif line.lstrip().startswith("#"): + if line.lstrip().startswith(b'T'): + input = line.lstrip().lstrip(b'T') + elif line.lstrip().startswith(b"#"): dstat_logger.info(line.lstrip().rstrip()) - elif line.lstrip().startswith("@DONE"): + elif line.lstrip().startswith(b"@DONE"): dstat_logger.debug(line.lstrip().rstrip()) ser.reset_input_buffer() break - - parted = input.rstrip().split('.') - + + parted = input.rstrip().split(b'.') + data_pipe.send(parted[0]) status = "DONE" - + return status + def read_light_sensor(): """Tries to contact DStat and get light sensor reading. Returns uint of light sensor clear channel. """ - + state.ser.flush_data() state.ser.start_exp(LightSensor()) - + logger.debug("read_light_sensor: %s", state.ser.get_proc(block=True)) - + return state.ser.get_data(block=True) + class SerialDevices(object): """Retrieves and stores list of serial devices in self.ports""" + def __init__(self): self.ports = [] self.refresh() - + def refresh(self): """Refreshes list of ports.""" try: self.ports, _, _ = zip(*list_ports.grep("DSTAT")) except ValueError: self.ports = [] - logger.error("No serial ports found") \ No newline at end of file + logger.error("No serial ports found") diff --git a/dstat_interface/core/dstat/dfu.py b/dstat_interface/core/dstat/dfu.py index 3b56288..a903a7f 100755 --- a/dstat_interface/core/dstat/dfu.py +++ b/dstat_interface/core/dstat/dfu.py @@ -53,6 +53,7 @@ from .comm import dstat_logger, exp_logger fwurl = "http://microfluidics.utoronto.ca/gitlab/api/v4/projects/4/jobs/artifacts/master/download?job=1.2.3&private_token=zkgSx1FaaTP7yLyFKkX6" + class FWDialog(object): def __init__(self, parent, connect, stop_callback, disconnect_callback, signal='activate'): self.parent = parent @@ -67,7 +68,7 @@ class FWDialog(object): self.missing_deps() return - self.stop() # Stop OCP + self.stop() # Stop OCP version_result, master = test_firmware_version() if version_result is False: @@ -75,11 +76,11 @@ class FWDialog(object): return if version_result == 'latest': - message = "Your firmware is already up to date." + message = "Your firmware is already up to date. (or on a descendent branch)" secondary = "Click yes to reflash firmware anyways." elif version_result == 'devel': message = "Your firmware is not on the master branch." - secondary = "You may have a development version. " +\ + secondary = "You may have a development version. " \ "Click yes to reflash firmware anyways." elif version_result == 'old': message = "Your firmware is out of date." @@ -186,8 +187,9 @@ def assert_deps(): for key, command in deps.items(): try: output = subprocess.check_output(command.split(), - stderr=subprocess.STDOUT) - logger.info("%s\n%s", command, output) + stderr=subprocess.STDOUT).decode(sys.stdout.encoding) + logger.info("Assert: %s", command) + logger.info("Result: %s", output) result[key] = True except subprocess.CalledProcessError: logger.warning("{} is not available.".format(key)) @@ -195,6 +197,7 @@ def assert_deps(): return result + def download_fw(): # from https://stackoverflow.com/a/16518224 temp_dir = mkdtemp() logger.info("Temporary directory: {}".format(temp_dir)) @@ -237,6 +240,7 @@ def download_fw(): # from https://stackoverflow.com/a/16518224 return fw_path + def test_firmware_version(current=None): if current is None: current = state.firmware_version @@ -246,10 +250,10 @@ def test_firmware_version(current=None): os.chdir(temp_dir) # Go to temporary directory command = "git clone http://microfluidics.utoronto.ca/gitlab/dstat/dstat-firmware.git" - logger.info('Cloning master.') + logger.info('Cloning master') try: output = subprocess.check_output(command.split(), - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT).decode(sys.stdout.encoding) except subprocess.CalledProcessError as e: logger.error("git failed with error code {}".format(e.returncode)) logger.error("Output: {}".format(e.output)) @@ -259,14 +263,15 @@ def test_firmware_version(current=None): os.chdir("./dstat-firmware") command = "git rev-parse --short master" - master = subprocess.check_output(command.split(), stderr=subprocess.STDOUT) + master = subprocess.check_output(command.split(), stderr=subprocess.STDOUT).decode(sys.stdout.encoding) logger.info("Current master commit: {}".format(master)) command = "git merge-base --is-ancestor master {}".format(current) test = subprocess.call(command.split()) - + logger.info(test) + if test == 0: # already newest - logger.info('Firmware is latest available.') + logger.info('Firmware is latest available (or on a newer descendent branch).') return 'latest', master elif test == 1: # old version logger.info('Firmware is out of date.') @@ -278,20 +283,21 @@ def test_firmware_version(current=None): logger.error('Unexpected git error. Git exited {}'.format(test)) return False, None + def dfu_program(path='./dstat-firmware.hex'): """Tries to program DStat over USB with DFU with hex file at path.""" try: command = "dfu-programmer atxmega256a3u erase" output = subprocess.check_output(command.split(), - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT).decode(sys.stdout.encoding) logger.info("%s\n%s", command, output) command = "dfu-programmer atxmega256a3u flash {}".format(path) output = subprocess.check_output(command.split(), - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT).decode(sys.stdout.encoding) logger.info("%s\n%s", command, output) command = "dfu-programmer atxmega256a3u launch" output = subprocess.check_output(command.split(), - stderr=subprocess.STDOUT) + stderr=subprocess.STDOUT).decode(sys.stdout.encoding) logger.info("%s\n%s", command, output) except subprocess.CalledProcessError as e: logger.error("{} failed with output:".format(" ".join(e.cmd))) @@ -313,7 +319,6 @@ def dstat_enter_dfu(): if result in ('SERIAL_ERROR', 'DONE'): break logger.info(result) - # state.ser.disconnect() time.sleep(.1) diff --git a/dstat_interface/core/dstat/state.py b/dstat_interface/core/dstat/state.py index 12262c4..4906481 100644 --- a/dstat_interface/core/dstat/state.py +++ b/dstat_interface/core/dstat/state.py @@ -1,14 +1,29 @@ from collections import OrderedDict + def reset(): + """Resets state variables.""" + settings = OrderedDict() ser = None dstat_version = None firmware_version = None board_instance = None + +def get_state(): + """ + Get state variables + :return dict of pickleable state variables + """ + return {'settings': settings, + 'dstat_version': dstat_version, + 'firmware_version': firmware_version, + 'board_instance': board_instance} + + settings = OrderedDict() ser = None dstat_version = None firmware_version = None -board_instance = None \ No newline at end of file +board_instance = None diff --git a/dstat_interface/core/experiments/experiment_loops.py b/dstat_interface/core/experiments/experiment_loops.py index 67fce8e..aa3fcb0 100644 --- a/dstat_interface/core/experiments/experiment_loops.py +++ b/dstat_interface/core/experiments/experiment_loops.py @@ -29,6 +29,7 @@ try: gi.require_version('Gtk', '3.0') from gi.repository import Gtk, GObject except ImportError: + import sys print("ERR: GTK not available") sys.exit(1) diff --git a/dstat_interface/core/experiments/experiment_template.py b/dstat_interface/core/experiments/experiment_template.py index a82114a..32b1d29 100755 --- a/dstat_interface/core/experiments/experiment_template.py +++ b/dstat_interface/core/experiments/experiment_template.py @@ -40,16 +40,20 @@ except ImportError: from matplotlib.figure import Figure import matplotlib.gridspec as gridspec -from matplotlib.backends.backend_gtk3agg \ - import FigureCanvasGTK3Agg as FigureCanvas - +# from matplotlib.backends.backend_gtk3agg \ + # import FigureCanvasGTK3Agg as FigureCanvas + +from matplotlib.backends.backend_gtk3cairo \ + import FigureCanvasGTK3Cairo as FigureCanvas + + from pandas import DataFrame import seaborn as sns sns.set(context='paper', style='darkgrid') import serial -from ..dstat import state, comm +from ..dstat import comm from ..dstat.comm import TransmitError from . import experiment_loops @@ -71,26 +75,27 @@ class Experiment(GObject.Object): 'exp_done': (GObject.SIGNAL_RUN_FIRST, None, ()), } - def __init__(self, parameters): + def __init__(self, parameters=None, state=None): """Adds commands for gain and ADC.""" super(Experiment, self).__init__() self.current_command = None self.parameters = parameters + self.state = state self.databytes = 8 self.datapoint = 0 self.scan = 0 self.time = 0 self.plots = [] - self.re_voltage_scale = state.board_instance.re_voltage_scale + self.re_voltage_scale = self.state['board_instance'].re_voltage_scale - self.gain = state.board_instance.gain[int(self.parameters['gain'])] + self.gain = self.state['board_instance'].gain[int(self.parameters['gain'])] try: self.gain_trim = int( - state.settings[ - state.board_instance.gain_trim[int(self.parameters['gain'])] - ][1] + self.state['settings'][ + self.state['board_instance'].gain_trim[int(self.parameters['gain'])] + ] ) except AttributeError: logger.debug("No gain trim table.") @@ -132,7 +137,7 @@ class Experiment(GObject.Object): """Write command to serial with optional number of retries.""" def get_reply(retries=3): while True: - reply = self.serial.readline().rstrip() + reply = self.serial.readline().decode('ascii').rstrip() if reply.startswith('#'): dstat_logger.info(reply) elif reply == "": @@ -151,7 +156,7 @@ class Experiment(GObject.Object): while True: time.sleep(0.2) self.serial.reset_input_buffer() - self.serial.write('!{}\n'.format(n)) + self.serial.write('!{}\n'.format(n).encode('ascii')) time.sleep(.1) try: @@ -171,7 +176,7 @@ class Experiment(GObject.Object): tries = 5 while True: - self.serial.write('{}\n'.format(cmd)) + self.serial.write('{}\n'.format(cmd).encode('ascii')) try: reply = get_reply() except TransmitError: @@ -210,7 +215,7 @@ class Experiment(GObject.Object): for i in params: while True: - self.serial.write(i + " ") + self.serial.write("{} ".format(i).encode('ascii')) try: reply = get_reply() if reply == "@RCVC {}".format(i): @@ -239,7 +244,7 @@ class Experiment(GObject.Object): for i in self.commands: self.current_command = i status = "DONE" - if isinstance(i, (str, unicode)): + if isinstance(i, (str, bytes)): logger.info("Command: %s", i) if not self.write_command(i): @@ -280,14 +285,14 @@ class Experiment(GObject.Object): if self.ctrl_pipe.poll(): input = self.ctrl_pipe.recv() logger.info("serial_handler: %s", input) - if input == "DISCONNECT": - self.serial.write('a') + if input == b"DISCONNECT": + self.serial.write(b'a') self.serial.reset_input_buffer() logger.info("serial_handler: ABORT pressed!") time.sleep(.3) return False elif input == 'a': - self.serial.write('a') + self.serial.write(b'a') else: self.serial.write(input) @@ -296,8 +301,9 @@ class Experiment(GObject.Object): check_ctrl() for line in self.serial: check_ctrl() - - if line.startswith('B'): + line_in = line.decode("ascii") + + if line_in.startswith('B'): data = self.data_handler( (scan, self.serial.read(size=self.databytes))) data = self.data_postprocessing(data) @@ -305,17 +311,17 @@ class Experiment(GObject.Object): self.data_pipe.send(data) try: self.datapoint += 1 - except AttributeError: #Datapoint counting is optional + except AttributeError: # Datapoint counting is optional pass - elif line.lstrip().startswith('S'): + elif line_in.lstrip().startswith('S'): scan += 1 - elif line.lstrip().startswith("#"): - dstat_logger.info(line.lstrip().rstrip()) + elif line_in.lstrip().startswith("#"): + dstat_logger.info(line_in.strip()) - elif line.lstrip().startswith("@DONE"): - dstat_logger.debug(line.lstrip().rstrip()) + elif line_in.lstrip().startswith("@DONE"): + dstat_logger.debug(line_in.strip()) time.sleep(.3) return True diff --git a/dstat_interface/core/experiments/idle.py b/dstat_interface/core/experiments/idle.py index b8caf4d..6068f38 100644 --- a/dstat_interface/core/experiments/idle.py +++ b/dstat_interface/core/experiments/idle.py @@ -2,14 +2,16 @@ import time import struct from .experiment_template import Experiment -from ..dstat import state + class OCPExp(Experiment): """Open circuit potential measumement in statusbar.""" id = 'ocp' - def __init__(self): - self.re_voltage_scale = state.board_instance.re_voltage_scale + def __init__(self, parameters=None, state=None): + self.parameters = parameters + self.state = state + self.re_voltage_scale = self.state['board_instance'].re_voltage_scale self.databytes = 8 self.commands = ["EA", "EP0 0 "] @@ -30,11 +32,14 @@ class PMTIdle(Experiment): """PMT idle mode.""" id = "pmt_idle" - def __init__(self): + def __init__(self, parameters=None, state=None): + self.parameters = parameters + self.state = state + self.databytes = 8 self.commands = ["EA", "EM"] - self.commands[0] += "2 " # input buffer - self.commands[0] += "3 " # 2.5 Hz sample rate - self.commands[0] += "1 " # 2x PGA + self.commands[0] += "2 " # input buffer + self.commands[0] += "3 " # 2.5 Hz sample rate + self.commands[0] += "1 " # 2x PGA diff --git a/dstat_interface/core/experiments/lsv.py b/dstat_interface/core/experiments/lsv.py index ce2a2e1..5957c32 100644 --- a/dstat_interface/core/experiments/lsv.py +++ b/dstat_interface/core/experiments/lsv.py @@ -2,10 +2,12 @@ import time import struct from .experiment_template import PlotBox, Experiment - + + class LSVExp(Experiment): """Linear Scan Voltammetry experiment""" id = 'lsv' + def setup(self): self.plotlims['current_voltage']['xlims'] = tuple( sorted( @@ -64,4 +66,4 @@ class LSVExp(Experiment): try: return 1 - (abs(self.stop_mv - self.data['current_voltage'][-1][0][-1])/self.max_mv) except IndexError: - return 0 \ No newline at end of file + return 0 diff --git a/dstat_interface/core/experiments/swv.py b/dstat_interface/core/experiments/swv.py index d60c447..a8617b1 100644 --- a/dstat_interface/core/experiments/swv.py +++ b/dstat_interface/core/experiments/swv.py @@ -233,4 +233,4 @@ class DPVExp(SWVExp): try: return 1 - (abs(self.stop_mv - self.data['swv'][-1][0][-1])/self.max_mv) except IndexError: - return 0 \ No newline at end of file + return 0 diff --git a/dstat_interface/core/interface/adc_pot.py b/dstat_interface/core/interface/adc_pot.py index 770ee7f..a8c25c7 100755 --- a/dstat_interface/core/interface/adc_pot.py +++ b/dstat_interface/core/interface/adc_pot.py @@ -34,6 +34,7 @@ from ..dstat import state mod_dir = os.path.dirname(os.path.abspath(__file__)) + class adc_pot(object): def __init__(self): self.builder = Gtk.Builder() diff --git a/dstat_interface/core/interface/exp_int.py b/dstat_interface/core/interface/exp_int.py index e4dc40d..401c2cd 100755 --- a/dstat_interface/core/interface/exp_int.py +++ b/dstat_interface/core/interface/exp_int.py @@ -63,8 +63,8 @@ class ExpInterface(GObject.Object): self.entry = {} # to be used only for str parameters self._params = None - def get_experiment(self, parameters): - return self.__class__.experiment(parameters) + def get_experiment(self, parameters, state=None): + return self.__class__.experiment(parameters, state) def _fill_params(self): self._params = dict.fromkeys(self.entry.keys()) @@ -372,11 +372,11 @@ class PD(ExpInterface): self.builder.get_object('light_label').set_text(str( dstat_comm.read_light_sensor())) comm.read_settings() - state.settings['tcs_enabled'][1] = '1' # Make sure TCS enabled + state.settings['tcs_enabled'] = '1' # Make sure TCS enabled comm.write_settings() self.builder.get_object('threshold_entry').set_text(str( - state.settings['tcs_clear_threshold'][1])) + state.settings['tcs_clear_threshold'])) __main__.MAIN.start_ocp() finally: @@ -390,12 +390,12 @@ class PD(ExpInterface): i.set_sensitive(False) try: - state.settings['tcs_clear_threshold'][1] = self.builder.get_object( + state.settings['tcs_clear_threshold'] = self.builder.get_object( 'threshold_entry').get_text() comm.write_settings() comm.read_settings() self.builder.get_object('threshold_entry').set_text( - str(state.settings['tcs_clear_threshold'][1])) + str(state.settings['tcs_clear_threshold'])) __main__.MAIN.start_ocp() finally: @@ -453,19 +453,19 @@ class CAL(ExpInterface): comm.read_settings() self.entry['R100'].set_text(str( - state.settings['r100_trim'][1])) + state.settings['r100_trim'])) self.entry['R3k'].set_text(str( - state.settings['r3k_trim'][1])) + state.settings['r3k_trim'])) self.entry['R30k'].set_text(str( - state.settings['r30k_trim'][1])) + state.settings['r30k_trim'])) self.entry['R300k'].set_text(str( - state.settings['r300k_trim'][1])) + state.settings['r300k_trim'])) self.entry['R3M'].set_text(str( - state.settings['r3M_trim'][1])) + state.settings['r3M_trim'])) self.entry['R30M'].set_text(str( - state.settings['r30M_trim'][1])) + state.settings['r30M_trim'])) self.entry['R100M'].set_text(str( - state.settings['r100M_trim'][1])) + state.settings['r100M_trim'])) __main__.MAIN.start_ocp() @@ -480,13 +480,13 @@ class CAL(ExpInterface): __main__.MAIN.on_pot_stop_clicked() __main__.MAIN.stop_ocp() - 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() + state.settings['r100_trim'] = self.entry['R100'].get_text() + state.settings['r3k_trim'] = self.entry['R3k'].get_text() + state.settings['r30k_trim'] = self.entry['R30k'].get_text() + state.settings['r300k_trim'] = self.entry['R300k'].get_text() + state.settings['r3M_trim'] = self.entry['R3M'].get_text() + state.settings['r30M_trim'] = self.entry['R30M'].get_text() + state.settings['r100M_trim'] = self.entry['R100M'].get_text() comm.write_settings() __main__.MAIN.start_ocp() @@ -510,22 +510,22 @@ class CAL(ExpInterface): for i in offset: logger.info("{} {}".format(i, str(-offset[i]))) - state.settings[i][1] = str(-offset[i]) + state.settings[i] = str(-offset[i]) self.entry['R100'].set_text(str( - state.settings['r100_trim'][1])) + state.settings['r100_trim'])) self.entry['R3k'].set_text(str( - state.settings['r3k_trim'][1])) + state.settings['r3k_trim'])) self.entry['R30k'].set_text(str( - state.settings['r30k_trim'][1])) + state.settings['r30k_trim'])) self.entry['R300k'].set_text(str( - state.settings['r300k_trim'][1])) + state.settings['r300k_trim'])) self.entry['R3M'].set_text(str( - state.settings['r3M_trim'][1])) + state.settings['r3M_trim'])) self.entry['R30M'].set_text(str( - state.settings['r30M_trim'][1])) + state.settings['r30M_trim'])) self.entry['R100M'].set_text(str( - state.settings['r100M_trim'][1])) + state.settings['r100M_trim'])) __main__.MAIN.start_ocp() finally: diff --git a/dstat_interface/core/interface/exp_window.py b/dstat_interface/core/interface/exp_window.py index 8df980c..cced5a5 100755 --- a/dstat_interface/core/interface/exp_window.py +++ b/dstat_interface/core/interface/exp_window.py @@ -89,7 +89,7 @@ class Experiments(GObject.Object): """Change the experiment window when experiment box changed.""" self.set_exp(self.expcombobox.get_active_id()) - def setup_exp(self, parameters): + def setup_exp(self, parameters, state=None): """Takes parameters. Returns experiment instance. """ @@ -101,7 +101,7 @@ class Experiments(GObject.Object): "Experiment {} has no defined parameter test.".format( exp.name) ) - return exp.get_experiment(parameters) + return exp.get_experiment(parameters, state) def hide_exps(self): for key in self.containers: diff --git a/dstat_interface/core/interface/save.py b/dstat_interface/core/interface/save.py index 92e6d16..cde0ca3 100755 --- a/dstat_interface/core/interface/save.py +++ b/dstat_interface/core/interface/save.py @@ -34,9 +34,7 @@ except ImportError: import sys print("ERR: GTK not available") sys.exit(1) -import numpy as np -from ..errors import InputError, VarError from ..params import save_params, load_params def manSave(current_exp): diff --git a/dstat_interface/core/params.py b/dstat_interface/core/params.py index b60f871..a42778c 100755 --- a/dstat_interface/core/params.py +++ b/dstat_interface/core/params.py @@ -21,7 +21,7 @@ import logging import yaml -from errors import InputError +from .errors import InputError logger = logging.getLogger(__name__) diff --git a/dstat_interface/core/plugin.py b/dstat_interface/core/plugin.py index 59ec93d..ad1058e 100644 --- a/dstat_interface/core/plugin.py +++ b/dstat_interface/core/plugin.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- import logging -from params import get_params, set_params, load_params, save_params -from interface.save import save_text, save_plot -from zmq_plugin.plugin import Plugin as ZmqPlugin -from zmq_plugin.schema import decode_content_data +from .params import get_params, set_params, load_params, save_params +from .interface.save import save_text, save_plot +# from zmq_plugin.plugin import Plugin as ZmqPlugin +# from zmq_plugin.schema import decode_content_data import gtk import zmq diff --git a/dstat_interface/main.py b/dstat_interface/main.py index d13d843..4fd1164 100755 --- a/dstat_interface/main.py +++ b/dstat_interface/main.py @@ -43,7 +43,7 @@ from dstat_interface.core.dstat import boards from dstat_interface.core.interface import (exp_window, adc_pot, plot_ui, data_view, save, hw_info) from dstat_interface.core.errors import InputError -from dstat_interface.core.plugin import DstatPlugin, get_hub_uri +# from dstat_interface.core.plugin import DstatPlugin, get_hub_uri try: import gi @@ -56,9 +56,6 @@ except ImportError: mod_dir = os.path.dirname(os.path.abspath(__file__)) conf_path = os.path.join(os.path.expanduser("~"), '.dstat-interface') -# if __name__ == "__parents_main__": # Only runs for forking emulation on win -# sys.path.append(mod_dir) - # Setup Logging logger = logging.getLogger(__name__) core_logger = logging.getLogger("dstat_interface.core") @@ -276,15 +273,17 @@ class Main(GObject.Object): dstat.comm.read_settings() try: - if dstat.state.settings['dac_units_true'][1] != b'1': - dstat.state.settings['dac_units_true'][1] = b'1' + if dstat.state.settings['dac_units_true'] != '1': + dstat.state.settings['dac_units_true'] = '1' dstat.comm.write_settings() except KeyError: logger.warning("Connected DStat does not support sending DAC units.") dialog = Gtk.MessageDialog( - self.window, 0, Gtk.MessageType.WARNING, - Gtk.ButtonsType.OK, "Connected DStat does not support sending DAC units." + - "Update firmware or set potentials will be incorrect!") + parent=self.window, + flags=0, message_type=Gtk.MessageType.WARNING, + buttons=Gtk.ButtonsType.OK, + text="Connected DStat does not support sending DAC units." + + "Update firmware or set potentials will be incorrect!") dialog.run() dialog.destroy() @@ -353,7 +352,7 @@ class Main(GObject.Object): if self.pmt_mode is True: logger.info("Start PMT idle mode") - dstat.state.ser.start_exp(idle.PMTIdle()) + dstat.state.ser.start_exp(idle.PMTIdle(state=dstat.state.get_state())) self.ocp_is_running = True self.ocp_proc = (GLib.timeout_add(250, self.ocp_running_proc) , @@ -361,7 +360,7 @@ class Main(GObject.Object): else: logger.info("Start OCP") - dstat.state.ser.start_exp(idle.OCPExp()) + dstat.state.ser.start_exp(idle.OCPExp(state=dstat.state.get_state())) self.ocp_proc = (GLib.timeout_add(300, self.ocp_running_data), GLib.timeout_add(250, self.ocp_running_proc) @@ -420,7 +419,7 @@ class Main(GObject.Object): try: incoming = dstat.state.ser.get_data() while incoming is not None: - if isinstance(incoming, basestring): # test if incoming is str + if isinstance(incoming, bytes): # test if incoming is str self.on_serial_disconnect_clicked() return False @@ -530,7 +529,7 @@ class Main(GObject.Object): callbacks = {'experiment_done' : self.experiment_done, 'progress_update' : self.progress_update} - self.current_exp = self.exp_window.setup_exp(parameters) + self.current_exp = self.exp_window.setup_exp(parameters, state=dstat.state.get_state()) plot_ui.replace_notebook_exp( self.plot_notebook, self.current_exp, self.window @@ -547,7 +546,7 @@ class Main(GObject.Object): return experiment_id except ValueError as i: - logger.info("ValueError: %s",i) + logger.info("ValueError: %s", i) self.statusbar.push(self.error_context_id, "Experiment parameters must be integers.") exceptions() @@ -675,7 +674,7 @@ class Main(GObject.Object): removed from GTK's queue. """ for plot in self.current_exp.plots: - if (plot.scan_refresh and self.line > self.lastline): + if plot.scan_refresh and self.line > self.lastline: while self.line > self.lastline: # make sure all of last line is added plot.updateline(self.current_exp, self.lastline) @@ -798,14 +797,14 @@ class Main(GObject.Object): """Listen for remote control connection from µDrop.""" # Prompt user for 0MQ plugin hub URI. - zmq_plugin_hub_uri = get_hub_uri(parent=self.window) + # zmq_plugin_hub_uri = get_hub_uri(parent=self.window) self.dropbot_enabled = True self.menu_dropbot_connect.set_sensitive(False) self.menu_dropbot_disconnect.set_sensitive(True) self.statusbar.push(self.message_context_id, "Waiting for µDrop to connect…") - self.enable_plugin(zmq_plugin_hub_uri) + # self.enable_plugin(zmq_plugin_hub_uri) def on_menu_dropbot_disconnect_activate(self, menuitem=None, data=None): """Disconnect µDrop connection and stop listening.""" @@ -826,14 +825,14 @@ class Main(GObject.Object): ''' self.cleanup_plugin() # Initialize 0MQ hub plugin and subscribe to hub messages. - self.plugin = DstatPlugin(self, 'dstat-interface', hub_uri, - subscribe_options={zmq.SUBSCRIBE: ''}) - # Initialize sockets. - self.plugin.reset() - - # Periodically process outstanding message received on plugin sockets. - self.plugin_timeout_id = Gtk.timeout_add(500, - self.plugin.check_sockets) + # self.plugin = DstatPlugin(self, 'dstat-interface', hub_uri, + # subscribe_options={zmq.SUBSCRIBE: ''}) + # # Initialize sockets. + # self.plugin.reset() + # + # # Periodically process outstanding message received on plugin sockets. + # self.plugin_timeout_id = Gtk.timeout_add(500, + # self.plugin.check_sockets) def cleanup_plugin(self): if self.plugin_timeout_id is not None: -- GitLab