├── MANIFEST.in ├── tclab ├── __main__.py ├── version.py ├── __init__.py ├── experiment.py ├── labtime.py ├── gui.py ├── historian.py └── tclab.py ├── .coveralls.yml ├── requirements.txt ├── notebooks ├── images │ ├── TCLabOverview.pdf │ ├── TCLabOverview.png │ ├── tclab_device.png │ └── TCLabOverview.tex ├── 04_Emulation_of_TCLab_for_Offline_Use.ipynb ├── 01_TCLab_Overview.ipynb ├── 03_Synchronizing_with_Real_Time.ipynb ├── 09_Labtime_Class.ipynb └── 02_Accessing_the_Temperature_Control_Laboratory.ipynb ├── .travis.yml ├── experimental ├── README.rst ├── PyMata Implementation.ipynb ├── VocCommChannel.py └── 10_Interactive_and_Nonblocking_Operation.ipynb ├── setup.cfg ├── tests ├── test_experiment.py ├── test_setup.py ├── test_version.py ├── test_tagdb.py ├── test_historian.py ├── test_tclab.py └── test_labtime.py ├── Makefile ├── CONTRIBUTORS.md ├── index.rst ├── DEVELOPMENT.rst ├── .gitignore ├── TROUBLESHOOTING.md ├── README.rst ├── conf.py ├── setup.py └── LICENSE.txt /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /tclab/__main__.py: -------------------------------------------------------------------------------- 1 | from .tclab import diagnose 2 | 3 | 4 | diagnose() -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 4JGMx8swBBgdLTVxTS6kF7WZCGBxMLh3D 2 | service_name: travis-ci 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=1.4 2 | ipykernel 3 | nbsphinx 4 | python-coveralls 5 | pytest-cov 6 | -------------------------------------------------------------------------------- /notebooks/images/TCLabOverview.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jckantor/TCLab/HEAD/notebooks/images/TCLabOverview.pdf -------------------------------------------------------------------------------- /notebooks/images/TCLabOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jckantor/TCLab/HEAD/notebooks/images/TCLabOverview.png -------------------------------------------------------------------------------- /notebooks/images/tclab_device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jckantor/TCLab/HEAD/notebooks/images/tclab_device.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | env: 7 | - TRAVIS=True 8 | install: 9 | - python setup.py install 10 | script: 11 | - pytest 12 | after_success: 13 | - coveralls 14 | -------------------------------------------------------------------------------- /experimental/README.rst: -------------------------------------------------------------------------------- 1 | Experimental 2 | ============ 3 | 4 | This directory contains items that are experimental in nature including python code, Jupyter notebooks documenting possible use cases, and development notes. User beware! 5 | -------------------------------------------------------------------------------- /tclab/version.py: -------------------------------------------------------------------------------- 1 | # Store the version here so: 2 | # 1) we don't load dependencies by storing it in __init__.py 3 | # 2) we can import it in setup.py for the same reason 4 | # 3) we can import it into your module module 5 | 6 | __version__ = "0.4.10dev" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | # This flag says to generate wheels that support both Python 2 and Python 3 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 4 | # need to generate separate wheels for each Python version that you 5 | # support. 6 | universal=1 7 | -------------------------------------------------------------------------------- /tests/test_experiment.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tclab.experiment import Experiment, runexperiment 4 | 5 | 6 | def test_constructor(): 7 | e = Experiment(connected=False, plot=False) 8 | 9 | 10 | def test_experiment_context(): 11 | with Experiment(connected=False, plot=False) as experiment: 12 | pass 13 | 14 | 15 | def test_experiment_run(): 16 | with Experiment(connected=False, plot=False, time=5) as experiment: 17 | for t in experiment.clock(): 18 | pass 19 | 20 | 21 | def test_runexperiment(): 22 | def function(t, lab): 23 | pass 24 | 25 | runexperiment(function, connected=False, plot=False, time=5) 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = TCLab 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tclab import setup, TCLab, TCLabModel 4 | 5 | 6 | def test_connected(): 7 | assert TCLab == setup(connected=True, speedup=1) 8 | assert TCLabModel == setup(connected=False, speedup=2) 9 | assert TCLabModel == setup(connected=False, speedup=10) 10 | 11 | 12 | def test_connected_error(): 13 | with pytest.raises(ValueError): 14 | setup(connected=True, speedup=2) 15 | with pytest.raises(ValueError): 16 | setup(connected=True, speedup=10) 17 | with pytest.raises(ValueError): 18 | setup(connected=False, speedup=0) 19 | with pytest.raises(ValueError): 20 | setup(connected=False, speedup=-1) 21 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import re 4 | import tclab 5 | 6 | 7 | def test_version_type(): 8 | """Test that version is a string.""" 9 | assert isinstance(tclab.__version__, str) 10 | 11 | 12 | def test_version_file(): 13 | """Test code used in conf.py for recovering version.""" 14 | exec(open('./tclab/version.py').read()) 15 | 16 | 17 | def test_version_conf(): 18 | """Test code used in conf.py for recovering version.""" 19 | version = '.'.join(tclab.__version__.split('.')[0:2]) 20 | 21 | 22 | def test_version_regex(): 23 | """Regular expression match for version number with optional dev at end.""" 24 | assert re.match('^(\d+\.)(\d+\.)(\d+)(dev)?$', tclab.__version__) 25 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | TCLab contributors (alphabetically) 2 | =================================== 3 | 4 | * **[John Hedengren](https://apm.byu.edu/prism/index.php/Members/JohnHedengren)** 5 | 6 | * Development of the Temperature Control Laboratory 7 | 8 | * **[Abe Martin](https://apm.byu.edu/prism/index.php/Members/AbeMartin)** 9 | 10 | * Design of the Temperature Control Laboratory 11 | 12 | * **[Jeff Kantor](https://engineering.nd.edu/profiles/jkantor)** 13 | 14 | * Author of sketch firmware 15 | * TCLab module 16 | 17 | * **[Carl Sandrock](http://www.up.ac.za/chemeng/csandrock)** 18 | 19 | * Historian module 20 | * Package development 21 | 22 | **[Full contributors list](https://github.com/jckantor/TCLab/contributors).** 23 | -------------------------------------------------------------------------------- /tests/test_tagdb.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tclab.historian import TagDB 4 | 5 | 6 | @pytest.fixture() 7 | def db(): 8 | return TagDB() 9 | 10 | 11 | def test_start_session(db): 12 | assert db.session is None 13 | db.new_session() 14 | assert db.session is not None 15 | 16 | 17 | def test_record(db): 18 | db.record(0, "Test", 1) 19 | 20 | 21 | def test_get(db): 22 | db.record(0, "Test", 1) 23 | assert db.get("Test") == [(0, 1)] 24 | 25 | db.record(1, "Test", 2) 26 | assert db.get("Test") == [(0, 1), (1, 2)] 27 | 28 | 29 | def test_get_sessions(db): 30 | db.new_session() 31 | db.new_session() 32 | sessions = db.get_sessions() 33 | 34 | assert len(sessions) == 2 35 | assert sessions[0][0] == 1 36 | assert sessions[1][0] == 2 37 | -------------------------------------------------------------------------------- /index.rst: -------------------------------------------------------------------------------- 1 | .. TCLab documentation master file, created by 2 | sphinx-quickstart on Sat Feb 10 09:41:30 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | TCLab Documentation 7 | =================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | README.rst 14 | TROUBLESHOOTING.md 15 | 16 | notebooks/01_TCLab_Overview.ipynb 17 | notebooks/02_Accessing_the_Temperature_Control_Laboratory.ipynb 18 | notebooks/03_Synchronizing_with_Real_Time.ipynb 19 | notebooks/04_Emulation_of_TCLab_for_Offline_Use.ipynb 20 | notebooks/05_TCLab_Historian.ipynb 21 | notebooks/06_TCLab_Plotter.ipynb 22 | notebooks/07_NotebookUI.ipynb 23 | notebooks/08_Experiment_Class.ipynb 24 | notebooks/09_Labtime_Class.ipynb 25 | 26 | 27 | Search Page 28 | ================== 29 | 30 | * :ref:`search` 31 | -------------------------------------------------------------------------------- /DEVELOPMENT.rst: -------------------------------------------------------------------------------- 1 | During development 2 | ================== 3 | 4 | 1. To develop, in the top directory of the respository use:: 5 | 6 | python setup.py develop 7 | 8 | 2. With the Arduino plugged, test changes using:: 9 | 10 | python -m pytest -v 11 | python -m pytest --cov=tclab tests/ 12 | 13 | Note that `pytest -v` fails because the root file is not included in the 14 | search path. 15 | 16 | After making changes 17 | -------------------- 18 | 19 | 1. Change the version number in ``tclab/version.py``. 20 | 2. Check the distribution:: 21 | 22 | python setup.py check 23 | 24 | 3. Push changes through to the master branch on Github. 25 | 4. Create and push tag for the version number:: 26 | 27 | git tag vX.Y.Z 28 | git push --tags 29 | 30 | 31 | Uploading to PyPI 32 | ----------------- 33 | 34 | 1. Build the distribution:: 35 | 36 | python setup.py sdist bdist_wheel 37 | 38 | 2. Upload (also see the `Python Packaging User Guide `__:: 39 | 40 | twine upload dist/* 41 | 42 | -------------------------------------------------------------------------------- /tclab/__init__.py: -------------------------------------------------------------------------------- 1 | from .tclab import TCLab, TCLabModel, diagnose 2 | from .historian import Historian, Plotter 3 | from .experiment import Experiment, runexperiment 4 | from .labtime import clock, labtime, setnow 5 | from .version import __version__ 6 | 7 | 8 | def setup(connected=True, speedup=1): 9 | """Set up a lab session with simple switching between real and model lab 10 | 11 | The idea of this function is that you will do 12 | 13 | >>> lab = setup(connected=True) 14 | 15 | to obtain a TCLab class reference. If `connected=False` then you will 16 | receive a TCLabModel class reference. This allows you to switch between 17 | the model and the real lab in your code easily. 18 | 19 | The speedup option can only be used when `connected=False` and is the 20 | ratio by which the lab clock will be sped up relative to real time 21 | during the simulation. 22 | 23 | For example 24 | 25 | >>> lab = setup(connected=False, speedup=2) 26 | 27 | will run the lab clock at twice real time (which means that the whole 28 | simulation will take half the time it would if connected to a real device). 29 | """ 30 | 31 | if connected: 32 | lab = TCLab 33 | if speedup != 1: 34 | raise ValueError('The real lab must run in real time') 35 | else: 36 | lab = TCLabModel 37 | if speedup < 0: 38 | raise ValueError('speedup must be positive. ' 39 | 'You passed speedup={}'.format(speedup)) 40 | 41 | labtime.set_rate(speedup) 42 | return lab 43 | -------------------------------------------------------------------------------- /notebooks/images/TCLabOverview.tex: -------------------------------------------------------------------------------- 1 | \documentclass[tikz]{standalone} 2 | \usepackage{units} 3 | \usepackage{helvet} 4 | \usepackage[T1]{sansmath} 5 | \renewcommand{\familydefault}{\sfdefault} 6 | \normalfont 7 | 8 | \usetikzlibrary{arrows,positioning} 9 | 10 | \begin{document} 11 | \begin{sansmath} 12 | 13 | \tikzstyle{block} = [ 14 | draw, 15 | minimum height = 1.0cm, 16 | minimum width = 5.0cm, 17 | rounded corners, 18 | fill=yellow!20, 19 | rectangle, 20 | text centered] 21 | 22 | \tikzstyle{env} = [ 23 | draw, 24 | minimum height = 4.0cm, 25 | minimum width = 6.0cm, 26 | rounded corners, 27 | fill=blue!20, 28 | rectangle, 29 | text centered] 30 | 31 | \begin{tikzpicture}[auto,thick,node distance = 1cm] 32 | %\draw[step=1cm,gray,very thin] (0,0) grid (16,9); 33 | 34 | \draw (5,7) node[env] (laptop) {}; 35 | \draw (5,2) node[env] (device) {}; 36 | \draw (5,8) node[block] (top) {\large\bf Jupyter notebooks \textbar\ Python}; 37 | \draw (5,6) node[block] (mid) {\large\bf tclab.py}; 38 | \draw (5,3) node[block] (bot) {\large\bf TCLab-sketch.ino}; 39 | \draw (5,1) node[block] (ard) {\large\bf Arduino}; 40 | 41 | \draw[->, ultra thick] (top) ++(-1,-0.5) -- ++(0,-1); 42 | \draw[->, ultra thick] (mid) ++(-1,-0.5) -- ++(0,-2); 43 | \draw[->, ultra thick] (bot) ++(-1,-0.5) -- ++(0,-1); 44 | 45 | \draw[<-, ultra thick] (top) ++(1,-0.5) -- ++(0,-1); 46 | \draw[<-, ultra thick] (mid) ++(1,-0.5) -- ++(0,-2); 47 | \draw[<-, ultra thick] (bot) ++(1,-0.5) -- ++(0,-1); 48 | 49 | \draw (7,4.5) node {USB}; 50 | \draw (2.5,8.75) node[right] {Laptop}; 51 | \draw (2.5,0.25) node[right] {Arduino}; 52 | 53 | \end{tikzpicture} 54 | \end{sansmath} 55 | \end{document} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .DS_Store 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | .texpadtmp 107 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Diagnose and Troubleshooting 2 | 3 | The library supplies a simple way to diagnose errors with the TCLab device in 4 | a function called `diagnose`, which is called as follows: 5 | 6 | ```python 7 | from tclab import diagnose 8 | 9 | diagnose() 10 | ``` 11 | 12 | This function will attempt to find the Arduino device, make a connection and 13 | attempt to exercise the full command set of the device to make sure everything 14 | is working correctly. 15 | 16 | The above code can also be run from a terminal by using 17 | 18 | ```shell 19 | python -m tclab 20 | ``` 21 | 22 | 23 | ## Problems and solutions 24 | 25 | ### No Arduino device found 26 | 27 | 1. First confirm that the device is correctly plugged in. 28 | 2. Plug it out and back in 29 | 3. Try a different port. 30 | 4. If no configuration has worked, you may need to install drivers (see below) 31 | 32 | 33 | ### Access denied 34 | 35 | A device has been found but you get an error which mentions "Access denied". 36 | 37 | If you are using Windows, this can be resolved by going to Device Manager and 38 | selecting a different port for the device. If the device shows up incorrecty 39 | in the Device Manager, you may need to install drivers (see below) 40 | 41 | 42 | ### Setting heaters makes temperature jump 43 | 44 | You may have plugged both of the USB leads into one computer. The device works 45 | best when the barrel-ended jack is plugged into a separate power supply or 46 | a different computer. 47 | 48 | 49 | ### Setting heater to 100 doesn't raise temperature 50 | 51 | You may only have plugged in your device into your computer using one cable. 52 | Your device needs to be plugged in to your computer *and* requires another 53 | connection to a power supply to power the heaters. 54 | 55 | 56 | ## Software fixes 57 | ### Install Drivers 58 | *If you are using Windows 10, the Arduino board should connect without additional drivers required.* 59 | 60 | For Arduino clones using the CH340G, CH34G or CH34X chipset you may need additional drivers. Only install these if you see a message saying "No Arduino device found." when connecting. 61 | 62 | * [macOS](https://github.com/adrianmihalko/ch340g-ch34g-ch34x-mac-os-x-driver) 63 | * [Windows](http://www.wch.cn/downfile/65) 64 | 65 | 66 | ### Update Firmware 67 | It is usually best to use the most recent version of the Arduino firmware, 68 | available from the [TCLab-Sketch repository](https://github.com/jckantor/TCLab-sketch). 69 | 70 | 71 | ### Update TCLab python library 72 | If you find that the code supplied in the documentation gives errors about 73 | functions not being found, or if you installed tclab a long time ago, you need 74 | to update the TCLab library. This can be done with the command 75 | 76 | ``` 77 | pip install --update tclab 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /experimental/PyMata Implementation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 7, 6 | "metadata": {}, 7 | "outputs": [ 8 | { 9 | "name": "stdout", 10 | "output_type": "stream", 11 | "text": [ 12 | "0 0\n", 13 | "1.0 157\n", 14 | "2.0 3713\n", 15 | "3.0 157\n", 16 | "4.0 157\n", 17 | "5.0 157\n" 18 | ] 19 | } 20 | ], 21 | "source": [ 22 | "import time\n", 23 | "\n", 24 | "from PyMata.pymata import PyMata\n", 25 | "from tclab import clock\n", 26 | "\n", 27 | "pinT1 = 0\n", 28 | "pinT2 = 2\n", 29 | "pinQ1 = 3\n", 30 | "pinQ2 = 5\n", 31 | "pinLED1 = 9\n", 32 | "\n", 33 | "class TCLabPyMata(object):\n", 34 | "\n", 35 | " \n", 36 | " def __init__(self, port=\"/dev/cu.usbmodem1411\"):\n", 37 | " self.board = PyMata(port, verbose=False)\n", 38 | " self.board.enable_analog_reporting(pinT1)\n", 39 | " \n", 40 | " def __enter__(self):\n", 41 | " return self\n", 42 | "\n", 43 | " def __exit__(self, exc_type, exc_value, traceback):\n", 44 | " self.close()\n", 45 | " return\n", 46 | " \n", 47 | " def LED(self, val=100):\n", 48 | " self.board.digital_write(pinLED1, 1)\n", 49 | " time.sleep(10)\n", 50 | " self.board.digital_write(pinLED1, 0)\n", 51 | " \n", 52 | " def close(self):\n", 53 | " self.board._command_handler.system_reset()\n", 54 | " self.board._command_handler.stop()\n", 55 | " self.board.transport.stop()\n", 56 | " self.board.transport.close()\n", 57 | " \n", 58 | " def T1(self):\n", 59 | " return self.board.analog_read(pinT1)\n", 60 | " \n", 61 | "\n", 62 | "with TCLabPyMata() as lab:\n", 63 | " #lab.LED()\n", 64 | " for t in clock(5):\n", 65 | " print(t, lab.T1())\n", 66 | " \n" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": 13, 72 | "metadata": {}, 73 | "outputs": [], 74 | "source": [ 75 | "from pyfirmata import Arduino, util\n" 76 | ] 77 | }, 78 | { 79 | "cell_type": "code", 80 | "execution_count": null, 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [] 84 | } 85 | ], 86 | "metadata": { 87 | "kernelspec": { 88 | "display_name": "Python 3", 89 | "language": "python", 90 | "name": "python3" 91 | }, 92 | "language_info": { 93 | "codemirror_mode": { 94 | "name": "ipython", 95 | "version": 3 96 | }, 97 | "file_extension": ".py", 98 | "mimetype": "text/x-python", 99 | "name": "python", 100 | "nbconvert_exporter": "python", 101 | "pygments_lexer": "ipython3", 102 | "version": "3.6.4" 103 | } 104 | }, 105 | "nbformat": 4, 106 | "nbformat_minor": 2 107 | } 108 | -------------------------------------------------------------------------------- /tests/test_historian.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tclab import Historian 4 | 5 | 6 | def test_constructor(): 7 | h = Historian(sources=()) 8 | 9 | 10 | def test_logging(): 11 | a = 0 12 | b = 0 13 | 14 | h = Historian(sources=[('a', lambda: a), 15 | ('b', lambda: b)]) 16 | 17 | a = 0.5 18 | h.update(1) 19 | a = 1 20 | h.update(2) 21 | b = 1 22 | h.update(3) 23 | 24 | assert h.logdict['a'][-1] == 1 25 | 26 | log = h.log 27 | 28 | assert len(log) == 3 29 | 30 | assert h.at(1, ['a']) == [0.5] 31 | 32 | assert h.after(2) == [[2, 3], [1, 1], [0, 1]] 33 | 34 | 35 | def test_implicit_time(): 36 | h = Historian(sources=[('a', lambda: a)]) 37 | 38 | a = 0 39 | h.update() 40 | a = 1 41 | h.update() 42 | 43 | assert len(h.log) == 2 44 | 45 | 46 | def test_wrong_arguments(): 47 | h = Historian(sources=[('a', lambda: [1]), 48 | ('b', None)]) 49 | 50 | with pytest.raises(ValueError): 51 | h.update() 52 | 53 | 54 | def test_logging_list(): 55 | a = 0 56 | b = 0 57 | 58 | h = Historian(sources=[('a', lambda: (a, b)), 59 | ('b', None)]) 60 | 61 | a = 0.5 62 | h.update(1) 63 | a = 1 64 | h.update(2) 65 | 66 | assert h.logdict['a'][-1] == 1 67 | 68 | log = h.log 69 | 70 | assert len(log) == 2 71 | 72 | assert h.at(1, ['a']) == [0.5] 73 | 74 | 75 | def test_sessions(): 76 | h = Historian(sources=[('a', lambda: a)]) 77 | 78 | a = 0 79 | h.update(1) 80 | a = 2 81 | h.update(2) 82 | 83 | assert h.session == 1 84 | assert len(h.get_sessions()) == 1 85 | 86 | h.new_session() 87 | 88 | assert h.session == 2 89 | 90 | a = 2 91 | h.update(1) 92 | a = 3 93 | h.update(2) 94 | h.update(3) 95 | 96 | assert len(h.get_sessions()) == 2 97 | 98 | assert h.at(1, ['a']) == [2] 99 | 100 | h.load_session(1) 101 | 102 | assert h.at(1, ['a']) == [0] 103 | 104 | 105 | def test_error_handling(): 106 | h = Historian(sources=[('a', lambda: a)], dbfile=None) 107 | 108 | with pytest.raises(NotImplementedError): 109 | h.new_session() 110 | 111 | with pytest.raises(NotImplementedError): 112 | h.get_sessions() 113 | 114 | with pytest.raises(NotImplementedError): 115 | h.load_session(1) 116 | 117 | 118 | def test_to_csv(tmpdir): 119 | outfile = tmpdir.join('test.csv') 120 | 121 | a = 0 122 | b = 0 123 | 124 | h = Historian(sources=[('a', lambda: (a, b)), 125 | ('b', None)]) 126 | 127 | a = 0.5 128 | h.update(1) 129 | a = 1 130 | h.update(2) 131 | 132 | # we need the str here for older versions of Python 133 | h.to_csv(str(outfile)) 134 | 135 | import csv 136 | lines = list(csv.reader(outfile.open())) 137 | 138 | assert len(lines) == 3 139 | assert lines[0] == h.columns 140 | assert lines[1:] == [[str(i) for i in line] for line in h.log] 141 | -------------------------------------------------------------------------------- /tests/test_tclab.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tclab import TCLabModel, TCLab 4 | from tclab.tclab import AlreadyConnectedError 5 | import os 6 | 7 | TRAVIS = "TRAVIS" in os.environ 8 | skip_on_travis = pytest.mark.skipif(TRAVIS, 9 | reason="Can't run this test on Travis") 10 | 11 | 12 | @pytest.fixture(scope="module", 13 | params=[TCLab, 14 | TCLabModel]) 15 | def lab(request): 16 | if TRAVIS and request.param is TCLab: 17 | pytest.skip("Can't use real TCLab on Travis") 18 | a = request.param() 19 | yield a 20 | a.close() 21 | 22 | 23 | @skip_on_travis 24 | def test_constructor_port(): 25 | """Raise RuntimeError for an unrecognized port.""" 26 | print("TRAVIS" in os.environ) 27 | with pytest.raises(RuntimeError): 28 | with TCLab(port='nonsense') as a: 29 | pass 30 | 31 | 32 | @skip_on_travis 33 | def test_context(): 34 | with TCLab() as _: 35 | pass 36 | 37 | 38 | @skip_on_travis 39 | def test_already_connected(): 40 | with pytest.raises(AlreadyConnectedError): 41 | lab = TCLab() 42 | _ = TCLab() 43 | lab.close() 44 | 45 | 46 | @skip_on_travis 47 | def test_lock_release(): 48 | """Check that we release the lock""" 49 | with TCLab() as lab: 50 | _ = lab.T1 51 | lab = TCLab() 52 | lab.close() 53 | 54 | 55 | def test_T1(lab): 56 | assert type(lab.T1) == float 57 | assert -10 < lab.T1 < 110 58 | 59 | 60 | def test_T2(lab): 61 | assert type(lab.T2) == float 62 | assert -10 < lab.T2 < 110 63 | 64 | 65 | def test_P1(lab): 66 | assert lab.P1 == 200 67 | lab.P1 = -10 68 | assert lab.P1 == 0 69 | lab.P1 = 300 70 | assert lab.P1 == 255 71 | lab.P1 = 200 72 | assert lab.P1 == 200 73 | 74 | 75 | def test_P2(lab): 76 | assert lab.P2 == 100 77 | lab.P2 = -10 78 | assert lab.P2 == 0 79 | lab.P2 = 300 80 | assert lab.P2 == 255 81 | lab.P2 = 100 82 | assert lab.P2 == 100 83 | 84 | 85 | def test_LED(lab): 86 | assert lab.LED(50) == 50 87 | 88 | 89 | def settertests(method): 90 | assert method(-10) == 0 91 | assert method(50) == 50 92 | assert method(0.5) == 0.5 93 | assert method(120) == 100 94 | assert 0 <= method() <= 100 95 | 96 | 97 | def test_Q1(lab): 98 | settertests(lab.Q1) 99 | 100 | 101 | def test_Q2(lab): 102 | settertests(lab.Q2) 103 | 104 | 105 | def test_U1(lab): 106 | lab.U1 = -10 107 | assert lab.U1 == 0 108 | lab.U1 = 50 109 | assert lab.U1 == 50 110 | lab.U1 = 120 111 | assert lab.U1 == 100 112 | 113 | 114 | def test_U2(lab): 115 | lab.U2 = -10 116 | assert lab.U2 == 0 117 | lab.U2 = 50 118 | assert lab.U2 == 50 119 | lab.U2 = 120 120 | assert lab.U2 == 100 121 | 122 | 123 | def test_scan(lab): 124 | lab.Q1(10) 125 | lab.Q2(20) 126 | 127 | T1, T2, Q1, Q2 = lab.scan() 128 | 129 | assert 0 < T1 < 200 130 | assert 0 < T2 < 200 131 | assert Q1 == 10 132 | assert Q2 == 20 133 | 134 | 135 | def test_quantize(): 136 | lab = TCLabModel() 137 | assert lab.quantize(-100) > -100 138 | assert lab.quantize(300) < 300 139 | assert lab.quantize(20.0) != 20.0 140 | 141 | 142 | def test_measurement(): 143 | lab = TCLabModel() 144 | for n in range(100): 145 | assert abs(lab.measurement(n) - n) <= 1.0 146 | -------------------------------------------------------------------------------- /tests/test_labtime.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | 4 | from tclab import labtime, clock, setnow 5 | 6 | 7 | def test_import(): 8 | assert labtime.running 9 | 10 | 11 | def test_get_rate_on_import(): 12 | assert labtime.get_rate() == 1 13 | 14 | 15 | def test_time(): 16 | delay = 1 17 | labtime.set_rate(1) 18 | tic = labtime.time() 19 | time.sleep(delay) 20 | toc = labtime.time() 21 | assert abs(delay - (toc-tic)) < 0.05 22 | 23 | 24 | def test_set_rate(): 25 | labtime.set_rate(2) 26 | assert labtime.get_rate() == 2 27 | 28 | 29 | def test_set_rate_default(): 30 | labtime.set_rate() 31 | assert labtime.get_rate() == 1 32 | 33 | 34 | def test_set_rate_errors(): 35 | with pytest.raises(ValueError): 36 | labtime.set_rate(0) 37 | with pytest.raises(ValueError): 38 | labtime.set_rate(-1) 39 | 40 | 41 | def test_rate(): 42 | sf = 2 43 | delay = 1 44 | labtime.set_rate(sf) 45 | tic = labtime.time() 46 | time.sleep(delay) 47 | toc = labtime.time() 48 | assert abs(toc - tic - sf * delay) < 0.05 49 | 50 | 51 | def test_stop(): 52 | labtime.stop() 53 | tic = labtime.time() 54 | time.sleep(1) 55 | toc = labtime.time() 56 | assert tic == toc 57 | assert labtime.running is False 58 | 59 | 60 | def test_start(): 61 | delay = 1 62 | labtime.set_rate(1) 63 | labtime.stop() 64 | tic = labtime.time() 65 | labtime.start() 66 | time.sleep(delay) 67 | toc = labtime.time() 68 | assert abs(toc - tic - delay) < 0.1 69 | assert labtime.running is True 70 | 71 | 72 | def test_reset(): 73 | time.sleep(2) 74 | labtime.reset() 75 | assert labtime.time() < 0.01 76 | labtime.reset(10) 77 | assert labtime.time() < 10.01 78 | 79 | 80 | def test_sleep(): 81 | sf = 5 82 | sdelay = 10 83 | labtime.set_rate(sf) 84 | stic = labtime.time() 85 | tic = time.time() 86 | labtime.sleep(sdelay) 87 | stoc = labtime.time() 88 | toc = time.time() 89 | assert abs(stoc - stic - sdelay) < 0.1 90 | assert abs(toc - tic - sdelay/sf) < 0.1 91 | 92 | 93 | def test_sleep_exception(): 94 | with pytest.raises(RuntimeWarning): 95 | labtime.stop() 96 | labtime.sleep(1) 97 | labtime.start() 98 | 99 | 100 | def test_setnow(): 101 | time.sleep(2) 102 | setnow() 103 | assert labtime.time() < 0.01 104 | setnow(10) 105 | assert labtime.time() < 10.01 106 | 107 | 108 | @pytest.mark.parametrize("rate", [1, 2, 5]) 109 | def test_generator(rate): 110 | labtime.set_rate(rate) 111 | assert [round(_) for _ in clock(3)] == [0, 1, 2, 3] 112 | 113 | 114 | @pytest.mark.parametrize("rate", [1, 2, 5]) 115 | def test_tstep(rate): 116 | labtime.set_rate(rate) 117 | print([_ for _ in clock(4,2)]) 118 | assert [round(_) for _ in clock(4, 2)] == [0, 2, 4] 119 | 120 | 121 | @pytest.mark.parametrize("rate", [1, 2, 5]) 122 | def test_tolerance(rate): 123 | labtime.set_rate(rate) 124 | for _ in clock(1, tol=0.25): 125 | labtime.sleep(1.2) 126 | for t in clock(5, 1, tol=0.25): 127 | if 0.5 < t < 2.5: 128 | labtime.sleep(1.1) 129 | assert round(t) == 5.0 130 | 131 | 132 | @pytest.mark.parametrize("rate", [1, 2, 5]) 133 | def test_sync(rate): 134 | labtime.set_rate(rate) 135 | for t in clock(5, 1, tol=0.25): 136 | if 0.5 < t < 2.5: 137 | labtime.sleep(1.1) 138 | assert round(t) == 5.0 139 | -------------------------------------------------------------------------------- /experimental/VocCommChannel.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import queue 4 | import socket 5 | import sys 6 | import tornado 7 | 8 | from threading import Thread 9 | from tornado.ioloop import IOLoop 10 | from tornado import gen 11 | from tornado.websocket import websocket_connect 12 | 13 | 14 | voc_send_q = queue.Queue() 15 | voc_rcv_q = queue.Queue() 16 | 17 | class VocChannel(object): 18 | 19 | def __init__(self): 20 | token = os.getenv("VOC_COMM_TOKEN") 21 | host = self.my_ip() 22 | port = os.getenv("VOC_COMM_PORT") 23 | proxy = os.getenv("VOC_COMM_PROXY") 24 | self.url = "wss://" + proxy + "/hostip/" + host + ":" + str(port) + "/voccomm/" + token + "/client?vocmsgs=1" 25 | self.ws = None 26 | self.ioloop = IOLoop(make_current=True) 27 | self.connect() 28 | self.ioloop.start() 29 | 30 | def my_ip(self): 31 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 32 | s.connect(('8.8.8.8', 53)) 33 | return s.getsockname()[0] 34 | 35 | @gen.coroutine 36 | def connect(self): 37 | print("VOC: Connecting to Vocareum Communication Server...") 38 | try: 39 | self.ws = yield websocket_connect(self.url, on_message_callback=self.receiver) 40 | except Exception: 41 | print("VOC: Error: Could not connect") 42 | else: 43 | print("VOC: Connected") 44 | self.sender() 45 | 46 | def receiver(self, msg): 47 | # print("DBG RCVR: MAYBE Adding to Q: {}".format(msg)) 48 | if msg is None: 49 | print("VOC: ERROR: Vocareum Connection closed") 50 | try: 51 | js = json.loads(msg) 52 | if 'voc' in js: 53 | if 'severity' in js and js['severity'] == "ERROR": 54 | if 'msg' in js and js['msg'] is not None: 55 | print("VOC: ERROR: {}".format(js['msg'])) 56 | return 57 | except: # Exception as e: # json.decoder.JSONDecodeError 58 | # not vocareum json - should be a real message 59 | pass 60 | # print("DBG RCVR: Adding to Q: {}".format(msg)) 61 | voc_rcv_q.put(msg) 62 | 63 | @gen.coroutine 64 | def sender(self): 65 | while True: 66 | if self.ws is not None: 67 | # print("VOC DBG: Sender: Wait for CMD...") 68 | try: 69 | msg = voc_send_q.get(block=False) 70 | # print("VOC DBG: Sender: Got CMD: {}".format(msg)) 71 | except queue.Empty: 72 | yield gen.sleep(0.01) 73 | else: 74 | self.ws.write_message(msg) 75 | 76 | 77 | class VocCommChannel(object): 78 | 79 | channel = None 80 | 81 | def __init__(self, read_timeout=5): 82 | if VocCommChannel.channel is None: 83 | VocCommChannel.channel = Thread(target=VocChannel) 84 | # VocCommChannel.channel.setDaemon(True) 85 | VocCommChannel.channel.start() 86 | self.read_timeout = read_timeout 87 | 88 | def read(self, block=True, timeout=None): 89 | if timeout is None: 90 | timeout = self.read_timeout 91 | # print("VOC DBG: Going to READ... ") 92 | msg = voc_rcv_q.get(block=block, timeout=timeout) 93 | # print("VOC DBG: Just READ: " + msg) 94 | return msg 95 | 96 | def write(self, msg): 97 | # print("VOC DBG: Going to WRITE: " + msg, flush=True) 98 | voc_send_q.put(msg) 99 | # print("VOC DBG: Just WROTE: " + msg, flush=True) 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /tclab/experiment.py: -------------------------------------------------------------------------------- 1 | from .tclab import TCLab, TCLabModel 2 | from .historian import Historian, Plotter 3 | from .labtime import labtime, clock 4 | 5 | 6 | class Experiment: 7 | """Utility class for running experiments on the TCLab 8 | 9 | The class is designed to be used as a context. On initialisation 10 | it will automatically connect to the TCLab, create a Historian and Plotter 11 | and provide a `.clock` method. 12 | 13 | >>> with Experiment(connected=False, plot=False, time=20) as experiment: 14 | ... for t in experiment.clock(): 15 | ... experiment.lab.Q1(100 if t < 100 else 0) 16 | ... # doctest: +SKIP 17 | ... # doctest: +ELLIPSIS 18 | TCLab version ... 19 | Simulated TCLab 20 | TCLab Model disconnected successfully. 21 | 22 | Once the experiment has run, you can access `experiment.historian` to see 23 | the results from the simulation. 24 | 25 | """ 26 | 27 | def __init__(self, connected=True, plot=True, 28 | twindow=200, time=500, 29 | dbfile=':memory:', 30 | speedup=1, synced=True, tol=0.5): 31 | """Parameters: 32 | connected: If True, connect to a physical TCLab, if False, connecte to 33 | TCLabModel 34 | plot: Use a Plotter 35 | twindow: (only applicable if plotting) the twindow for the Plotter 36 | time: total time to run for (used for experiment.clock) 37 | dbfile: The dbfile to use for the Historian 38 | speedup: speedup factor to use if not connected. 39 | synced: Try to run at a fixed factor of real time. If this is False, run 40 | as fast as possible regardless of the value of speedup. 41 | tol: Clock tolerance (used for experiment.clock) 42 | """ 43 | if (speedup != 1 or not synced) and connected: 44 | raise ValueError('The real TCLab can only run real time.') 45 | 46 | self.connected = connected 47 | self.plot = plot 48 | self.twindow = twindow 49 | self.time = time 50 | self.dbfile = dbfile 51 | self.speedup = speedup 52 | self.synced = synced 53 | self.tol = tol 54 | if synced: 55 | labtime.set_rate(speedup) 56 | 57 | self.lab = None 58 | self.historian = None 59 | self.plotter = None 60 | 61 | def __enter__(self): 62 | if self.connected: 63 | self.lab = TCLab() 64 | else: 65 | self.lab = TCLabModel(synced=self.synced) 66 | self.historian = Historian(self.lab.sources, dbfile=self.dbfile) 67 | if self.plot: 68 | self.plotter = Plotter(self.historian, twindow=self.twindow) 69 | 70 | return self 71 | 72 | def __exit__(self, exc_type, exc_value, traceback): 73 | self.lab.close() 74 | self.historian.close() 75 | 76 | def clock(self): 77 | if self.synced: 78 | times = clock(self.time, tol=self.tol) 79 | else: 80 | times = range(self.time) 81 | for t in times: 82 | yield t 83 | if self.plot: 84 | self.plotter.update(t) 85 | else: 86 | self.historian.update(t) 87 | if not self.synced: 88 | self.lab.update(t) 89 | 90 | 91 | def runexperiment(function, *args, **kwargs): 92 | """Simple wrapper for Experiment which builds an experiment and calls a 93 | function in a timed for loop. 94 | 95 | The function will be passed the time and a TCLab instance at every tick of 96 | the clock. 97 | 98 | The remaining arguments are passed Experiment. 99 | """ 100 | with Experiment(*args, **kwargs) as experiment: 101 | for t in experiment.clock(): 102 | function(t, experiment.lab) 103 | return experiment 104 | -------------------------------------------------------------------------------- /tclab/labtime.py: -------------------------------------------------------------------------------- 1 | import time as time 2 | 3 | 4 | class Labtime(): 5 | def __init__(self): 6 | self._realtime = time.time() 7 | self._labtime = 0 8 | self._rate = 1 9 | self._running = True 10 | self.lastsleep = 0 11 | 12 | @property 13 | def running(self): 14 | """Returns variable indicating whether labtime is running.""" 15 | return self._running 16 | 17 | def time(self): 18 | """Return current labtime.""" 19 | if self.running: 20 | elapsed = time.time() - self._realtime 21 | return self._labtime + self._rate * elapsed 22 | else: 23 | return self._labtime 24 | 25 | def set_rate(self, rate=1): 26 | """Set the rate of labtime relative to real time.""" 27 | if rate <= 0: 28 | raise ValueError("Labtime rates must be positive.") 29 | self._labtime = self.time() 30 | self._realtime = time.time() 31 | self._rate = rate 32 | 33 | def get_rate(self): 34 | """Return the rate of labtime relative to real time.""" 35 | return self._rate 36 | 37 | def sleep(self, delay): 38 | """Sleep in labtime for a period delay.""" 39 | self.lastsleep = delay 40 | if self._running: 41 | time.sleep(delay / self._rate) 42 | else: 43 | raise RuntimeWarning("sleep is not valid when labtime is stopped.") 44 | 45 | def stop(self): 46 | """Stop labtime.""" 47 | self._labtime = self.time() 48 | self._realtime = time.time() 49 | self._running = False 50 | 51 | def start(self): 52 | """Restart labtime.""" 53 | self._realtime = time.time() 54 | self._running = True 55 | 56 | def reset(self, val=0): 57 | """Reset labtime to a specified value.""" 58 | self._labtime = val 59 | self._realtime = time.time() 60 | 61 | 62 | labtime = Labtime() 63 | 64 | 65 | # for backwards compatability 66 | def setnow(tnow=0): 67 | labtime.reset(tnow) 68 | 69 | 70 | def clock(period, step=1, tol=float('inf'), adaptive=True): 71 | """Generator providing time values in sync with real time clock. 72 | 73 | Args: 74 | period (float): Time interval for clock operation in seconds. 75 | step (float): Time step. 76 | tol (float): Maximum permissible deviation from real time. 77 | adaptive (Boolean): If true, and if the rate != 1, then the labtime 78 | rate is adjusted to maximize simulation speed. 79 | 80 | Yields: 81 | float: The next time step rounded to nearest 10th of a second. 82 | 83 | 84 | Note: 85 | * Passing `tol=float('inf')` will effectively disable sync error checking 86 | * When large values for `tol` are used, no guarantees are made that the 87 | last time returned will be equal to `period`. 88 | """ 89 | start = labtime.time() 90 | now = 0 91 | 92 | while round(now, 0) <= period: 93 | yield round(now, 2) 94 | if round(now) >= period: 95 | break 96 | elapsed = labtime.time() - (start + now) 97 | rate = labtime.get_rate() 98 | if (rate != 1) and adaptive: 99 | if elapsed > step: 100 | labtime.set_rate(0.8 * rate * step / elapsed) 101 | elif (elapsed < 0.5 * step) & (rate < 50): 102 | labtime.set_rate(1.25 * rate) 103 | else: 104 | if elapsed > step + tol: 105 | message = ('Labtime clock lost synchronization with real time. ' 106 | 'Step size was {} s, but {:.2f} s elapsed ' 107 | '({:.2f} too long). Consider increasing step.') 108 | raise RuntimeError(message.format(step, elapsed, elapsed-step)) 109 | labtime.sleep(step - (labtime.time() - start) % step) 110 | now = labtime.time() - start 111 | -------------------------------------------------------------------------------- /notebooks/04_Emulation_of_TCLab_for_Offline_Use.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# TCLab Emulation for Offline Use\n", 8 | "\n", 9 | "`TCLabModel` replaces `TCLab` for occasions where the TCLab hardware might not be available. To use, include the import\n", 10 | "\n", 11 | " from tclab import TCLabModel as TCLab \n", 12 | " \n", 13 | "The rest of your code will work without change. Be advised the underlying model used to approximate the behavior of the Temperature Control Laboratory is an approximation to the dynamics of the actual hardware." 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 1, 19 | "metadata": {}, 20 | "outputs": [ 21 | { 22 | "name": "stdout", 23 | "output_type": "stream", 24 | "text": [ 25 | "TCLab version 0.4.5dev\n", 26 | "Simulated TCLab\n", 27 | "Temperature 1: 20.95 °C\n", 28 | "Temperature 2: 20.95 °C\n", 29 | "TCLab Model disconnected successfully.\n" 30 | ] 31 | } 32 | ], 33 | "source": [ 34 | "from tclab import TCLabModel as TCLab\n", 35 | "\n", 36 | "with TCLab() as a:\n", 37 | " print(\"Temperature 1: {0:0.2f} °C\".format(a.T1))\n", 38 | " print(\"Temperature 2: {0:0.2f} °C\".format(a.T2))" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "## Choosing Real or Emulation mode with `setup()`\n", 46 | "\n", 47 | "The `tclab.setup()` function provides a choice of using actual hardware or an emulation of the TCLab device by changing a single line of code. When emulating TCLab, a second parameter `speedup` allows the emulation to run at a multiple of real time.\n", 48 | "\n", 49 | " # connect to TCLab mounted on arduino\n", 50 | " TCLab = tclab.setup(connected=True) \n", 51 | " \n", 52 | " # Emulate the operation of TCLab using TCLabModel\n", 53 | " TCLab = tclab.setup(connected=False)\n", 54 | " \n", 55 | " # Emulate operation at 5× realtime\n", 56 | " TCLab = tclab.setup(connected=False, speedup=5)\n", 57 | " \n", 58 | "The next cell demonsrates emulation of the TCLab device at 5× real time." 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 2, 64 | "metadata": {}, 65 | "outputs": [ 66 | { 67 | "name": "stdout", 68 | "output_type": "stream", 69 | "text": [ 70 | "TCLab version 0.4.5dev\n", 71 | "Simulated TCLab\n", 72 | "t = 0.0 Q1 = 100 % T1 = 20.95\n", 73 | "t = 1.0 Q1 = 100 % T1 = 20.95\n", 74 | "t = 2.0 Q1 = 100 % T1 = 20.95\n", 75 | "t = 3.0 Q1 = 100 % T1 = 20.95\n", 76 | "t = 4.0 Q1 = 100 % T1 = 20.95\n", 77 | "t = 5.0 Q1 = 100 % T1 = 21.27\n", 78 | "t = 6.1 Q1 = 100 % T1 = 21.27\n", 79 | "t = 7.0 Q1 = 100 % T1 = 21.27\n", 80 | "t = 8.1 Q1 = 100 % T1 = 21.59\n", 81 | "t = 9.0 Q1 = 100 % T1 = 21.59\n", 82 | "t = 10.1 Q1 = 0 % T1 = 21.92\n", 83 | "t = 11.2 Q1 = 0 % T1 = 21.92\n", 84 | "t = 12.2 Q1 = 0 % T1 = 22.24\n", 85 | "t = 13.2 Q1 = 0 % T1 = 22.24\n", 86 | "t = 14.0 Q1 = 0 % T1 = 22.56\n", 87 | "t = 15.2 Q1 = 0 % T1 = 22.88\n", 88 | "t = 16.2 Q1 = 0 % T1 = 22.88\n", 89 | "t = 17.2 Q1 = 0 % T1 = 22.88\n", 90 | "t = 18.2 Q1 = 0 % T1 = 23.21\n", 91 | "t = 19.0 Q1 = 0 % T1 = 23.21\n", 92 | "t = 20.1 Q1 = 0 % T1 = 23.21\n", 93 | "TCLab Model disconnected successfully.\n" 94 | ] 95 | } 96 | ], 97 | "source": [ 98 | "%matplotlib inline\n", 99 | "import tclab\n", 100 | "\n", 101 | "TCLab = tclab.setup(connected=False, speedup=5)\n", 102 | "\n", 103 | "with TCLab() as lab:\n", 104 | " for t in tclab.clock(20):\n", 105 | " lab.Q1(100 if t < 10 else 0)\n", 106 | " print(\"t = {0:4.1f} Q1 = {1:3.0f} % T1 = {2:5.2f}\".format(t, lab.Q1(), lab.T1))" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [] 122 | } 123 | ], 124 | "metadata": { 125 | "kernelspec": { 126 | "display_name": "Python 3", 127 | "language": "python", 128 | "name": "python3" 129 | }, 130 | "language_info": { 131 | "codemirror_mode": { 132 | "name": "ipython", 133 | "version": 3 134 | }, 135 | "file_extension": ".py", 136 | "mimetype": "text/x-python", 137 | "name": "python", 138 | "nbconvert_exporter": "python", 139 | "pygments_lexer": "ipython3", 140 | "version": "3.6.4" 141 | } 142 | }, 143 | "nbformat": 4, 144 | "nbformat_minor": 2 145 | } 146 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | TCLab: Temperature Control Laboratory 2 | ===================================== 3 | 4 | Master: 5 | 6 | .. image:: https://travis-ci.org/jckantor/TCLab.svg?branch=master 7 | :target: https://travis-ci.org/jckantor/TCLab 8 | 9 | .. image:: https://readthedocs.org/projects/tclab/badge/?version=latest 10 | :target: http://tclab.readthedocs.io/en/latest/?badge=latest 11 | 12 | .. image:: https://badge.fury.io/py/tclab.svg 13 | :target: https://badge.fury.io/py/tclab 14 | 15 | Development: 16 | 17 | .. image:: https://travis-ci.org/jckantor/TCLab.svg?branch=development 18 | :target: https://travis-ci.org/jckantor/TCLab 19 | 20 | ``TCLab`` provides a Python interface to the 21 | `Temperature Control Lab `_ 22 | implemented on an Arduino microcontroller over a USB interface. 23 | ``TCLab`` is implemented as a Python class within 24 | the ``tclab`` package. The ``tclab`` package also includes: 25 | 26 | * ``clock`` A Python generator for soft real-time implementation of 27 | process control algorithms. 28 | * ``Historian`` A Python class to log results of a process control 29 | experiment. 30 | * ``Plotter`` Provides an historian with real-time plotting within a 31 | Jupyter notebook. 32 | * ``TCLabModel`` An embedded model of the temperature control lab 33 | for off-line and faster-than-realtime simulation of process control 34 | experiments. No hardware needs to be attached to use ``TCLabModel``. 35 | 36 | The companion Arduino firmware for device operation is available at the 37 | `TCLab-Sketch repository `_. 38 | 39 | The `Arduino Temperature Control Lab `_ 40 | is a modular, portable, and inexpensive solution for hands-on process 41 | control learning. Heat output is adjusted by modulating current flow to 42 | each of two transistors. Thermistors measure the temperatures. Energy 43 | from the transistor output is transferred by conduction and convection 44 | to the temperature sensor. The dynamics of heat transfer provide rich 45 | opportunities to implement single and multivariable control systems. 46 | The lab is integrated into a small PCB shield which can be mounted to 47 | any `Arduino `_ or Arduino compatible 48 | microcontroller. 49 | 50 | Installation 51 | ------------ 52 | 53 | Install using :: 54 | 55 | pip install tclab 56 | 57 | To upgrade an existing installation, use the command :: 58 | 59 | pip install tclab --upgrade 60 | 61 | 62 | The development version contains new features, but may be less stable. To install the development version use the command :: 63 | 64 | pip install --upgrade https://github.com/jckantor/TCLab/archive/development.zip 65 | 66 | 67 | Hardware setup 68 | -------------- 69 | 70 | 1. Plug a compatible Arduino device (UNO, Leonardo, NHduino) with the 71 | lab attached into your computer via the USB connection. Plug the DC 72 | power adapter into the wall. 73 | 74 | 2. (optional) Install Arduino Drivers 75 | 76 | *If you are using Windows 10, the Arduino board should connect 77 | without additional drivers required.* 78 | 79 | For Arduino clones using the CH340G, CH34G or CH34X chipset you may need additional drivers. Only install these if you see a message saying "No Arduino device found." when connecting. 80 | 81 | * `macOS `__. 82 | * `Windows `__. 83 | 84 | 3. (optional) Install Arduino Firmware 85 | 86 | ``TCLab`` requires the one-time installation of custom firmware on 87 | an Arduino device. If it hasn't been pre-installed, the necessary 88 | firmware and instructions are available from the 89 | `TCLab-Sketch repository `_. 90 | 91 | Checking that everything works 92 | ------------------------------ 93 | 94 | Execute the following code :: 95 | 96 | import tclab 97 | with tclab.TCLab() as lab: 98 | print(lab.T1) 99 | 100 | If everything has worked, you should see the following output message :: 101 | 102 | Connecting to TCLab 103 | TCLab Firmware Version 1.2.1 on NHduino connected to port XXXX 104 | 21.54 105 | TCLab disconnected successfully. 106 | 107 | The number returned is the temperature of sensor T1 in °C. 108 | 109 | 110 | Troubleshooting 111 | --------------- 112 | 113 | If something went wrong in the above process, refer to our troubleshooting guide 114 | in TROUBLESHOOTING.md. 115 | 116 | Next Steps 117 | ---------- 118 | 119 | The notebook directory provides examples on how to use the TCLab module. 120 | The latest documentation is available at 121 | `Read the Docs `_. 122 | 123 | Course Websites 124 | --------------- 125 | 126 | Additional information, instructional videos, and Jupyter notebook 127 | examples are available at the following course websites. 128 | 129 | * `Arduino temperature control lab page `__ on the BYU Process Dynamics and Control course website. 130 | * `CBE 30338 `__ for the Notre Dame 131 | Chemical Process Control course website. 132 | * `Dynamics and Control `__ for notebooks developed at the University of Pretoria. 133 | -------------------------------------------------------------------------------- /conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # TCLab documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Feb 10 09:41:30 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | from recommonmark.parser import CommonMarkParser 25 | 26 | source_parsers = { 27 | '.md': CommonMarkParser, 28 | } 29 | 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = '1.0' 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 39 | # ones. 40 | extensions = [ 41 | 'nbsphinx', 42 | 'sphinx.ext.mathjax' 43 | ] 44 | 45 | nbsphinx_timeout = 1000 46 | nbsphinx_allow_errors = True 47 | nbsphinx_execute = 'never' 48 | 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ['_templates'] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = ['.rst', '.md'] 58 | 59 | # The master toctree document. 60 | master_doc = 'index' 61 | 62 | # General information about the project. 63 | project = 'TCLab' 64 | copyright = '2018, Jeffrey Kantor and Carl Sandrock' 65 | author = 'Jeffrey Kantor and Carl Sandrock' 66 | 67 | # The version info for the project you're documenting, acts as replacement for 68 | # |version| and |release|, also used in various other places throughout the 69 | # built documents. 70 | exec(open('tclab/version.py').read()) 71 | 72 | # The short X.Y version. 73 | version = '.'.join(__version__.split('.')[0:2]) 74 | 75 | # The full version, including alpha/beta/rc tags. 76 | release = __version__ 77 | 78 | # The language for content autogenerated by Sphinx. Refer to documentation 79 | # for a list of supported languages. 80 | # 81 | # This is also used if you do content translation via gettext catalogs. 82 | # Usually you set "language" from the command line for these cases. 83 | language = None 84 | 85 | # List of patterns, relative to source directory, that match files and 86 | # directories to ignore when looking for source files. 87 | # This patterns also effect to html_static_path and html_extra_path 88 | exclude_patterns = [ 89 | '_build', 90 | '**.ipynb_checkpoints', 91 | 'Thumbs.db', 92 | '.DS_Store' 93 | ] 94 | 95 | # The name of the Pygments (syntax highlighting) style to use. 96 | pygments_style = 'sphinx' 97 | 98 | # If true, `todo` and `todoList` produce output, else they produce nothing. 99 | todo_include_todos = False 100 | 101 | 102 | # -- Options for HTML output ---------------------------------------------- 103 | 104 | # The theme to use for HTML and HTML Help pages. See the documentation for 105 | # a list of builtin themes. 106 | # 107 | 108 | html_theme = 'sphinx_rtd_theme' 109 | html_theme_options = { 110 | 'collapse_navigation': False, 111 | } 112 | 113 | # Theme options are theme-specific and customize the look and feel of a theme 114 | # further. For a list of options available for each theme, see the 115 | # documentation. 116 | # 117 | # html_theme_options = {} 118 | 119 | # Add any paths that contain custom static files (such as style sheets) here, 120 | # relative to this directory. They are copied after the builtin static files, 121 | # so a file named "default.css" will overwrite the builtin "default.css". 122 | html_static_path = ['_static'] 123 | 124 | # Custom sidebar templates, must be a dictionary that maps document names 125 | # to template names. 126 | # 127 | # This is required for the alabaster theme 128 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 129 | html_sidebars = { 130 | '**': [ 131 | 'relations.html', # needs 'show_related': True theme option to display 132 | 'searchbox.html', 133 | ] 134 | } 135 | 136 | 137 | # -- Options for HTMLHelp output ------------------------------------------ 138 | 139 | # Output file base name for HTML help builder. 140 | htmlhelp_basename = 'TCLabdoc' 141 | 142 | 143 | # -- Options for LaTeX output --------------------------------------------- 144 | 145 | latex_elements = { 146 | # The paper size ('letterpaper' or 'a4paper'). 147 | # 148 | # 'papersize': 'letterpaper', 149 | 150 | # The font size ('10pt', '11pt' or '12pt'). 151 | # 152 | # 'pointsize': '10pt', 153 | 154 | # Additional stuff for the LaTeX preamble. 155 | # 156 | # 'preamble': '', 157 | 158 | # Latex figure (float) alignment 159 | # 160 | # 'figure_align': 'htbp', 161 | } 162 | 163 | # Grouping the document tree into LaTeX files. List of tuples 164 | # (source start file, target name, title, 165 | # author, documentclass [howto, manual, or own class]). 166 | latex_documents = [ 167 | (master_doc, 'TCLab.tex', 'TCLab Documentation', 168 | 'Jeffrey Kantor and Carl Sandrock', 'manual'), 169 | ] 170 | 171 | 172 | # -- Options for manual page output --------------------------------------- 173 | 174 | # One entry per manual page. List of tuples 175 | # (source start file, name, description, authors, manual section). 176 | man_pages = [ 177 | (master_doc, 'tclab', 'TCLab Documentation', 178 | [author], 1) 179 | ] 180 | 181 | 182 | # -- Options for Texinfo output ------------------------------------------- 183 | 184 | # Grouping the document tree into Texinfo files. List of tuples 185 | # (source start file, target name, title, author, 186 | # dir menu entry, description, category) 187 | texinfo_documents = [ 188 | (master_doc, 'TCLab', 'TCLab Documentation', 189 | author, 'TCLab', 'One line description of project.', 190 | 'Miscellaneous'), 191 | ] 192 | -------------------------------------------------------------------------------- /notebooks/01_TCLab_Overview.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# TCLab Overview\n", 8 | "\n", 9 | "The `tclab` package provides a set of Python tools for interfacing with the [BYU Temperature Control Laboratory](http://apmonitor.com/pdc/index.php/Main/ArduinoTemperatureControl). The Temperature Control Laboratory consists of two heaters and two temperature sensors mounted on an Arduino microcontroller board. Together, the `tclab` package and the Temperature Control Laboratory provide a low-cost experimental platform for implementing algorithms commonly used for process control.\n", 10 | "\n", 11 | "![](images/tclab_device.png) " 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## TCLab Architecture\n", 19 | "\n", 20 | "The `tclab` package is intended to be used as a teaching tool. The package provides high-level access to sensors, heaters, a pseudo-realtime clock. The package includes the following Python classes and functions:\n", 21 | "\n", 22 | "* `TCLab()` providing access to the Temperature Control Laboratory hardware.\n", 23 | "* `TCLabModel()` providing access to a simulation of the Temperature Control Laboratory hardware. \n", 24 | "* `clock` for synchronizing with a real time clock.\n", 25 | "* `Historian` for data logging.\n", 26 | "* `Plotter` for realtime plotting.\n", 27 | "\n", 28 | "![](images/TCLabOverview.png)\n", 29 | "\n", 30 | "Using these Python tools, students can create Jupyter notebooks and python codes covering a wide range of topics in process control.\n", 31 | "\n", 32 | "* **tclab.py:** A Python package providing high-level access to sensors, heaters, a pseudo-realtime clock. The package includes `TCLab()` providing access to the device, `clock` for synchronizing with a real time clock, `Historian` for data logging, and `Plotter` for realtime plotting.\n", 33 | "\n", 34 | "* **TCLab-sketch.ino:** Firmware for the intrisically safe operation of the Arduino board and shield. The sketch is available at [https://github.com/jckantor/TCLab-sketch](https://github.com/jckantor/TCLab-sketch).\n", 35 | "\n", 36 | "* **Arduino:** Hardware platform for the Temperature Control Laboratory. TCLab is compatiable with Arduino Uno, Arduino Leonardo, and compatible clones." 37 | ] 38 | }, 39 | { 40 | "cell_type": "markdown", 41 | "metadata": {}, 42 | "source": [ 43 | "## Getting Started\n", 44 | "\n", 45 | "### Installation\n", 46 | "\n", 47 | "Install using\n", 48 | "\n", 49 | " pip install tclab\n", 50 | " \n", 51 | "To upgrade an existing installation, use the command\n", 52 | "\n", 53 | " pip install tclab --upgrade\n", 54 | "\n", 55 | "The development version contains new features, but may be less stable. To install the development version use the command\n", 56 | "\n", 57 | " pip install --upgrade https://github.com/jckantor/TCLab/archive/development.zip\n", 58 | "\n", 59 | "### Hardware Setup\n", 60 | "\n", 61 | "1. Plug a compatible Arduino device (UNO, Leonardo, NHduino) with the\n", 62 | " lab attached into your computer via the USB connection. Plug the DC\n", 63 | " power adapter into the wall.\n", 64 | "\n", 65 | "2. (optional) Install Arduino Drivers.\n", 66 | "\n", 67 | " *If you are using Windows 10, the Arduino board should connect without additional drivers required.*\n", 68 | "\n", 69 | " Mac OS X users may need to install a serial driver. For Arduino clones using the CH340G, CH34G or CH34X chipset, a suitable driver can be found [here](https://github.com/MPParsley/ch340g-ch34g-ch34x-mac-os-x-driver) or [here](https://github.com/adrianmihalko/ch340g-ch34g-ch34x-mac-os-x-driver).\n", 70 | "\n", 71 | "3. (optional) Install Arduino Firmware;\n", 72 | "\n", 73 | " `TCLab` requires the one-time installation of custom firmware on an Arduino device. If it hasn't been pre-installed, the necessary firmware and instructions are available from the [TCLab-Sketch repository](https://github.com/jckantor/TCLab-sketch).\n", 74 | "\n", 75 | "### Checking that everything works\n", 76 | "\n", 77 | "Execute the following code\n", 78 | "\n", 79 | " import tclab\n", 80 | " with tclab.TCLab() as lab:\n", 81 | " print(lab.T1)\n", 82 | "\n", 83 | "If everything has worked, you should see the following output message\n", 84 | "\n", 85 | " Connecting to TCLab\n", 86 | " TCLab Firmware Version 1.2.1 on NHduino connected to port XXXX\n", 87 | " 21.54\n", 88 | " TCLab disconnected successfully. \n", 89 | "\n", 90 | "The number returned is the temperature of sensor T1 in °C." 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "## Next Steps\n", 98 | "\n", 99 | "The notebook directory provides examples on how to use the TCLab module.\n", 100 | "The latest documentation is available at\n", 101 | "[Read the Docs](http://tclab.readthedocs.io/en/latest/index.html).\n", 102 | "\n", 103 | "### Course Web Sites\n", 104 | "\n", 105 | "More information, instructional videos, and Jupyter notebook\n", 106 | "examples are available at the following course websites.\n", 107 | "\n", 108 | "* [Arduino temperature control lab page](http://apmonitor.com/pdc/index.php/Main/ArduinoTemperatureControl) on the BYU Process Dynamics and Control course website.\n", 109 | "* [CBE 30338](http://jckantor.github.io/CBE30338/) for the Notre Dame\n", 110 | " Chemical Process Control course website.\n", 111 | "* [Dynamics and Control](https://github.com/alchemyst/Dynamics-and-Control) for notebooks developed at the University of Pretoria.\n" 112 | ] 113 | } 114 | ], 115 | "metadata": { 116 | "kernelspec": { 117 | "display_name": "Python 3", 118 | "language": "python", 119 | "name": "python3" 120 | }, 121 | "language_info": { 122 | "codemirror_mode": { 123 | "name": "ipython", 124 | "version": 3 125 | }, 126 | "file_extension": ".py", 127 | "mimetype": "text/x-python", 128 | "name": "python", 129 | "nbconvert_exporter": "python", 130 | "pygments_lexer": "ipython3", 131 | "version": "3.6.4" 132 | } 133 | }, 134 | "nbformat": 4, 135 | "nbformat_minor": 2 136 | } 137 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | # To use a consistent encoding 4 | from codecs import open 5 | from os import path 6 | 7 | here = path.abspath(path.dirname(__file__)) 8 | 9 | exec(open("tclab/version.py").read()) 10 | 11 | # Get the long description from the README file 12 | with open(path.join(here, 'README.rst'), encoding='utf-8') as f: 13 | long_description = f.read() 14 | 15 | # Arguments marked as "Required" below must be included for upload to PyPI. 16 | # Fields marked as "Optional" may be commented out. 17 | 18 | setup( 19 | # This is the name of your project. The first time you publish this 20 | # package, this name will be registered for you. It will determine how 21 | # users can install this project, e.g.: 22 | # 23 | # $ pip install sampleproject 24 | # 25 | # And where it will live on PyPI: https://pypi.org/project/sampleproject/ 26 | # 27 | # There are some restrictions on what makes a valid project name 28 | # specification here: 29 | # https://packaging.python.org/specifications/core-metadata/#name 30 | name='tclab', # Required 31 | 32 | # Versions should comply with PEP 440: 33 | # https://www.python.org/dev/peps/pep-0440/ 34 | # 35 | # For a discussion on single-sourcing the version across setup.py and the 36 | # project code, see 37 | # https://packaging.python.org/en/latest/single_source_version.html 38 | version=__version__, # Required 39 | 40 | # This is a one-line description or tagline of what your project does. This 41 | # corresponds to the "Summary" metadata field: 42 | # https://packaging.python.org/specifications/core-metadata/#summary 43 | description='Python bindings for the BYU Arduino Temperature Control Lab', # Required 44 | 45 | # This is an optional longer description of your project that represents 46 | # the body of text which users will see when they visit PyPI. 47 | # 48 | # Often, this is the same as your README, so you can just read it in from 49 | # that file directly (as we have already done above) 50 | # 51 | # This field corresponds to the "Description" metadata field: 52 | # https://packaging.python.org/specifications/core-metadata/#description-optional 53 | long_description=long_description, # Optional 54 | 55 | # This should be a valid link to your project's main homepage. 56 | # 57 | # This field corresponds to the "Home-Page" metadata field: 58 | # https://packaging.python.org/specifications/core-metadata/#home-page-optional 59 | url='https://github.com/jckantor/TCLab', # Optional 60 | 61 | # This should be your name or the name of the organization which owns the 62 | # project. 63 | author='BYUPRISM', # Optional 64 | 65 | # This should be a valid email address corresponding to the author listed 66 | # above. 67 | author_email='john_hedengren@byu.edu', # Optional 68 | 69 | license='Apache Software License', 70 | 71 | # Classifiers help users find your project by categorizing it. 72 | # 73 | # For a list of valid classifiers, see 74 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers 75 | classifiers=[ # Optional 76 | # How mature is this project? Common values are 77 | # 3 - Alpha 78 | # 4 - Beta 79 | # 5 - Production/Stable 80 | 'Development Status :: 3 - Alpha', 81 | 82 | # Indicate who your project is intended for 83 | 'Intended Audience :: Education', 84 | 'Topic :: System :: Hardware :: Hardware Drivers', 85 | 86 | # Pick your license as you wish 87 | 'License :: OSI Approved :: Apache Software License', 88 | 89 | # Specify the Python versions you support here. In particular, ensure 90 | # that you indicate whether you support Python 2, Python 3 or both. 91 | 'Programming Language :: Python :: 2', 92 | 'Programming Language :: Python :: 2.7', 93 | 'Programming Language :: Python :: 3', 94 | 'Programming Language :: Python :: 3.4', 95 | 'Programming Language :: Python :: 3.5', 96 | 'Programming Language :: Python :: 3.6', 97 | ], 98 | 99 | # This field adds keywords for your project which will appear on the 100 | # project page. What does your project relate to? 101 | # 102 | # Note that this is a string of words separated by whitespace, not a list. 103 | keywords='apmonitor control hardware', # Optional 104 | 105 | # You can just specify package directories manually here if your project is 106 | # simple. Or you can use find_packages(). 107 | # 108 | # Alternatively, if you just want to distribute a single Python file, use 109 | # the `py_modules` argument instead as follows, which will expect a file 110 | # called `my_module.py` to exist: 111 | # 112 | # py_modules=["my_module"], 113 | # 114 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Required 115 | 116 | # This field lists other packages that your project depends on to run. 117 | # Any package you put here will be installed by pip when your project is 118 | # installed, so they must be valid existing projects. 119 | # 120 | # For an analysis of "install_requires" vs pip's requirements files see: 121 | # https://packaging.python.org/en/latest/requirements.html 122 | install_requires=['pyserial'], # Optional 123 | 124 | # List additional groups of dependencies here (e.g. development 125 | # dependencies). Users will be able to install these using the "extras" 126 | # syntax, for example: 127 | # 128 | # $ pip install sampleproject[dev] 129 | # 130 | # Similar to `install_requires` above, these must be valid existing 131 | # projects. 132 | extras_require={ # Optional 133 | 'dev': ['check-manifest'], 134 | 'test': ['coverage', 'pytest'], 135 | }, 136 | 137 | # If there are data files included in your packages that need to be 138 | # installed, specify them here. 139 | # 140 | # If using Python 2.6 or earlier, then these have to be included in 141 | # MANIFEST.in as well. 142 | # package_data={ # Optional 143 | # 'sample': ['package_data.dat'], 144 | # }, 145 | 146 | # Although 'package_data' is the preferred approach, in some case you may 147 | # need to place data files outside of your packages. See: 148 | # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files 149 | # 150 | # In this case, 'data_file' will be installed into '/my_data' 151 | # data_files=[('my_data', ['data/data_file'])], # Optional 152 | 153 | # To provide executable scripts, use entry points in preference to the 154 | # "scripts" keyword. Entry points provide cross-platform support and allow 155 | # `pip` to create the appropriate form of executable for the target 156 | # platform. 157 | # 158 | # For example, the following would provide a command called `sample` which 159 | # executes the function `main` from this package when invoked: 160 | # entry_points={ # Optional 161 | # 'console_scripts': [ 162 | # 'sample=sample:main', 163 | # ], 164 | # }, 165 | ) 166 | -------------------------------------------------------------------------------- /tclab/gui.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import datetime 4 | import tornado 5 | 6 | from .tclab import TCLab, TCLabModel 7 | from .historian import Historian, Plotter 8 | from .labtime import labtime, clock 9 | 10 | from ipywidgets import Button, Label, FloatSlider, HBox, VBox, Checkbox,\ 11 | IntText 12 | 13 | 14 | def actionbutton(description, action, disabled=True): 15 | """Return button widget with specified label and callback action.""" 16 | button = Button(description=description, disabled=disabled) 17 | button.on_click(action) 18 | 19 | return button 20 | 21 | 22 | def labelledvalue(label, value, units=''): 23 | """Return widget and HBox for label, value, units display.""" 24 | labelwidget = Label(value=label) 25 | valuewidget = Label(value=str(value)) 26 | unitwidget = Label(value=units) 27 | box = HBox([labelwidget, valuewidget, unitwidget]) 28 | 29 | return valuewidget, box 30 | 31 | 32 | def slider(label, action=None, minvalue=0, maxvalue=100, disabled=True): 33 | """Return slider widget for specified label and action callback.""" 34 | sliderwidget = FloatSlider(description=label, min=minvalue, max=maxvalue) 35 | sliderwidget.disabled = disabled 36 | if action: 37 | sliderwidget.observe(action, names='value') 38 | 39 | return sliderwidget 40 | 41 | 42 | class NotebookInteraction(): 43 | """Base class for Notebook UI interaction controllers. 44 | 45 | You should inherit from this class to build new interactions. 46 | """ 47 | def __init__(self): 48 | self.lab = None 49 | self.ui = None 50 | 51 | def update(self, t): 52 | """Called on a timer to update the interaction. 53 | 54 | t is the current simulation time. """ 55 | raise NotImplementedError 56 | 57 | def connect(self, lab): 58 | """This is called when the interface connects to a lab 59 | 60 | lab is an instance of TCLab or TCLabModel 61 | """ 62 | self.lab = lab 63 | self.lab.connected = True 64 | 65 | def start(self): 66 | """Called when the Start button is pressed""" 67 | raise NotImplementedError 68 | 69 | def stop(self): 70 | """Called when the Stop button is pressed""" 71 | raise NotImplementedError 72 | 73 | def disconnect(self): 74 | """Called when the interface disconnects from a lab""" 75 | self.lab.connected = False 76 | 77 | 78 | class SimpleInteraction(NotebookInteraction): 79 | """Simple interaction with the TCLab 80 | 81 | Provides a ui with two sliders for the heaters and text boxes showing 82 | the temperatures. 83 | 84 | Notice that the class must define a "layout" property suitable to pass as 85 | thelayout argument to Plotter. It must also define a "sources" property 86 | suitable to pass to Historian. This is typically only possible after 87 | connecting, so in this class we define sources in .connect() 88 | 89 | """ 90 | def __init__(self): 91 | super().__init__() 92 | 93 | self.layout = (('Q1', 'Q2'), 94 | ('T1', 'T2')) 95 | 96 | # Sliders for heaters 97 | self.Q1widget = slider('Q1', self.action_Q1) 98 | self.Q2widget = slider('Q2', self.action_Q2) 99 | 100 | heaters = VBox([self.Q1widget, self.Q2widget]) 101 | 102 | # Temperature display 103 | self.T1widget, T1box = labelledvalue('T1:', 0, '°C') 104 | self.T2widget, T2box = labelledvalue('T2:', 0, '°C') 105 | 106 | temperatures = VBox([T1box, T2box]) 107 | 108 | self.ui = HBox([heaters, temperatures]) 109 | 110 | def update(self, t): 111 | self.T1widget.value = '{:2.1f}'.format(self.lab.T1) 112 | self.T2widget.value = '{:2.1f}'.format(self.lab.T2) 113 | 114 | def connect(self, lab): 115 | super().connect(lab) 116 | self.sources = self.lab.sources 117 | 118 | def start(self): 119 | self.Q1widget.disabled = False 120 | self.Q2widget.disabled = False 121 | 122 | def stop(self): 123 | self.Q1widget.disabled = True 124 | self.Q2widget.disabled = True 125 | 126 | def action_Q1(self, change): 127 | """Change heater 1 power.""" 128 | self.lab.Q1(change['new']) 129 | 130 | def action_Q2(self, change): 131 | """Change heater 2 power.""" 132 | self.lab.Q2(change['new']) 133 | 134 | 135 | class NotebookUI: 136 | def __init__(self, Controller=SimpleInteraction): 137 | self.timer = tornado.ioloop.PeriodicCallback(self.update, 1000) 138 | self.lab = None 139 | self.plotter = None 140 | self.historian = None 141 | self.seconds = 0 142 | self.firstsession = True 143 | 144 | # Model or real 145 | self.usemodel = Checkbox(value=False, description='Use model') 146 | self.usemodel.observe(self.togglemodel, names='value') 147 | self.speedup = slider('Speedup', minvalue=1, maxvalue=10) 148 | modelbox = HBox([self.usemodel, self.speedup]) 149 | 150 | # Buttons 151 | self.connect = actionbutton('Connect', self.action_connect, False) 152 | self.start = actionbutton('Start', self.action_start) 153 | self.stop = actionbutton('Stop', self.action_stop) 154 | self.disconnect = actionbutton('Disconnect', self.action_disconnect) 155 | 156 | buttons = HBox([self.connect, self.start, self.stop, self.disconnect]) 157 | 158 | # status 159 | self.timewidget, timebox = labelledvalue('Timestamp:', 'No data') 160 | self.sessionwidget, sessionbox = labelledvalue('Session:', 'No data') 161 | statusbox = HBox([timebox, sessionbox]) 162 | 163 | self.controller = Controller() 164 | 165 | self.gui = VBox([HBox([modelbox, buttons]), 166 | statusbox, 167 | self.controller.ui, 168 | ]) 169 | 170 | def update(self): 171 | """Update GUI display.""" 172 | self.timer.callback_time = 1000/self.speedup.value 173 | labtime.set_rate(self.speedup.value) 174 | 175 | self.timewidget.value = '{:.2f}'.format(labtime.time()) 176 | self.controller.update(labtime.time()) 177 | self.plotter.update(labtime.time()) 178 | 179 | def togglemodel(self, change): 180 | """Speedup can only be enabled when working with the model""" 181 | self.speedup.disabled = not change['new'] 182 | self.speedup.value = 1 183 | 184 | def action_start(self, widget): 185 | """Start TCLab operation.""" 186 | if not self.firstsession: 187 | self.historian.new_session() 188 | self.firstsession = False 189 | self.sessionwidget.value = str(self.historian.session) 190 | 191 | self.start.disabled = True 192 | self.stop.disabled = False 193 | self.disconnect.disabled = True 194 | 195 | self.controller.start() 196 | self.timer.start() 197 | labtime.reset() 198 | labtime.start() 199 | 200 | def action_stop(self, widget): 201 | """Stop TCLab operation.""" 202 | self.timer.stop() 203 | labtime.stop() 204 | 205 | self.start.disabled = False 206 | self.stop.disabled = True 207 | self.disconnect.disabled = False 208 | self.controller.stop() 209 | 210 | def action_connect(self, widget): 211 | """Connect to TCLab.""" 212 | if self.usemodel.value: 213 | self.lab = TCLabModel() 214 | else: 215 | self.lab = TCLab() 216 | labtime.stop() 217 | labtime.reset() 218 | 219 | self.controller.connect(self.lab) 220 | self.historian = Historian(self.controller.sources) 221 | self.plotter = Plotter(self.historian, 222 | twindow=500, 223 | layout=self.controller.layout) 224 | 225 | self.usemodel.disabled = True 226 | self.connect.disabled = True 227 | self.start.disabled = False 228 | self.disconnect.disabled = False 229 | 230 | def action_disconnect(self, widget): 231 | """Disconnect TCLab.""" 232 | self.lab.close() 233 | 234 | self.controller.disconnect() 235 | 236 | self.usemodel.disabled = False 237 | self.connect.disabled = False 238 | self.disconnect.disabled = True 239 | self.start.disabled = True 240 | -------------------------------------------------------------------------------- /notebooks/03_Synchronizing_with_Real_Time.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "slideshow": { 7 | "slide_type": "skip" 8 | } 9 | }, 10 | "source": [ 11 | "# Synchronizing with Real Time" 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": { 17 | "slideshow": { 18 | "slide_type": "skip" 19 | } 20 | }, 21 | "source": [ 22 | "## Simple use of `tclab.clock()`\n", 23 | "\n", 24 | "The tclab module includes a function `clock` for synchronizing calculations with real time. `clock(period)` is an iterator that generates a sequence of equally spaced time steps from zero to `period` separated by one second intervals. For each step `clock` returns time since start rounded to the nearest 10th of a second." 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 1, 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "name": "stdout", 34 | "output_type": "stream", 35 | "text": [ 36 | "0 sec.\n", 37 | "1.0 sec.\n", 38 | "2.0 sec.\n", 39 | "3.0 sec.\n", 40 | "4.0 sec.\n" 41 | ] 42 | } 43 | ], 44 | "source": [ 45 | "import tclab\n", 46 | "\n", 47 | "period = 5\n", 48 | "for t in tclab.clock(period):\n", 49 | " print(t, \"sec.\")" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "metadata": {}, 55 | "source": [ 56 | "`tclab.clock()` is implemented as a Python generator. A consequence of this implementation is that `tclab.clock()` is 'blocking' which limits its use for creating interactive demonstrations. See later sections of this user's guide for non-blocking alternatives that can be used for interactive demonstrations or GUI's." 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "## Optional Parameters\n", 64 | "\n", 65 | "### `step`: Clock time step\n", 66 | "\n", 67 | "An optional parameter `step` specifies a time step different from one second." 68 | ] 69 | }, 70 | { 71 | "cell_type": "code", 72 | "execution_count": 2, 73 | "metadata": {}, 74 | "outputs": [ 75 | { 76 | "name": "stdout", 77 | "output_type": "stream", 78 | "text": [ 79 | "0 sec.\n", 80 | "2.5 sec.\n" 81 | ] 82 | } 83 | ], 84 | "source": [ 85 | "import tclab\n", 86 | "\n", 87 | "period = 5\n", 88 | "step = 2.5\n", 89 | "for t in tclab.clock(period, step):\n", 90 | " print(t, \"sec.\")" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "### `tol`: clock tolerance\n", 98 | "\n", 99 | "There are some considerations when using `clock`. First, by its nature Python is not a real-time environment. `clock` makes a best effort to stay in sync with the wall clock but there can be no guarantees. The default behavior of `clock` is to maintain long-term synchronization with the real time clock.\n", 100 | "\n", 101 | "The `tol` argument specifies the allowable error on time steps. By default it is 0.5 seconds.\n", 102 | "\n", 103 | "The following cell demonstrates the effect of an intermittent calculation that exceeds the time step specified by `step`. In this instance, a `sleep` timeout of 2 seconds occurs at t=2. The default behaviour is to raise an error when desynchronisation occurs. " 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": 3, 109 | "metadata": {}, 110 | "outputs": [ 111 | { 112 | "name": "stdout", 113 | "output_type": "stream", 114 | "text": [ 115 | "0 sec.\n", 116 | "1.0 sec.\n", 117 | "2.0 sec.\n" 118 | ] 119 | }, 120 | { 121 | "ename": "RuntimeError", 122 | "evalue": "Labtime clock lost synchronization with real time. Step size was 1 s, but 2.01 s elapsed (1.01 too long). Consider increasing step.", 123 | "output_type": "error", 124 | "traceback": [ 125 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 126 | "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", 127 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mstep\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0;32mfor\u001b[0m \u001b[0mt\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mtclab\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclock\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mperiod\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"sec.\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0;36m1.9\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0mt\u001b[0m \u001b[0;34m<\u001b[0m \u001b[0;36m2.5\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 128 | "\u001b[0;32m~/Documents/Development/TCLab/tclab/labtime.py\u001b[0m in \u001b[0;36mclock\u001b[0;34m(period, step, tol, adaptive)\u001b[0m\n\u001b[1;32m 102\u001b[0m \u001b[0;34m'Step size was {} s, but {:.2f} s elapsed '\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 103\u001b[0m '({:.2f} too long). Consider increasing step.')\n\u001b[0;32m--> 104\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmessage\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstep\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0melapsed\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0melapsed\u001b[0m\u001b[0;34m-\u001b[0m\u001b[0mstep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 105\u001b[0m \u001b[0mlabtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstep\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0;34m(\u001b[0m\u001b[0mlabtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mstart\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0mstep\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 106\u001b[0m \u001b[0mnow\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlabtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtime\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m-\u001b[0m \u001b[0mstart\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 129 | "\u001b[0;31mRuntimeError\u001b[0m: Labtime clock lost synchronization with real time. Step size was 1 s, but 2.01 s elapsed (1.01 too long). Consider increasing step." 130 | ] 131 | } 132 | ], 133 | "source": [ 134 | "import tclab\n", 135 | "import time\n", 136 | "\n", 137 | "period = 5\n", 138 | "step = 1\n", 139 | "\n", 140 | "for t in tclab.clock(period, step):\n", 141 | " print(t, \"sec.\")\n", 142 | " if 1.9 < t < 2.5:\n", 143 | " time.sleep(2)" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "We can avoid the error above by specifying a larger value of `step` as advised, or we can specify a larger value for `tol`. Note that now time steps are skipped." 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 4, 156 | "metadata": {}, 157 | "outputs": [ 158 | { 159 | "name": "stdout", 160 | "output_type": "stream", 161 | "text": [ 162 | "0 sec.\n", 163 | "1.0 sec.\n", 164 | "2.0 sec.\n", 165 | "5.0 sec.\n" 166 | ] 167 | } 168 | ], 169 | "source": [ 170 | "for t in tclab.clock(period, step, tol=2):\n", 171 | " print(t, \"sec.\")\n", 172 | " if 1.9 < t < 2.5:\n", 173 | " time.sleep(2)" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "## Using `tclab.clock()` with TCLab\n", 181 | "\n", 182 | "An important use of the `tclab.clock()` generator is to implement and test control and estimation algorithms. The following cell shows how the `clock` generator can be used within the context defined by the Python `with` statement." 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": 4, 188 | "metadata": {}, 189 | "outputs": [ 190 | { 191 | "name": "stdout", 192 | "output_type": "stream", 193 | "text": [ 194 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 195 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 196 | "\n", 197 | "Set Heater 1 to 100.000000 %\n", 198 | "Set Heater 2 to 100.000000 %\n", 199 | "\n", 200 | " 0.0 sec: T1 = 30.9 °C T2 = 33.1 °C\n", 201 | " 2.0 sec: T1 = 30.9 °C T2 = 33.5 °C\n", 202 | " 4.0 sec: T1 = 30.9 °C T2 = 33.1 °C\n", 203 | " 6.0 sec: T1 = 30.6 °C T2 = 32.2 °C\n", 204 | " 8.0 sec: T1 = 30.9 °C T2 = 33.1 °C\n", 205 | " 10.0 sec: T1 = 30.9 °C T2 = 33.1 °C\n", 206 | " 12.0 sec: T1 = 30.9 °C T2 = 33.5 °C\n", 207 | " 14.0 sec: T1 = 30.9 °C T2 = 33.8 °C\n", 208 | " 16.0 sec: T1 = 31.2 °C T2 = 32.8 °C\n", 209 | " 18.0 sec: T1 = 31.2 °C T2 = 34.4 °C\n", 210 | " 20.0 sec: T1 = 31.5 °C T2 = 34.8 °C\n", 211 | "TCLab disconnected successfully.\n" 212 | ] 213 | } 214 | ], 215 | "source": [ 216 | "import tclab\n", 217 | "\n", 218 | "period = 20\n", 219 | "step = 2\n", 220 | "\n", 221 | "with tclab.TCLab() as lab:\n", 222 | " lab.Q1(100)\n", 223 | " lab.Q2(100)\n", 224 | " \n", 225 | " print(\"\\nSet Heater 1 to {0:f} %\".format(lab.Q1()))\n", 226 | " print(\"Set Heater 2 to {0:f} %\\n\".format(lab.Q2()))\n", 227 | "\n", 228 | " sfmt = \" {0:5.1f} sec: T1 = {1:0.1f} °C T2 = {2:0.1f} °C\"\n", 229 | " \n", 230 | " for t in tclab.clock(period, step):\n", 231 | " print(sfmt.format(t, lab.T1, lab.T2), flush=True)" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [] 240 | } 241 | ], 242 | "metadata": { 243 | "kernelspec": { 244 | "display_name": "Python 3", 245 | "language": "python", 246 | "name": "python3" 247 | }, 248 | "language_info": { 249 | "codemirror_mode": { 250 | "name": "ipython", 251 | "version": 3 252 | }, 253 | "file_extension": ".py", 254 | "mimetype": "text/x-python", 255 | "name": "python", 256 | "nbconvert_exporter": "python", 257 | "pygments_lexer": "ipython3", 258 | "version": "3.6.5" 259 | } 260 | }, 261 | "nbformat": 4, 262 | "nbformat_minor": 2 263 | } 264 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tclab/historian.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | from __future__ import division 6 | import bisect 7 | import sqlite3 8 | from .labtime import labtime 9 | import time 10 | 11 | 12 | class TagDB: 13 | """Interface to sqlite database containing tag values""" 14 | def __init__(self, filename=":memory:"): 15 | """Create or connect to a database 16 | 17 | :param filename: The filename of the database. 18 | By default, values are stored in memory.""" 19 | self.db = sqlite3.connect(filename) 20 | self.cursor = self.db.cursor() 21 | creates = ["""CREATE TABLE IF NOT EXISTS tagvalues ( 22 | session_id REFERENCES session (id), 23 | timeseconds, name, value)""", 24 | """CREATE TABLE IF NOT EXISTS sessions ( 25 | id INTEGER PRIMARY KEY, 26 | starttime)"""] 27 | for statement in creates: 28 | self.cursor.execute(statement) 29 | self.db.commit() 30 | self.session = None 31 | 32 | def new_session(self): 33 | self.cursor.execute("""INSERT INTO SESSIONS (starttime) 34 | VALUES (datetime('now'))""") 35 | self.session = self.cursor.lastrowid 36 | self.db.commit() 37 | 38 | def get_sessions(self): 39 | query = """SELECT id, starttime, COUNT(DISTINCT timeseconds) 40 | FROM sessions LEFT JOIN tagvalues ON sessions.id=session_id 41 | GROUP BY id ORDER BY starttime""" 42 | return list(self.cursor.execute(query)) 43 | 44 | def delete_session(self, session_id): 45 | queries = ['DELETE FROM sessions WHERE id = ?', 46 | 'DELETE FROM tagvalues WHERE session_id = ?'] 47 | for query in queries: 48 | self.cursor.execute(query, (session_id,)) 49 | self.db.commit() 50 | 51 | def record(self, timeseconds, name, value): 52 | if self.session is None: 53 | self.new_session() 54 | self.cursor.execute("INSERT INTO tagvalues VALUES (?, ?, ?, ?)", 55 | (self.session, timeseconds, name, value)) 56 | self.db.commit() 57 | 58 | def get(self, name, timeseconds=None, session=None): 59 | if session is None: 60 | session = self.session 61 | query = """SELECT timeseconds, value FROM tagvalues 62 | WHERE session_id=? AND name=?""" 63 | parameters = [session, name] 64 | if timeseconds is not None: 65 | query += " and timeseconds=?" 66 | parameters.append(timeseconds) 67 | query += " ORDER BY timeseconds" 68 | return list(self.cursor.execute(query, parameters)) 69 | 70 | def clean(self): 71 | """Delete sessions with no associated points""" 72 | query = """DELETE FROM sessions WHERE id NOT IN 73 | (SELECT DISTINCT session_id FROM tagvalues)""" 74 | self.cursor.execute(query) 75 | self.db.commit() 76 | 77 | def close(self): 78 | self.clean() 79 | self.db.close() 80 | 81 | 82 | class Historian(object): 83 | """Generalised logging class""" 84 | def __init__(self, sources, dbfile=":memory:"): 85 | """ 86 | sources: an iterable of (name, callable) tuples 87 | - name (str) is the name of a signal and the 88 | - callable is evaluated to obtain the value. 89 | 90 | Example: 91 | 92 | >>> a = 1 93 | >>> def getvalue(): 94 | ... return a 95 | >>> h = Historian([('a', getvalue)]) 96 | >>> h.update(0) 97 | >>> h.log 98 | [(0, 1)] 99 | 100 | 101 | Sometimes, multiple values are obtained from one function call. In such 102 | cases, names can still be specified as before, but callable can be 103 | passed as None for the subsequent names which come from a previous 104 | callable. 105 | 106 | Example: 107 | 108 | >>> a = 1 109 | >>> b = 2 110 | >>> def getvalues(): 111 | ... return [a, b] 112 | >>> h = Historian([('a', getvalues), 113 | ... ('b', None)]) 114 | >>> h.update(0) 115 | >>> h.log 116 | [(0, 1, 2)] 117 | 118 | """ 119 | self.sources = [('Time', lambda: self.tnow)] + list(sources) 120 | if dbfile: 121 | self.db = TagDB(dbfile) 122 | self.db.new_session() 123 | self.session = self.db.session 124 | else: 125 | self.db = None 126 | self.session = 1 127 | 128 | self.tstart = labtime.time() 129 | 130 | self.columns = [name for name, _ in self.sources] 131 | 132 | self.build_fields() 133 | 134 | def build_fields(self): 135 | self.fields = [[] for _ in self.columns] 136 | self.logdict = dict(zip(self.columns, self.fields)) 137 | self.t = self.logdict['Time'] 138 | 139 | def update(self, tnow=None): 140 | if tnow is None: 141 | self.tnow = labtime.time() - self.tstart 142 | else: 143 | self.tnow = tnow 144 | 145 | for name, valuefunction in self.sources: 146 | if valuefunction: 147 | v = valuefunction() 148 | try: 149 | values = iter(v) 150 | except TypeError: 151 | values = iter([v]) 152 | try: 153 | value = next(values) 154 | except StopIteration: 155 | raise ValueError("valuefunction did not return enough values") 156 | 157 | self.logdict[name].append(value) 158 | if self.db and name != "Time": 159 | self.db.record(self.tnow, name, value) 160 | 161 | @property 162 | def log(self): 163 | return list(zip(*[self.logdict[c] for c in self.columns])) 164 | 165 | def timeindex(self, t): 166 | return max(bisect.bisect(self.t, t) - 1, 0) 167 | 168 | def timeslice(self, tstart=0, tend=None, columns=None): 169 | start = self.timeindex(tstart) 170 | if tend is None: 171 | stop = len(self.t) + 1 172 | # Ensure that we always return at least one time's value 173 | if tend == tstart: 174 | stop = start + 1 175 | if columns is None: 176 | columns = self.columns 177 | return [self.logdict[c][start:stop] for c in columns] 178 | 179 | def at(self, t, columns=None): 180 | """ Return the values of columns at or just before a certain time""" 181 | return [c[0] for c in self.timeslice(t, t, columns)] 182 | 183 | def after(self, t, columns=None): 184 | """ Return the values of columns after or just before a certain time""" 185 | return self.timeslice(t, columns=columns) 186 | 187 | def _dbcheck(self): 188 | if self.db is None: 189 | raise NotImplementedError("Sessions not supported without dbfile") 190 | return True 191 | 192 | def new_session(self): 193 | self._dbcheck() 194 | self.db.new_session() 195 | self.session = self.db.session 196 | self.tstart = labtime.time() 197 | self.build_fields() 198 | 199 | def get_sessions(self): 200 | self._dbcheck() 201 | return self.db.get_sessions() 202 | 203 | def load_session(self, session): 204 | self._dbcheck() 205 | self.db.session = session 206 | self.build_fields() 207 | # FIXME: The way time is handled here is a bit brittle 208 | first = True 209 | for name in self.columns[1:]: 210 | for t, value in self.db.get(name): 211 | self.logdict[name].append(value) 212 | if first: 213 | self.t.append(t) 214 | first = False 215 | 216 | def close(self): 217 | if self.db: 218 | self.db.close() 219 | 220 | def to_csv(self, filename): 221 | """Output contents of log file to CSV""" 222 | import csv 223 | 224 | with open(filename, 'w') as f: 225 | writer = csv.writer(f) 226 | writer.writerow(self.columns) 227 | writer.writerows(self.log) 228 | 229 | 230 | class Plotter: 231 | def __init__(self, historian, twindow=120, layout=None): 232 | """Generalised graphical output of a Historian 233 | 234 | :param historian: An instance of the Historian class 235 | :param twindow: Amount of time to show in the plot 236 | :param layout: A tuple of tuples indicating how the fields should be 237 | plotted. For example (("T1", "T2"), ("Q1", "Q2")) indicates 238 | that there will be two subplots with the two indicated fields 239 | plotted on each. 240 | 241 | Note: A single plot is specified as (("T1",),) (note the commas) 242 | """ 243 | import matplotlib.pyplot as plt 244 | from matplotlib import get_backend 245 | self.backend = get_backend() 246 | self.historian = historian 247 | self.twindow = twindow 248 | self.last_plot_update = 0 249 | self.last_plotted_time = 0 250 | 251 | if layout is None: 252 | layout = tuple((field,) for field in historian.columns[1:]) 253 | self.layout = layout 254 | 255 | line_options = {'where': 'post', 'lw': 2, 'alpha': 0.8} 256 | self.lines = {} 257 | self.fig, self.axes = plt.subplots(len(layout), 1, figsize=(8, 1.5*len(layout)), 258 | dpi=80, 259 | sharex=True, 260 | gridspec_kw={'hspace': 0}, 261 | squeeze=False) 262 | self.axes = self.axes[:, 0] 263 | values = {c: 0 for c in historian.columns} 264 | plt.setp([a.get_xticklabels() for a in self.axes[:-1]], visible=False) 265 | for axis, fields in zip(self.axes, self.layout): 266 | for field in fields: 267 | y = values[field] 268 | self.lines[field] = axis.step(0, y, label=field, 269 | **line_options)[0] 270 | axis.set_xlim(0, self.twindow) 271 | axis.autoscale(axis='y', tight=False) 272 | axis.set_ylabel(', '.join(fields)) 273 | if len(fields) > 1: 274 | axis.legend() 275 | axis.grid() 276 | 277 | self.axes[-1].set_xlabel('Time / Seconds') 278 | plt.tight_layout() 279 | self.fig.canvas.draw() 280 | self.fig.show() 281 | if self.backend != 'nbAgg': 282 | from IPython import display 283 | self.display = display 284 | self.display.clear_output(wait=True) 285 | self.display.display(self.fig) 286 | 287 | def update(self, tnow=None): 288 | self.historian.update(tnow) 289 | 290 | minfps = 3 291 | maxskip = 50 292 | 293 | clocktime_since_refresh = time.time() - self.last_plot_update 294 | simtime_since_refresh = self.historian.tnow - self.last_plotted_time 295 | 296 | if clocktime_since_refresh <= 1/minfps and simtime_since_refresh < maxskip: 297 | return 298 | 299 | tmin = max(self.historian.tnow - self.twindow, 0) 300 | tmax = max(self.historian.tnow, self.twindow) 301 | for axis in self.axes: 302 | axis.set_xlim(tmin, tmax) 303 | data = self.historian.after(tmin) 304 | datadict = dict(zip(self.historian.columns, data)) 305 | t = datadict['Time'] 306 | for axis, fields in zip(self.axes, self.layout): 307 | for field in fields: 308 | y = datadict[field] 309 | self.lines[field].set_data(t, y) 310 | axis.relim() 311 | axis.autoscale_view() 312 | self.fig.canvas.draw() 313 | if self.backend != 'nbAgg': 314 | self.display.clear_output(wait=True) 315 | self.display.display(self.fig) 316 | 317 | self.last_plot_update = time.time() 318 | self.last_plotted_time = self.historian.tnow -------------------------------------------------------------------------------- /notebooks/09_Labtime_Class.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# The Labtime Class\n", 8 | "\n", 9 | "The `Labtime` class is a tool for speeding up the simulation of process control experiments. With this tool you can more quickly develop control algorithms through simulation, then apply the algorithms to the Temperature Control Lab device with minimal changes to your code.\n", 10 | "\n", 11 | "In most cases you do not need to directly invoke `Labtime`. For example, `setup` with the optional parameter `speedup` (described in the chapter _TCLab Simulation for Offline Use_) uses `Labtime` to adjust the operation of the `clock` iterator. This is sufficient for many applications." 12 | ] 13 | }, 14 | { 15 | "cell_type": "markdown", 16 | "metadata": {}, 17 | "source": [ 18 | "## Basic Usage\n", 19 | "\n", 20 | "### .time()\n", 21 | "\n", 22 | "`Labtime` provides a replacement for the `time.time()` function from the Python standard library. The basic usage is demonstrated in the following cell. Note that `import` brings in an instance of `Labtime`. `labtime.time()` returns the _lab time_ elapsed since first imported into the Python kernal. " 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "name": "stdout", 32 | "output_type": "stream", 33 | "text": [ 34 | "Time since first imported = 0.0 labtime seconds.\n", 35 | "Time since first imported = 2.01 labtime seconds.\n" 36 | ] 37 | } 38 | ], 39 | "source": [ 40 | "from tclab import labtime\n", 41 | "\n", 42 | "tic = labtime.time()\n", 43 | "labtime.sleep(2)\n", 44 | "toc = labtime.time()\n", 45 | "\n", 46 | "print(\"Time since first imported = \", round(tic, 2), \" labtime seconds.\")\n", 47 | "print(\"Time since first imported = \", round(toc, 2), \" labtime seconds.\")" 48 | ] 49 | }, 50 | { 51 | "cell_type": "markdown", 52 | "metadata": {}, 53 | "source": [ 54 | "By default, `labtime.time()` progresses at the same rate at real time as measured by the Python `time` package. The following cell demonstrates the default correspondence of labtime and real time." 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 2, 60 | "metadata": {}, 61 | "outputs": [ 62 | { 63 | "name": "stdout", 64 | "output_type": "stream", 65 | "text": [ 66 | "real time = 0.00 lab time = 0.00\n", 67 | "real time = 1.00 lab time = 1.00\n", 68 | "real time = 2.01 lab time = 2.01\n", 69 | "real time = 3.01 lab time = 3.01\n", 70 | "real time = 4.01 lab time = 4.01\n" 71 | ] 72 | } 73 | ], 74 | "source": [ 75 | "from tclab import labtime\n", 76 | "import time\n", 77 | "\n", 78 | "time_start = time.time()\n", 79 | "labtime_start = labtime.time()\n", 80 | "\n", 81 | "def do(n):\n", 82 | " for k in range(0,n):\n", 83 | " t_real = time.time() - time_start\n", 84 | " t_lab = labtime.time() - labtime_start\n", 85 | " print(\"real time = {0:4.2f} lab time = {1:4.2f}\".format(t_real, t_lab))\n", 86 | " time.sleep(1)\n", 87 | " \n", 88 | "do(5)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "metadata": {}, 94 | "source": [ 95 | "### .set_rate(rate) and .get_rate(rate)\n", 96 | "\n", 97 | "Lab time can proceed at a rate faster or slower than real time. The relative rate of lab time to real time is set with the `labtime.set_rate(rate)`. The default value is one. The current value of the rate is returned by the `get_rate()`." 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": 3, 103 | "metadata": {}, 104 | "outputs": [ 105 | { 106 | "name": "stdout", 107 | "output_type": "stream", 108 | "text": [ 109 | "Ratio of lab time to real time = 2\n", 110 | "real time = 0.00 lab time = 0.00\n", 111 | "real time = 1.00 lab time = 2.00\n", 112 | "real time = 2.01 lab time = 4.01\n", 113 | "real time = 3.01 lab time = 6.02\n", 114 | "real time = 4.01 lab time = 8.03\n", 115 | "\n", 116 | "Ratio of lab time to real time = 1\n", 117 | "real time = 5.02 lab time = 10.04\n", 118 | "real time = 6.02 lab time = 11.04\n", 119 | "real time = 7.02 lab time = 12.04\n", 120 | "real time = 8.03 lab time = 13.05\n", 121 | "real time = 9.03 lab time = 14.05\n" 122 | ] 123 | } 124 | ], 125 | "source": [ 126 | "from tclab import labtime\n", 127 | "import time\n", 128 | "\n", 129 | "time_start = time.time()\n", 130 | "labtime_start = labtime.time()\n", 131 | "\n", 132 | "labtime.set_rate(2)\n", 133 | "print(\"Ratio of lab time to real time = \", labtime.get_rate())\n", 134 | "\n", 135 | "do(5)\n", 136 | "\n", 137 | "labtime.set_rate()\n", 138 | "print(\"\\nRatio of lab time to real time = \", labtime.get_rate())\n", 139 | "\n", 140 | "do(5)" 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "metadata": {}, 146 | "source": [ 147 | "As demonstrated, conceptually you can thinkg of lab time as a piecewise linear function of real time with the following properties\n", 148 | "\n", 149 | "* monotonically increasing\n", 150 | "* continuous\n", 151 | "* shared by all functions using `labtime`." 152 | ] 153 | }, 154 | { 155 | "cell_type": "markdown", 156 | "metadata": {}, 157 | "source": [ 158 | "### .sleep(delay)\n", 159 | "\n", 160 | "The `labtime.sleep()` function suspends execution for a period `delay` in lab time units. This is used, for example, in the `clock` iterator to speed up execution of a control loop when used in simulation mode." 161 | ] 162 | }, 163 | { 164 | "cell_type": "markdown", 165 | "metadata": {}, 166 | "source": [ 167 | "## Advanced Usage\n", 168 | "\n", 169 | "An additional set of functions are available in `Labtime` to facilitate construction of GUI's, and for programmatically creating code to simulate the behavior of more complex control systems." 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "metadata": {}, 175 | "source": [ 176 | "### .reset(t)\n", 177 | "\n", 178 | "The `labtime.reset(t)` method resets lab time to `t` (default 0). The function `setnow(t)` provides an equivalent service, and is included to provide backward compatibility early versions of `tclab`. This function is typically used within a GUI for repeated testing and tuning of a control algorithm." 179 | ] 180 | }, 181 | { 182 | "cell_type": "code", 183 | "execution_count": 4, 184 | "metadata": {}, 185 | "outputs": [ 186 | { 187 | "name": "stdout", 188 | "output_type": "stream", 189 | "text": [ 190 | "Resetting lab time to zero.\n", 191 | "labtime = 3.2901763916015625e-05 \n", 192 | "\n", 193 | "Resetting lab time to ten.\n", 194 | "labtime = 10.000032901763916 \n", 195 | "\n" 196 | ] 197 | } 198 | ], 199 | "source": [ 200 | "from tclab import labtime\n", 201 | "\n", 202 | "print(\"Resetting lab time to zero.\")\n", 203 | "labtime.reset(0)\n", 204 | "print(\"labtime =\", labtime.time(),\"\\n\")\n", 205 | "\n", 206 | "print(\"Resetting lab time to ten.\")\n", 207 | "labtime.reset(10)\n", 208 | "print(\"labtime =\", labtime.time(),\"\\n\")" 209 | ] 210 | }, 211 | { 212 | "cell_type": "markdown", 213 | "metadata": {}, 214 | "source": [ 215 | "### .stop() / .start() /.running\n", 216 | "\n", 217 | "`labtime.stop()` freezes the labtime clock at its current value. A Runtime warning is generated if there is an attempt to sleep while the labtime is stopped.\n", 218 | "\n", 219 | "`labtime.start()` restarts the labtime clock following a stoppage.\n", 220 | "\n", 221 | "`labtime.running` is a Boolean value that is `True` if the labtime clock is running, otherwise it is `False`." 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 5, 227 | "metadata": {}, 228 | "outputs": [ 229 | { 230 | "name": "stdout", 231 | "output_type": "stream", 232 | "text": [ 233 | "Is labtime running? True\n", 234 | "labtime = 10.015635967254639 \n", 235 | "\n", 236 | "Now we'll stop the labtime.\n", 237 | "Is labtime running? False \n", 238 | "\n", 239 | "We'll pause for 2 seconds in real time.\n", 240 | "\n", 241 | "We'll restart labtime and pick up where we left off.\n", 242 | "labtime = 10.015910148620605\n" 243 | ] 244 | } 245 | ], 246 | "source": [ 247 | "from tclab import labtime\n", 248 | "import time\n", 249 | "\n", 250 | "print(\"Is labtime running?\", labtime.running)\n", 251 | "print(\"labtime =\", labtime.time(), \"\\n\")\n", 252 | "\n", 253 | "print(\"Now we'll stop the labtime.\")\n", 254 | "labtime.stop()\n", 255 | "print(\"Is labtime running?\", labtime.running, \"\\n\")\n", 256 | "\n", 257 | "print(\"We'll pause for 2 seconds in real time.\\n\")\n", 258 | "time.sleep(2)\n", 259 | "\n", 260 | "print(\"We'll restart labtime and pick up where we left off.\")\n", 261 | "labtime.start()\n", 262 | "print(\"labtime =\", labtime.time())" 263 | ] 264 | }, 265 | { 266 | "cell_type": "markdown", 267 | "metadata": {}, 268 | "source": [ 269 | "## Auxiliary Functions\n", 270 | "\n", 271 | "### clock(tperiod, tstep)\n", 272 | "\n", 273 | "The `clock` iterator was introduced in an earlier section on synchronizing `tclab` with real time. In fact, `clock` uses the `Labtime` class to cooridinate with real time, and to provide faster than real time operation in simulation mode. " 274 | ] 275 | }, 276 | { 277 | "cell_type": "code", 278 | "execution_count": 6, 279 | "metadata": {}, 280 | "outputs": [ 281 | { 282 | "name": "stdout", 283 | "output_type": "stream", 284 | "text": [ 285 | "\n", 286 | "Rate = 1\n", 287 | "real time = 0.0 lab time = 0.0\n", 288 | "real time = 1.0 lab time = 1.0\n", 289 | "real time = 2.0 lab time = 2.0\n", 290 | "real time = 3.0 lab time = 3.0\n", 291 | "real time = 4.0 lab time = 4.0\n", 292 | "real time = 5.0 lab time = 5.0\n", 293 | "\n", 294 | "Rate = 10\n", 295 | "real time = 5.0 lab time = 5.0\n", 296 | "real time = 5.1 lab time = 6.0\n", 297 | "real time = 5.2 lab time = 7.1\n", 298 | "real time = 5.3 lab time = 8.0\n", 299 | "real time = 5.4 lab time = 9.1\n", 300 | "real time = 5.5 lab time = 10.0\n" 301 | ] 302 | } 303 | ], 304 | "source": [ 305 | "from tclab import labtime, clock\n", 306 | "import time\n", 307 | "\n", 308 | "time_start = time.time()\n", 309 | "labtime_start = labtime.time()\n", 310 | "\n", 311 | "def do(n):\n", 312 | " print(\"\\nRate =\", labtime.get_rate())\n", 313 | " for t in clock(n):\n", 314 | " t_real = time.time() - time_start\n", 315 | " t_lab = labtime.time() - labtime_start\n", 316 | " print(\"real time = {0:4.1f} lab time = {1:4.1f}\".format(t_real, t_lab))\n", 317 | "\n", 318 | "labtime.set_rate(1)\n", 319 | "do(5)\n", 320 | "\n", 321 | "labtime.set_rate(10)\n", 322 | "do(5)" 323 | ] 324 | }, 325 | { 326 | "cell_type": "markdown", 327 | "metadata": {}, 328 | "source": [ 329 | "### setnow(t)\n", 330 | "\n", 331 | "`setnow(t)` performs the same function as `labtime.reset(t)`. This function appeared in an early version of `tclab`, and is included here for backwards compatibility." 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [] 340 | } 341 | ], 342 | "metadata": { 343 | "kernelspec": { 344 | "display_name": "Python 3", 345 | "language": "python", 346 | "name": "python3" 347 | }, 348 | "language_info": { 349 | "codemirror_mode": { 350 | "name": "ipython", 351 | "version": 3 352 | }, 353 | "file_extension": ".py", 354 | "mimetype": "text/x-python", 355 | "name": "python", 356 | "nbconvert_exporter": "python", 357 | "pygments_lexer": "ipython3", 358 | "version": "3.6.4" 359 | } 360 | }, 361 | "nbformat": 4, 362 | "nbformat_minor": 2 363 | } 364 | -------------------------------------------------------------------------------- /notebooks/02_Accessing_the_Temperature_Control_Laboratory.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "slideshow": { 7 | "slide_type": "skip" 8 | } 9 | }, 10 | "source": [ 11 | "# Accessing the Temperature Control Laboratory\n", 12 | "\n", 13 | "## Importing `tclab`\n", 14 | "\n", 15 | "Once installed the package can be imported into Python and an instance created with the Python statements\n", 16 | "\n", 17 | " import tclab\n", 18 | " lab = tclab.TCLab()\n", 19 | "\n", 20 | "`TCLab` provides access to temperature measurements, heaters, and LED on board the Temperature Control Laboratory. When called with no arguments, attempts to find a device connected to a serial port and returns a connection. An error is generated if no device is found. The connection should be closed with\n", 21 | "\n", 22 | " lab.close()\n", 23 | " \n", 24 | "when no longer in use. The following cell demonstrates this process, and uses the tclab `LED()` function to flash the LED on the Temperature Control Lab for a period of 10 seconds at a 100% brightness level. " 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 3, 30 | "metadata": {}, 31 | "outputs": [ 32 | { 33 | "name": "stdout", 34 | "output_type": "stream", 35 | "text": [ 36 | "Collecting tclab\n", 37 | " Downloading https://files.pythonhosted.org/packages/b5/5a/751cc39b1b90c8a8fcce83d8aa498345752effe8a22eead6e4420bacbaac/tclab-0.4.6-py2.py3-none-any.whl\n", 38 | "Collecting pyserial (from tclab)\n", 39 | "\u001b[?25l Downloading https://files.pythonhosted.org/packages/0d/e4/2a744dd9e3be04a0c0907414e2a01a7c88bb3915cbe3c8cc06e209f59c30/pyserial-3.4-py2.py3-none-any.whl (193kB)\n", 40 | "\u001b[K 100% |████████████████████████████████| 194kB 4.1MB/s ta 0:00:01\n", 41 | "\u001b[?25hInstalling collected packages: pyserial, tclab\n", 42 | "Successfully installed pyserial-3.4 tclab-0.4.6\n" 43 | ] 44 | } 45 | ], 46 | "source": [ 47 | "!pip install tclab" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": 4, 53 | "metadata": {}, 54 | "outputs": [ 55 | { 56 | "name": "stdout", 57 | "output_type": "stream", 58 | "text": [ 59 | "TCLab version 0.4.6\n", 60 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUART1 at 115200 baud.\n", 61 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 62 | "TCLab disconnected successfully.\n" 63 | ] 64 | } 65 | ], 66 | "source": [ 67 | "import tclab\n", 68 | "\n", 69 | "lab = tclab.TCLab()\n", 70 | "lab.LED(100)\n", 71 | "lab.close()" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "### A note on terminology\n", 79 | "\n", 80 | "`TCLab` is a *class*. We *instantiate* the class by calling `TCLab()`. What is returned is an *instance* of the `TCLab` class. So in the above code, we would say `lab` is an instance of `TCLab`." 81 | ] 82 | }, 83 | { 84 | "cell_type": "markdown", 85 | "metadata": {}, 86 | "source": [ 87 | "## Using TCLab with Python's `with` statement\n", 88 | "\n", 89 | "The Python `with` statement provides a simple means of setting up and closing a connection to the Temperature Control Laboratory. The `with` statement establishes a context where a `TCLab` instance is created, assigned to a variable, and automatically closed upon completion." 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 2, 95 | "metadata": { 96 | "slideshow": { 97 | "slide_type": "skip" 98 | } 99 | }, 100 | "outputs": [ 101 | { 102 | "name": "stdout", 103 | "output_type": "stream", 104 | "text": [ 105 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 106 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 107 | "TCLab disconnected successfully.\n" 108 | ] 109 | } 110 | ], 111 | "source": [ 112 | "import tclab\n", 113 | "\n", 114 | "with tclab.TCLab() as lab:\n", 115 | " lab.LED(100)" 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "metadata": {}, 121 | "source": [ 122 | "The `with` statement is likely to be the most common way to connect the Temperature Control Laboratory for most uses." 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "metadata": {}, 128 | "source": [ 129 | "## Reading Temperatures\n", 130 | "\n", 131 | "Once a `TCLab` instance is created and connected to a device, the temperature sensors on the temperature control lab can be acccessed with the attributes `.T1` and `.T2`. For example, given an instance `lab`, the temperatures are accessed as\n", 132 | "\n", 133 | " T1 = lab.T1\n", 134 | " T2 = a.T2\n", 135 | "\n", 136 | "Note that `lab.T1` and `lab.T2` are read-only properties. Any attempt to set them to a value will return a Python error." 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 3, 142 | "metadata": {}, 143 | "outputs": [ 144 | { 145 | "name": "stdout", 146 | "output_type": "stream", 147 | "text": [ 148 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 149 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 150 | "Temperature 1: 27.67 °C\n", 151 | "Temperature 2: 27.03 °C\n", 152 | "TCLab disconnected successfully.\n" 153 | ] 154 | } 155 | ], 156 | "source": [ 157 | "import tclab\n", 158 | "\n", 159 | "with tclab.TCLab() as lab:\n", 160 | " print(\"Temperature 1: {0:0.2f} °C\".format(lab.T1))\n", 161 | " print(\"Temperature 2: {0:0.2f} °C\".format(lab.T2))" 162 | ] 163 | }, 164 | { 165 | "cell_type": "markdown", 166 | "metadata": { 167 | "slideshow": { 168 | "slide_type": "skip" 169 | } 170 | }, 171 | "source": [ 172 | "## Setting Heaters\n", 173 | "\n", 174 | "The heaters are controlled by functions `.Q1()` and `.Q2()` of a `TCLab` instance. For example, both heaters can be set to 100% power with the functions\n", 175 | "\n", 176 | " lab.Q1(100)\n", 177 | " lab.Q2(100)\n", 178 | "\n", 179 | "The device firmware limits the heaters to a range of 0 to 100%. The current value of attributes may be accessed via\n", 180 | "\n", 181 | " Q1 = lab.Q1()\n", 182 | " Q2 = lab.Q2()\n", 183 | " \n", 184 | "Note that the retrieved values may be different due to the range-limiting enforced by the device firmware.\n", 185 | "\n", 186 | "Alternatively, the heaters can also be specified with the properties `.U1` and `.U2`. Thus setting\n", 187 | "\n", 188 | " lab.U1 = 100\n", 189 | " lab.U2 = 100\n", 190 | " \n", 191 | "would set both heaters to 100% power. The current value of the heaters can be accessed as\n", 192 | "\n", 193 | " print(\"Current setting of heater 1 is\", lab.U1, \"%\")\n", 194 | " print(\"Current setting of heater 2 is\", lab.U2, \"%\")\n", 195 | " \n", 196 | "The choice to use a function (i.e, `.Q1()` and `.Q2()`) or a property (i.e, `.U1` or `.U2`) to set and access heater settings is a matter of user preference." 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 4, 202 | "metadata": { 203 | "slideshow": { 204 | "slide_type": "skip" 205 | } 206 | }, 207 | "outputs": [ 208 | { 209 | "name": "stdout", 210 | "output_type": "stream", 211 | "text": [ 212 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 213 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 214 | "\n", 215 | "Starting Temperature 1: 27.67 °C\n", 216 | "Starting Temperature 2: 27.03 °C\n", 217 | "\n", 218 | "Set Heater 1: 100.0 %\n", 219 | "Set Heater 2: 100.0 %\n", 220 | "\n", 221 | "Heat for 20 seconds\n", 222 | "\n", 223 | "Turn Heaters Off\n", 224 | "\n", 225 | "Set Heater 1: 0.0 %\n", 226 | "Set Heater 2: 0.0 %\n", 227 | "\n", 228 | "Final Temperature 1: 28.96 °C\n", 229 | "Final Temperature 2: 29.29 °C\n", 230 | "TCLab disconnected successfully.\n" 231 | ] 232 | } 233 | ], 234 | "source": [ 235 | "import tclab\n", 236 | "import time\n", 237 | "\n", 238 | "with tclab.TCLab() as lab:\n", 239 | " print(\"\\nStarting Temperature 1: {0:0.2f} °C\".format(lab.T1),flush=True)\n", 240 | " print(\"Starting Temperature 2: {0:0.2f} °C\".format(lab.T2),flush=True)\n", 241 | "\n", 242 | " lab.Q1(100)\n", 243 | " lab.Q2(100)\n", 244 | " print(\"\\nSet Heater 1:\", lab.Q1(), \"%\",flush=True)\n", 245 | " print(\"Set Heater 2:\", lab.Q2(), \"%\",flush=True)\n", 246 | " \n", 247 | " t_heat = 20\n", 248 | " print(\"\\nHeat for\", t_heat, \"seconds\")\n", 249 | " time.sleep(t_heat)\n", 250 | "\n", 251 | " print(\"\\nTurn Heaters Off\")\n", 252 | " lab.Q1(0)\n", 253 | " lab.Q2(0)\n", 254 | " print(\"\\nSet Heater 1:\", lab.Q1(), \"%\",flush=True)\n", 255 | " print(\"Set Heater 2:\", lab.Q2(), \"%\",flush=True)\n", 256 | " \n", 257 | " print(\"\\nFinal Temperature 1: {0:0.2f} °C\".format(lab.T1))\n", 258 | " print(\"Final Temperature 2: {0:0.2f} °C\".format(lab.T2))" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "## Setting Maximum Heater Power\n", 266 | "\n", 267 | "The control inputs to the heaters power is normally set with functions `.Q1()` and `.Q2()` (or properties `.U1` and `.U2`) specifying a value in a range from 0 to 100% of maximum heater power. \n", 268 | "\n", 269 | "The values of maximum heater power are specified in firmware with values in the range from 0 to 255. The default values are 200 for heater 1 and 100 for heater 2. The maximum heater power can be retrieved and set by properties `P1` and `P2`. The following code, for example, sets both heaters to a maximum power of 100." 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 5, 275 | "metadata": {}, 276 | "outputs": [ 277 | { 278 | "name": "stdout", 279 | "output_type": "stream", 280 | "text": [ 281 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 282 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 283 | "Maximum power of heater 1 = 200.0\n", 284 | "Maximum power of heater 2 = 100.0\n", 285 | "Adjusting the maximum power of heater 1.\n", 286 | "Maximum power of heater 1 = 100.0\n", 287 | "Maximum power of heater 2 = 100.0\n", 288 | "TCLab disconnected successfully.\n" 289 | ] 290 | } 291 | ], 292 | "source": [ 293 | "import tclab\n", 294 | "\n", 295 | "with tclab.TCLab() as lab:\n", 296 | " print(\"Maximum power of heater 1 = \", lab.P1)\n", 297 | " print(\"Maximum power of heater 2 = \", lab.P2)\n", 298 | " \n", 299 | " print(\"Adjusting the maximum power of heater 1.\")\n", 300 | " lab.P1 = 100\n", 301 | " \n", 302 | " print(\"Maximum power of heater 1 = \", lab.P1)\n", 303 | " print(\"Maximum power of heater 2 = \", lab.P2)" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "metadata": {}, 309 | "source": [ 310 | "The actual power supplied to the heaters is a function of the power supply voltage applied to the Temperature Control Lab shield,\n", 311 | "\n", 312 | "The maximum power applied to the heaters is a product of the settings (`P1`,`P2`) and of the power supply used with the TCLab hardware. The TCLab hardware is normally used with a 5 watt USB power supply capable of supply up to 1 amp at 5 volts. \n", 313 | "\n", 314 | "The TCLab hardware actually draws more than 1 amp when both `P1` and `P2` are set to 255 and `Q1` and `Q2` are at 100%. This situation will overload the power supply and result in the power supply shutting down. Normally the power supply will reset itself after unplugging from the power mains.\n", 315 | "\n", 316 | "Experience with the device shows keeping the sum `P1` and `P2` to a value less than 300 will avoid problems with the 5 watt power supply. If you have access to larger power supplies, then you can adjust `P1` and `P2` accordingly to achieve a wider range of temperatures." 317 | ] 318 | }, 319 | { 320 | "cell_type": "markdown", 321 | "metadata": {}, 322 | "source": [ 323 | "## `tclab` Sampling Speed\n", 324 | "\n", 325 | "There are limits to how quickly the board can be sampled. The following examples show values for a particular type of board. You can run them to see how quick your board is." 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "### Temperature Sampling Speed" 333 | ] 334 | }, 335 | { 336 | "cell_type": "code", 337 | "execution_count": 6, 338 | "metadata": {}, 339 | "outputs": [ 340 | { 341 | "name": "stdout", 342 | "output_type": "stream", 343 | "text": [ 344 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 345 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 346 | "TCLab disconnected successfully.\n", 347 | "Reading temperature at 12.3 samples per second.\n" 348 | ] 349 | } 350 | ], 351 | "source": [ 352 | "import time\n", 353 | "import tclab\n", 354 | "\n", 355 | "TCLab = tclab.setup(connected=True)\n", 356 | "\n", 357 | "N = 100\n", 358 | "meas = []\n", 359 | "with TCLab() as lab:\n", 360 | " tic = time.time()\n", 361 | " for k in range(0,N):\n", 362 | " meas.append(lab.T1)\n", 363 | " toc = time.time()\n", 364 | "\n", 365 | "print('Reading temperature at', round(N/(toc-tic),1), 'samples per second.')" 366 | ] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "metadata": {}, 371 | "source": [ 372 | "### Heater Sampling Speed" 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": 7, 378 | "metadata": {}, 379 | "outputs": [ 380 | { 381 | "name": "stdout", 382 | "output_type": "stream", 383 | "text": [ 384 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 385 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 386 | "TCLab disconnected successfully.\n", 387 | "Setting heater at 8.2 samples per second.\n" 388 | ] 389 | } 390 | ], 391 | "source": [ 392 | "import time\n", 393 | "import tclab\n", 394 | "\n", 395 | "TCLab = tclab.setup(connected=True)\n", 396 | "\n", 397 | "N = 100\n", 398 | "with TCLab() as lab:\n", 399 | " tic = time.time()\n", 400 | " for k in range(0,N):\n", 401 | " lab.Q1(100)\n", 402 | " toc = time.time()\n", 403 | "\n", 404 | "print('Setting heater at', round(N/(toc-tic),1), 'samples per second.')" 405 | ] 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": null, 410 | "metadata": {}, 411 | "outputs": [], 412 | "source": [] 413 | } 414 | ], 415 | "metadata": { 416 | "kernelspec": { 417 | "display_name": "Python 3", 418 | "language": "python", 419 | "name": "python3" 420 | }, 421 | "language_info": { 422 | "codemirror_mode": { 423 | "name": "ipython", 424 | "version": 3 425 | }, 426 | "file_extension": ".py", 427 | "mimetype": "text/x-python", 428 | "name": "python", 429 | "nbconvert_exporter": "python", 430 | "pygments_lexer": "ipython3", 431 | "version": "3.6.8" 432 | } 433 | }, 434 | "nbformat": 4, 435 | "nbformat_minor": 2 436 | } 437 | -------------------------------------------------------------------------------- /tclab/tclab.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function 5 | import time 6 | import os 7 | import random 8 | import serial 9 | from serial.tools import list_ports 10 | from .labtime import labtime 11 | from .version import __version__ 12 | 13 | 14 | sep = ' ' # command/value separator in TCLab firmware 15 | 16 | arduinos = [('USB VID:PID=16D0:0613', 'Arduino Uno'), 17 | ('USB VID:PID=1A86:7523', 'NHduino'), 18 | ('USB VID:PID=2341:8036', 'Arduino Leonardo'), 19 | ('USB VID:PID=2A03', 'Arduino.org device'), 20 | ('USB VID:PID', 'unknown device'), 21 | ] 22 | 23 | _sketchurl = 'https://github.com/jckantor/TCLab-sketch' 24 | _connected = False 25 | 26 | 27 | def clip(val, lower=0, upper=100): 28 | """Limit value to be between lower and upper limits""" 29 | return max(lower, min(val, upper)) 30 | 31 | 32 | def command(name, argument, lower=0, upper=100): 33 | """Construct command to TCLab-sketch.""" 34 | return name + sep + str(clip(argument, lower, upper)) 35 | 36 | 37 | def find_arduino(port=''): 38 | """Locates Arduino and returns port and device.""" 39 | comports = [tuple for tuple in list_ports.comports() if port in tuple[0]] 40 | for port, desc, hwid in comports: 41 | for identifier, arduino in arduinos: 42 | if hwid.startswith(identifier): 43 | return port, arduino 44 | print('--- Serial Ports ---') 45 | for port, desc, hwid in list_ports.comports(): 46 | print(port, desc, hwid) 47 | return None, None 48 | 49 | 50 | class AlreadyConnectedError(Exception): 51 | pass 52 | 53 | 54 | class TCLab(object): 55 | def __init__(self, port='', debug=False): 56 | global _connected 57 | self.debug = debug 58 | print("TCLab version", __version__) 59 | self.port, self.arduino = find_arduino(port) 60 | if self.port is None: 61 | raise RuntimeError('No Arduino device found.') 62 | 63 | try: 64 | self.connect(baud=115200) 65 | except AlreadyConnectedError: 66 | raise 67 | except: 68 | try: 69 | _connected = False 70 | self.sp.close() 71 | self.connect(baud=9600) 72 | print('Could not connect at high speed, but succeeded at low speed.') 73 | print('This may be due to an old TCLab firmware.') 74 | print('New Arduino TCLab firmware available at:') 75 | print(_sketchurl) 76 | except: 77 | raise RuntimeError('Failed to Connect.') 78 | 79 | self.sp.readline().decode('UTF-8') 80 | self.version = self.send_and_receive('VER') 81 | if self.sp.isOpen(): 82 | print(self.arduino, 'connected on port', self.port, 83 | 'at', self.baud, 'baud.') 84 | print(self.version + '.') 85 | labtime.set_rate(1) 86 | labtime.start() 87 | self._P1 = 200.0 88 | self._P2 = 100.0 89 | self.Q2(0) 90 | self.sources = [('T1', self.scan), 91 | ('T2', None), 92 | ('Q1', None), 93 | ('Q2', None), 94 | ] 95 | 96 | def __enter__(self): 97 | return self 98 | 99 | def __exit__(self, exc_type, exc_value, traceback): 100 | self.close() 101 | return 102 | 103 | def connect(self, baud): 104 | """Establish a connection to the arduino 105 | 106 | baud: baud rate""" 107 | global _connected 108 | 109 | if _connected: 110 | raise AlreadyConnectedError('You already have an open connection') 111 | 112 | _connected = True 113 | 114 | self.sp = serial.Serial(port=self.port, baudrate=baud, timeout=2) 115 | time.sleep(2) 116 | self.Q1(0) # fails if not connected 117 | self.baud = baud 118 | 119 | def close(self): 120 | """Shut down TCLab device and close serial connection.""" 121 | global _connected 122 | 123 | self.Q1(0) 124 | self.Q2(0) 125 | self.send_and_receive('X') 126 | self.sp.close() 127 | _connected = False 128 | print('TCLab disconnected successfully.') 129 | return 130 | 131 | def send(self, msg): 132 | """Send a string message to the TCLab firmware.""" 133 | self.sp.write((msg + '\r\n').encode()) 134 | if self.debug: 135 | print('Sent: "' + msg + '"') 136 | self.sp.flush() 137 | 138 | def receive(self): 139 | """Return a string message received from the TCLab firmware.""" 140 | msg = self.sp.readline().decode('UTF-8').replace('\r\n', '') 141 | if self.debug: 142 | print('Return: "' + msg + '"') 143 | return msg 144 | 145 | def send_and_receive(self, msg, convert=str): 146 | """Send a string message and return the response""" 147 | self.send(msg) 148 | return convert(self.receive()) 149 | 150 | def LED(self, val=100): 151 | """Flash TCLab LED at a specified brightness for 10 seconds.""" 152 | return self.send_and_receive(command('LED', val), float) 153 | 154 | @property 155 | def T1(self): 156 | """Return a float denoting TCLab temperature T1 in degrees C.""" 157 | return self.send_and_receive('T1', float) 158 | 159 | @property 160 | def T2(self): 161 | """Return a float denoting TCLab temperature T2 in degrees C.""" 162 | return self.send_and_receive('T2', float) 163 | 164 | @property 165 | def P1(self): 166 | """Return a float denoting maximum power of heater 1 in pwm.""" 167 | return self._P1 168 | 169 | @P1.setter 170 | def P1(self, val): 171 | """Set maximum power of heater 1 in pwm, range 0 to 255.""" 172 | self._P1 = self.send_and_receive(command('P1', val, 0, 255), float) 173 | 174 | @property 175 | def P2(self): 176 | """Return a float denoting maximum power of heater 2 in pwm.""" 177 | return self._P2 178 | 179 | @P2.setter 180 | def P2(self, val): 181 | """Set maximum power of heater 2 in pwm, range 0 to 255.""" 182 | self._P2 = self.send_and_receive(command('P2', val, 0, 255), float) 183 | 184 | def Q1(self, val=None): 185 | """Get or set TCLab heater power Q1 186 | 187 | val: Value of heater power, range is limited to 0-100 188 | 189 | return clipped value.""" 190 | if val is None: 191 | msg = 'R1' 192 | else: 193 | msg = 'Q1' + sep + str(clip(val)) 194 | return self.send_and_receive(msg, float) 195 | 196 | def Q2(self, val=None): 197 | """Get or set TCLab heater power Q2 198 | 199 | val: Value of heater power, range is limited to 0-100 200 | 201 | return clipped value.""" 202 | if val is None: 203 | msg = 'R2' 204 | else: 205 | msg = 'Q2' + sep + str(clip(val)) 206 | return self.send_and_receive(msg, float) 207 | 208 | def scan(self): 209 | #self.send('SCAN') 210 | T1 = self.T1 # float(self.receive()) 211 | T2 = self.T2 # float(self.receive()) 212 | Q1 = self.Q1() # float(self.receive()) 213 | Q2 = self.Q2() # float(self.receive()) 214 | return T1, T2, Q1, Q2 215 | 216 | # Define properties for Q1 and Q2 217 | U1 = property(fget=Q1, fset=Q1, doc="Heater 1 value") 218 | U2 = property(fget=Q2, fset=Q2, doc="Heater 2 value") 219 | 220 | 221 | class TCLabModel(object): 222 | def __init__(self, port='', debug=False, synced=True): 223 | self.debug = debug 224 | self.synced = synced 225 | print("TCLab version", __version__) 226 | labtime.start() 227 | print('Simulated TCLab') 228 | self.Ta = 21 # ambient temperature 229 | self.tstart = labtime.time() # start time 230 | self.tlast = self.tstart # last update time 231 | self._P1 = 200.0 # max power heater 1 232 | self._P2 = 100.0 # max power heater 2 233 | self._Q1 = 0 # initial heater 1 234 | self._Q2 = 0 # initial heater 2 235 | self._T1 = self.Ta # temperature thermister 1 236 | self._T2 = self.Ta # temperature thermister 2 237 | self._H1 = self.Ta # temperature heater 1 238 | self._H2 = self.Ta # temperature heater 2 239 | self.maxstep = 0.2 # maximum time step for integration 240 | self.sources = [('T1', self.scan), 241 | ('T2', None), 242 | ('Q1', None), 243 | ('Q2', None), 244 | ] 245 | 246 | def __enter__(self): 247 | return self 248 | 249 | def __exit__(self, exc_type, exc_value, traceback): 250 | self.close() 251 | return 252 | 253 | def close(self): 254 | """Simulate shutting down TCLab device.""" 255 | self.Q1(0) 256 | self.Q2(0) 257 | print('TCLab Model disconnected successfully.') 258 | return 259 | 260 | def LED(self, val=100): 261 | """Simulate flashing TCLab LED 262 | 263 | val : specified brightness (default 100). """ 264 | self.update() 265 | return clip(val) 266 | 267 | @property 268 | def T1(self): 269 | """Return a float denoting TCLab temperature T1 in degrees C.""" 270 | self.update() 271 | return self.measurement(self._T1) 272 | 273 | @property 274 | def T2(self): 275 | """Return a float denoting TCLab temperature T2 in degrees C.""" 276 | self.update() 277 | return self.measurement(self._T2) 278 | 279 | @property 280 | def P1(self): 281 | """Return a float denoting maximum power of heater 1 in pwm.""" 282 | self.update() 283 | return self._P1 284 | 285 | @P1.setter 286 | def P1(self, val): 287 | """Set maximum power of heater 1 in pwm, range 0 to 255.""" 288 | self.update() 289 | self._P1 = clip(val, 0, 255) 290 | 291 | @property 292 | def P2(self): 293 | """Return a float denoting maximum power of heater 2 in pwm.""" 294 | self.update() 295 | return self._P2 296 | 297 | @P2.setter 298 | def P2(self, val): 299 | """Set maximum power of heater 2 in pwm, range 0 to 255.""" 300 | self.update() 301 | self._P2 = clip(val, 0, 255) 302 | 303 | def Q1(self, val=None): 304 | """Get or set TCLabModel heater power Q1 305 | 306 | val: Value of heater power, range is limited to 0-100 307 | 308 | return clipped value.""" 309 | self.update() 310 | if val is not None: 311 | self._Q1 = clip(val) 312 | return self._Q1 313 | 314 | def Q2(self, val=None): 315 | """Get or set TCLabModel heater power Q2 316 | 317 | val: Value of heater power, range is limited to 0-100 318 | 319 | return clipped value.""" 320 | self.update() 321 | if val is not None: 322 | self._Q2 = clip(val) 323 | return self._Q2 324 | 325 | def scan(self): 326 | self.update() 327 | return (self.measurement(self._T1), 328 | self.measurement(self._T2), 329 | self._Q1, 330 | self._Q2) 331 | 332 | # Define properties for Q1 and Q2 333 | U1 = property(fget=Q1, fset=Q1, doc="Heater 1 value") 334 | U2 = property(fget=Q2, fset=Q2, doc="Heater 2 value") 335 | 336 | def quantize(self, T): 337 | """Quantize model temperatures to mimic Arduino A/D conversion.""" 338 | return max(-50, min(132.2, T - T % 0.3223)) 339 | 340 | def measurement(self, T): 341 | return self.quantize(T + random.normalvariate(0, 0.043)) 342 | 343 | def update(self, t=None): 344 | if t is None: 345 | if self.synced: 346 | self.tnow = labtime.time() - self.tstart 347 | else: 348 | return 349 | else: 350 | self.tnow = t 351 | 352 | teuler = self.tlast 353 | 354 | while teuler < self.tnow: 355 | dt = min(self.maxstep, self.tnow - teuler) 356 | DeltaTaH1 = self.Ta - self._H1 357 | DeltaTaH2 = self.Ta - self._H2 358 | DeltaT12 = self._H1 - self._H2 359 | dH1 = self._P1 * self._Q1 / 5720 + DeltaTaH1 / 20 - DeltaT12 / 100 360 | dH2 = self._P2 * self._Q2 / 5720 + DeltaTaH2 / 20 + DeltaT12 / 100 361 | dT1 = (self._H1 - self._T1)/140 362 | dT2 = (self._H2 - self._T2)/140 363 | 364 | self._H1 += dt * dH1 365 | self._H2 += dt * dH2 366 | self._T1 += dt * dT1 367 | self._T2 += dt * dT2 368 | teuler += dt 369 | 370 | self.tlast = self.tnow 371 | 372 | 373 | def diagnose(port=''): 374 | def countdown(t=10): 375 | for i in reversed(range(t)): 376 | print('\r' + "Countdown: {0:d} ".format(i), end='', flush=True) 377 | time.sleep(1) 378 | print() 379 | 380 | def heading(string): 381 | print() 382 | print(string) 383 | print('-'*len(string)) 384 | 385 | heading('Checking connection') 386 | 387 | if port: 388 | print('Looking for Arduino on {} ...'.format(port)) 389 | else: 390 | print('Looking for Arduino on any port...') 391 | comport, name = find_arduino(port=port) 392 | 393 | if comport is None: 394 | print('No known Arduino was found in the ports listed above.') 395 | return 396 | 397 | print(name, 'found on port', comport) 398 | 399 | heading('Testing TCLab object in debug mode') 400 | 401 | with TCLab(port=port, debug=True) as lab: 402 | print('Reading temperature') 403 | print(lab.T1) 404 | 405 | heading('Testing TCLab functions') 406 | 407 | with TCLab(port=port) as lab: 408 | print('Testing LED. Should turn on for 10 seconds.') 409 | lab.LED(100) 410 | countdown() 411 | 412 | print() 413 | print('Reading temperatures') 414 | T1 = lab.T1 415 | T2 = lab.T2 416 | print('T1 = {} °C, T2 = {} °C'.format(T1, T2)) 417 | 418 | print() 419 | print('Writing fractional value to heaters...') 420 | try: 421 | Q1 = lab.Q1(0.5) 422 | except: 423 | Q1 = -1.0 424 | print("We wrote Q1 = 0.5, and read back Q1 =", Q1) 425 | 426 | if Q1 != 0.5: 427 | print("Your TCLab firmware version ({}) doesn't support" 428 | "fractional heater values.".format(lab.version)) 429 | print("You need to upgrade to at least version 1.4.0 for this:") 430 | print(_sketchurl) 431 | 432 | print() 433 | print('We will now turn on the heaters, wait 30 seconds ' 434 | 'and see if the temperatures have gone up. ') 435 | lab.Q1(100) 436 | lab.Q2(100) 437 | countdown(30) 438 | 439 | print() 440 | def tempcheck(name, T_initial, T_final): 441 | print('{} started a {} °C and went to {} °C' 442 | .format(name, T_initial, T_final)) 443 | if T_final - T_initial < 0.8: 444 | print('The temperature went up less than expected.') 445 | print('Check the heater power supply.') 446 | 447 | T1_final = lab.T1 448 | T2_final = lab.T2 449 | 450 | tempcheck('T1', T1, T1_final) 451 | tempcheck('T2', T2, T2_final) 452 | 453 | print() 454 | heading("Throughput check") 455 | print("This part checks how fast your unit is") 456 | print("We will read T1 as fast as possible") 457 | 458 | start = time.time() 459 | n = 0 460 | while time.time() - start < 10: 461 | elapsed = time.time() - start + 0.0001 # avoid divide by zero 462 | T1 = lab.T1 463 | n += 1 464 | print('\rTime elapsed: {:3.2f} s.' 465 | ' Number of reads: {}.' 466 | ' Sampling rate: {:2.2f} Hz'.format(elapsed, n, n/elapsed), 467 | end='') 468 | 469 | print() 470 | 471 | print() 472 | print('Diagnostics complete') 473 | -------------------------------------------------------------------------------- /experimental/10_Interactive_and_Nonblocking_Operation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Interactive and Non-blocking Operation\n", 8 | "\n", 9 | "The following sections in this notebook demonstrate methods for interacting with TCLab, for building non-blocking implementations of a control loop, and for various experiments and tests with the package." 10 | ] 11 | }, 12 | { 13 | "cell_type": "markdown", 14 | "metadata": {}, 15 | "source": [ 16 | "## Experiments in Non-blocking Operation with `threading` Library\n", 17 | "\n", 18 | "The current implementation of " 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": 1, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "def bar():\n", 28 | " clock.send(None)\n", 29 | "\n", 30 | "def clock(tperiod):\n", 31 | " tstart = time.time()\n", 32 | " tfinish = tstart + tperiod\n", 33 | " t = 0\n", 34 | " while t + tstart < tfinish:\n", 35 | " z = yield t\n", 36 | " t += 1\n", 37 | "\n", 38 | "def bar():\n", 39 | " clock.send(2)" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "import threading, time\n", 49 | "import datetime\n", 50 | "\n", 51 | "next_call = time.time()\n", 52 | "k = 0\n", 53 | "\n", 54 | "def foo():\n", 55 | " global next_call, k\n", 56 | " if k < 5:\n", 57 | " print(k, datetime.datetime.now())\n", 58 | " next_call = next_call+1\n", 59 | " threading.Timer( next_call - time.time(), foo ).start()\n", 60 | " k += 1\n", 61 | " else:\n", 62 | " print(k, \"Last Call\")\n", 63 | "\n", 64 | "foo()" 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 3, 70 | "metadata": {}, 71 | "outputs": [ 72 | { 73 | "data": { 74 | "text/plain": [ 75 | "" 76 | ] 77 | }, 78 | "metadata": {}, 79 | "output_type": "display_data" 80 | }, 81 | { 82 | "ename": "SyntaxError", 83 | "evalue": "name 'tnext' is parameter and global (, line 18)", 84 | "output_type": "error", 85 | "traceback": [ 86 | "\u001b[0;36m File \u001b[0;32m\"\"\u001b[0;36m, line \u001b[0;32m18\u001b[0m\n\u001b[0;31m global tnext, tfinish, tstep\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m name 'tnext' is parameter and global\n" 87 | ] 88 | }, 89 | { 90 | "name": "stdout", 91 | "output_type": "stream", 92 | "text": [ 93 | "1 2018-02-10 14:08:05.355379\n", 94 | "2 2018-02-10 14:08:06.353722\n", 95 | "3 2018-02-10 14:08:07.353436\n", 96 | "4 2018-02-10 14:08:08.357000\n", 97 | "5 Last Call\n" 98 | ] 99 | } 100 | ], 101 | "source": [ 102 | "from tclab import TCLabModel, Historian, Plotter\n", 103 | "import threading, time\n", 104 | "\n", 105 | "tstep = 1\n", 106 | "tperiod = 20\n", 107 | "\n", 108 | "tstart = time.time()\n", 109 | "tfinish = tstart + tperiod\n", 110 | "tnext = tstart\n", 111 | "\n", 112 | "a = TCLabModel()\n", 113 | "h = Historian(a.sources)\n", 114 | "p = Plotter(h,20)\n", 115 | "a.U1 = 100\n", 116 | "\n", 117 | "\n", 118 | "def tasks(tnext):\n", 119 | " global tnext, tfinish, tstep\n", 120 | " p.update(tnext-tstart)\n", 121 | " tnext = tnext + tstep\n", 122 | " if tnext <= tfinish:\n", 123 | " threading.Timer(tnext-time.time(), update).start()\n", 124 | " else:\n", 125 | " a.close()\n", 126 | "\n", 127 | "update()" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "%matplotlib notebook\n", 137 | "\n", 138 | "import time\n", 139 | "from threading import Timer\n", 140 | "from tclab import setup, Historian, Plotter\n", 141 | "\n", 142 | "lab = setup(connected=False, speedup=1)\n", 143 | "a = lab()\n", 144 | "h = Historian(a.sources)\n", 145 | "p = Plotter(h)\n", 146 | "\n", 147 | "SP = 40\n", 148 | "\n", 149 | "tstart = time.time()\n", 150 | "def loop():\n", 151 | " PV = a.T1\n", 152 | " MV = 100 if PV < SP else 0\n", 153 | " a.U1 = MV\n", 154 | " p.update(time.time()-tstart)\n", 155 | "\n", 156 | "for t in range(0,100):\n", 157 | " Timer(t, loop).start()\n", 158 | "Timer(100,a.close).start()" 159 | ] 160 | }, 161 | { 162 | "cell_type": "code", 163 | "execution_count": null, 164 | "metadata": {}, 165 | "outputs": [], 166 | "source": [ 167 | "SP = 20" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "import threading, time, datetime\n", 177 | "\n", 178 | "def loop():\n", 179 | " yield\n", 180 | " print(datetime.datetime.now())\n", 181 | " threading.Timer(1000, lambda: next(loop_gen)).start()\n", 182 | " \n", 183 | "loop_gen = loop()\n", 184 | "next(loop_gen)\n" 185 | ] 186 | }, 187 | { 188 | "cell_type": "code", 189 | "execution_count": 2, 190 | "metadata": {}, 191 | "outputs": [ 192 | { 193 | "data": { 194 | "text/plain": [ 195 | "A Jupyter Widget" 196 | ] 197 | }, 198 | "metadata": {}, 199 | "output_type": "display_data" 200 | } 201 | ], 202 | "source": [ 203 | "%matplotlib inline\n", 204 | "import matplotlib.pyplot as plt\n", 205 | "import numpy as np\n", 206 | "\n", 207 | "import threading\n", 208 | "from IPython.display import display\n", 209 | "import ipywidgets as widgets\n", 210 | "import time\n", 211 | "progress = widgets.FloatProgress(value=0.0, min=0.0, max=1.0)\n", 212 | "\n", 213 | "def work(progress):\n", 214 | " t = np.linspace(0,100)\n", 215 | " for i in range(total):\n", 216 | " time.sleep(0.2)\n", 217 | " progress.value = float(i+1)/total\n", 218 | "\n", 219 | "thread = threading.Thread(target=work, args=(progress,))\n", 220 | "display(progress)\n", 221 | "thread.start()" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": 4, 227 | "metadata": {}, 228 | "outputs": [ 229 | { 230 | "data": { 231 | "text/plain": [ 232 | "12" 233 | ] 234 | }, 235 | "execution_count": 4, 236 | "metadata": {}, 237 | "output_type": "execute_result" 238 | } 239 | ], 240 | "source": [ 241 | "a = 12\n", 242 | "a" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": {}, 248 | "source": [ 249 | "## Run Class" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "from threading import Timer\n", 259 | "import time\n", 260 | "import tclab\n", 261 | "\n", 262 | "class Run(object):\n", 263 | " def __init__(self, function, tfinal, tinterval=1):\n", 264 | " self.lab = tclab.TCLab()\n", 265 | " self.tfinal = tfinal\n", 266 | " self.tinterval = tinterval\n", 267 | " self.function = function\n", 268 | " self._timer = None\n", 269 | " self.tstart = time.time()\n", 270 | " self.tnow = self.tstart\n", 271 | " self.is_running = False\n", 272 | " self.start()\n", 273 | "\n", 274 | " def _run(self):\n", 275 | " \"\"\"Start a new timer, then run the callback.\"\"\"\n", 276 | " self.is_running = False\n", 277 | " self.start()\n", 278 | " self.function(self.lab, self.tnow)\n", 279 | "\n", 280 | " def start(self):\n", 281 | " if not self.is_running:\n", 282 | " self.tnow = time.time() - self.tstart\n", 283 | " if self.tnow < self.tfinal:\n", 284 | " self._timer = Timer(self.tinterval - self.tnow % self.tinterval, self._run)\n", 285 | " else:\n", 286 | " self._timer = Timer(self.tinterval - self.tnow % self.tinterval, self.stop)\n", 287 | " self._timer.start()\n", 288 | " self.is_running = True \n", 289 | "\n", 290 | " def stop(self):\n", 291 | " if self.is_running:\n", 292 | " self._timer.cancel()\n", 293 | " self.is_running = False\n", 294 | " print(\"\")\n", 295 | " self.lab.close()" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": null, 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "SP = 40\n", 305 | "Kp = 15\n", 306 | "\n", 307 | "def loop(lab, t):\n", 308 | " PV = lab.T1\n", 309 | " MV = Kp*(SP-PV)\n", 310 | " lab.U1 = MV\n", 311 | " print(\"\\r{0:8.2f} {1:6.2f} {2:6.0f}\".format(t,PV,MV), end='')\n", 312 | " \n", 313 | "expt = Run(loop, 200, 1)\n", 314 | "time.sleep(10)\n", 315 | "expt.stop()" 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": null, 321 | "metadata": {}, 322 | "outputs": [], 323 | "source": [ 324 | "expt.stop()" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": null, 330 | "metadata": {}, 331 | "outputs": [], 332 | "source": [ 333 | "import tclab\n", 334 | "\n", 335 | "SP = 90\n", 336 | "\n", 337 | "def ControlLoop(lab, t):\n", 338 | " PV = lab.T1\n", 339 | " MV = 100 if PV < SP else 0\n", 340 | " lab.U1 = MV\n", 341 | " print(round(t,4), PV, MV)\n", 342 | " p.update(t)\n", 343 | " \n", 344 | " \n", 345 | "lab = tclab.TCLab()\n", 346 | "h = tclab.Historian(lab.sources, dbfile=None)\n", 347 | "p = tclab.Plotter(h)\n", 348 | "expt = PeriodicCallback(lab, ControlLoop, 10, 2)" 349 | ] 350 | }, 351 | { 352 | "cell_type": "markdown", 353 | "metadata": {}, 354 | "source": [ 355 | "## Working with Asyncio" 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 3, 361 | "metadata": { 362 | "scrolled": false 363 | }, 364 | "outputs": [ 365 | { 366 | "name": "stdout", 367 | "output_type": "stream", 368 | "text": [ 369 | "Arduino Leonardo connected on port /dev/cu.usbmodemWUAR1 at 115200 baud.\n", 370 | "TCLab Firmware 1.3.0 Arduino Leonardo/Micro.\n", 371 | "2.262144701962825 19.94 40 200.6\n", 372 | "4.000887564965524 19.94 40 200.6\n", 373 | "6.002586096001323 19.94 40 200.6\n", 374 | "8.002790475962684 19.94 40 200.6\n", 375 | "10.002313077973668 19.94 40 200.6\n", 376 | "12.003470623982139 19.94 40 200.6\n", 377 | "14.003858379961457 19.94 40 200.6\n", 378 | "16.00242742500268 19.94 40 200.6\n", 379 | "18.00530177797191 19.94 40 200.6\n", 380 | "20.004569922981318 19.94 40 200.6\n", 381 | "22.005170746007934 19.94 40 200.6\n", 382 | "24.00298130098963 19.94 40 200.6\n", 383 | "26.001176378980745 19.94 40 200.6\n", 384 | "28.00103564100573 19.94 40 200.6\n", 385 | "30.000585703994147 19.94 40 200.6\n", 386 | "32.00018944096519 19.94 40 200.6\n", 387 | "34.000133888970595 19.94 40 200.6\n", 388 | "36.000356623961125 19.94 40 200.6\n", 389 | "38.00522666599136 19.94 40 200.6\n", 390 | "40.00289643899305 19.94 40 200.6\n", 391 | "42.00436601799447 19.94 40 200.6\n", 392 | "44.00090896798065 19.94 40 200.6\n", 393 | "46.00117017800221 19.94 40 200.6\n", 394 | "48.00323838897748 19.94 40 200.6\n", 395 | "50.00018589699175 19.94 40 200.6\n", 396 | "52.00072305195499 19.94 40 200.6\n", 397 | "54.001219235011376 19.94 40 200.6\n", 398 | "56.00168056396069 19.94 40 200.6\n", 399 | "58.00438054098049 19.94 40 200.6\n", 400 | "60.00217565399362 19.94 40 200.6\n", 401 | "62.002833932987414 19.94 40 200.6\n", 402 | "64.00300401099958 19.94 40 200.6\n", 403 | "66.0036942109582 19.94 40 200.6\n", 404 | "68.00075817597099 19.94 40 200.6\n", 405 | "70.00092395499814 19.94 40 200.6\n", 406 | "72.00472351396456 20.26 40 197.39999999999998\n", 407 | "74.0054052589694 19.94 40 200.6\n", 408 | "76.00324653700227 19.94 40 200.6\n", 409 | "78.00263775599888 19.94 40 200.6\n", 410 | "80.00051065901062 19.94 40 200.6\n", 411 | "82.00130692799576 19.94 40 200.6\n", 412 | "84.00314901600359 19.94 40 200.6\n", 413 | "86.00340130197583 19.94 40 200.6\n", 414 | "88.00071146601113 19.94 40 200.6\n", 415 | "90.00536374299554 19.94 40 200.6\n", 416 | "92.001040172996 19.94 40 200.6\n", 417 | "94.00027199299075 19.94 40 200.6\n", 418 | "96.00035540695535 19.94 40 200.6\n", 419 | "98.00145010696724 19.94 40 200.6\n" 420 | ] 421 | } 422 | ], 423 | "source": [ 424 | "%gui asyncio\n", 425 | "\n", 426 | "import asyncio\n", 427 | "import tclab\n", 428 | "\n", 429 | "# define time function\n", 430 | "time = asyncio.get_event_loop().time\n", 431 | "tstart = time()\n", 432 | "tstep = 2\n", 433 | "tfinal = tstart + 100\n", 434 | "\n", 435 | "lab = tclab.setup(connected=True)\n", 436 | "a = lab()\n", 437 | "\n", 438 | "class PID():\n", 439 | " def __init__(self, Kp=1, Ki=0, Kd=0):\n", 440 | " self.Kp = Kp\n", 441 | " self.Ki = Ki\n", 442 | " self.Kd = Kd\n", 443 | " self.SP = \n", 444 | " self.eint = 0\n", 445 | " \n", 446 | " \n", 447 | " def update(self, PV, SP):\n", 448 | " return self.Kp*(SP - PV)\n", 449 | " \n", 450 | "pcontrol = PID(10,0,0)\n", 451 | "\n", 452 | "async def control_loop():\n", 453 | " while time() < tfinal:\n", 454 | " t = time() - tstart\n", 455 | " PV = a.T1\n", 456 | " SP = 40\n", 457 | " MV = pcontrol.update(PV,SP)\n", 458 | " a.Q1(MV)\n", 459 | " print(t, PV, SP, MV)\n", 460 | " await asyncio.sleep(tstep - (time() - tstart) % tstep)\n", 461 | "\n", 462 | "task = asyncio.ensure_future(control_loop())\n" 463 | ] 464 | }, 465 | { 466 | "cell_type": "code", 467 | "execution_count": 2, 468 | "metadata": {}, 469 | "outputs": [ 470 | { 471 | "name": "stdout", 472 | "output_type": "stream", 473 | "text": [ 474 | "32.00036316696787 19.94 40 200.6\n", 475 | "TCLab disconnected successfully.\n" 476 | ] 477 | } 478 | ], 479 | "source": [ 480 | "task.cancel()\n", 481 | "a.close()" 482 | ] 483 | }, 484 | { 485 | "cell_type": "markdown", 486 | "metadata": {}, 487 | "source": [ 488 | "## Working with Tornado\n", 489 | "\n", 490 | "This is an experiment to build a non-blocking event loop for TCLab. The main idea is to implement the main event loop as a generator, then use Tornando's non-blocking timer to send periodic messages to the generator." 491 | ] 492 | }, 493 | { 494 | "cell_type": "code", 495 | "execution_count": null, 496 | "metadata": {}, 497 | "outputs": [], 498 | "source": [ 499 | "%matplotlib inline\n", 500 | "import tornado\n", 501 | "import time\n", 502 | "from tclab import setup, Historian, Plotter\n", 503 | "\n", 504 | "SP = 40\n", 505 | "Kp = 10\n", 506 | "\n", 507 | "def update(lab):\n", 508 | " t = 0\n", 509 | " h = Historian(lab.sources)\n", 510 | " p = Plotter(h,120)\n", 511 | " while True:\n", 512 | " PV = lab.T1\n", 513 | " MV = Kp*(SP-PV)\n", 514 | " lab.U1 = MV\n", 515 | " p.update(t)\n", 516 | " yield\n", 517 | " t += 1\n", 518 | "\n", 519 | "lab = setup(connected=True)\n", 520 | "a = lab()\n", 521 | "update_gen = update(a)\n", 522 | "timer = tornado.ioloop.PeriodicCallback(lambda: next(update_gen), 1000)\n", 523 | "timer.start()" 524 | ] 525 | }, 526 | { 527 | "cell_type": "code", 528 | "execution_count": null, 529 | "metadata": {}, 530 | "outputs": [], 531 | "source": [ 532 | "timer.stop()\n", 533 | "a.close()" 534 | ] 535 | }, 536 | { 537 | "cell_type": "markdown", 538 | "metadata": {}, 539 | "source": [ 540 | "### Adding Widgets\n", 541 | "\n", 542 | "`tclab.clock` is based on a generator, which maintains a single thread of execution. One consequence is that there is no interaction with Jupyter widgets." 543 | ] 544 | }, 545 | { 546 | "cell_type": "code", 547 | "execution_count": null, 548 | "metadata": {}, 549 | "outputs": [], 550 | "source": [ 551 | "from ipywidgets import interactive\n", 552 | "from IPython.display import display\n", 553 | "from tclab import clock\n", 554 | "\n", 555 | "Kp = interactive(lambda Kp: Kp, Kp = 12)\n", 556 | "display(Kp)\n", 557 | "\n", 558 | "for t in clock(10):\n", 559 | " print(t, Kp.result)" 560 | ] 561 | }, 562 | { 563 | "cell_type": "code", 564 | "execution_count": null, 565 | "metadata": {}, 566 | "outputs": [], 567 | "source": [ 568 | "import tornado\n", 569 | "from ipywidgets import interactive\n", 570 | "from IPython.display import display\n", 571 | "from tclab import TCLab, Historian, Plotter\n", 572 | "\n", 573 | "Kp = interactive(lambda Kp: Kp, Kp = (0,20))\n", 574 | "SP = interactive(lambda SP: SP, SP = (25,55))\n", 575 | "SP.layout.height = '500px'\n", 576 | "\n", 577 | "def update(tperiod):\n", 578 | " t = 0\n", 579 | " with TCLab() as a:\n", 580 | " h = Historian(a.sources)\n", 581 | " p = Plotter(h)\n", 582 | " while t <= tperiod:\n", 583 | " yield\n", 584 | " p.update(t)\n", 585 | " display(Kp)\n", 586 | " display(SP)\n", 587 | " a.U1 = SP.result\n", 588 | " t += 1\n", 589 | " timer.stop()\n", 590 | "\n", 591 | "update_gen = update(20)\n", 592 | "timer = tornado.ioloop.PeriodicCallback(lambda: next(update_gen), 1000)\n", 593 | "timer.start()" 594 | ] 595 | }, 596 | { 597 | "cell_type": "code", 598 | "execution_count": null, 599 | "metadata": {}, 600 | "outputs": [], 601 | "source": [ 602 | "from ipywidgets import interactive\n", 603 | "from tclab import setup, clock, Historian, Plotter\n", 604 | "\n", 605 | "def proportional(Kp):\n", 606 | " MV = 0\n", 607 | " while True:\n", 608 | " PV, SP = yield MV\n", 609 | " MV = Kp*(SP-PV)\n", 610 | "\n", 611 | "def sim(Kp=1, SP=40):\n", 612 | " controller = proportional(Kp)\n", 613 | " controller.send(None)\n", 614 | "\n", 615 | " lab = setup(connected=False, speedup=20)\n", 616 | " with lab() as a:\n", 617 | " h = Historian(a.sources)\n", 618 | " p = Plotter(h,200)\n", 619 | " for t in clock(200):\n", 620 | " PV = a.T1\n", 621 | " MV = controller.send([PV,SP])\n", 622 | " a.U1 = MV\n", 623 | " h.update()\n", 624 | " p.update() \n", 625 | "\n", 626 | "interactive_plot = interactive(sim, Kp=(0,20,1), SP=(25,60,5), continuous_update=False);\n", 627 | "output = interactive_plot.children[-1]\n", 628 | "output.layout.height = '500px'\n", 629 | "interactive_plot" 630 | ] 631 | }, 632 | { 633 | "cell_type": "code", 634 | "execution_count": null, 635 | "metadata": {}, 636 | "outputs": [], 637 | "source": [ 638 | "timer.stop()" 639 | ] 640 | }, 641 | { 642 | "cell_type": "code", 643 | "execution_count": null, 644 | "metadata": {}, 645 | "outputs": [], 646 | "source": [] 647 | } 648 | ], 649 | "metadata": { 650 | "kernelspec": { 651 | "display_name": "Python 3", 652 | "language": "python", 653 | "name": "python3" 654 | }, 655 | "language_info": { 656 | "codemirror_mode": { 657 | "name": "ipython", 658 | "version": 3 659 | }, 660 | "file_extension": ".py", 661 | "mimetype": "text/x-python", 662 | "name": "python", 663 | "nbconvert_exporter": "python", 664 | "pygments_lexer": "ipython3", 665 | "version": "3.6.8" 666 | } 667 | }, 668 | "nbformat": 4, 669 | "nbformat_minor": 2 670 | } 671 | --------------------------------------------------------------------------------