{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 14. Asynchronously receiving data from Arduino\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " \n", " Loading BokehJS ...\n", "
" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "\n", "(function(root) {\n", " function now() {\n", " return new Date();\n", " }\n", "\n", " var force = true;\n", "\n", " if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n", " root._bokeh_onload_callbacks = [];\n", " root._bokeh_is_loading = undefined;\n", " }\n", "\n", " var JS_MIME_TYPE = 'application/javascript';\n", " var HTML_MIME_TYPE = 'text/html';\n", " var EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n", " var CLASS_NAME = 'output_bokeh rendered_html';\n", "\n", " /**\n", " * Render data to the DOM node\n", " */\n", " function render(props, node) {\n", " var script = document.createElement(\"script\");\n", " node.appendChild(script);\n", " }\n", "\n", " /**\n", " * Handle when an output is cleared or removed\n", " */\n", " function handleClearOutput(event, handle) {\n", " var cell = handle.cell;\n", "\n", " var id = cell.output_area._bokeh_element_id;\n", " var server_id = cell.output_area._bokeh_server_id;\n", " // Clean up Bokeh references\n", " if (id != null && id in Bokeh.index) {\n", " Bokeh.index[id].model.document.clear();\n", " delete Bokeh.index[id];\n", " }\n", "\n", " if (server_id !== undefined) {\n", " // Clean up Bokeh references\n", " var cmd = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n", " cell.notebook.kernel.execute(cmd, {\n", " iopub: {\n", " output: function(msg) {\n", " var id = msg.content.text.trim();\n", " if (id in Bokeh.index) {\n", " Bokeh.index[id].model.document.clear();\n", " delete Bokeh.index[id];\n", " }\n", " }\n", " }\n", " });\n", " // Destroy server and session\n", " var cmd = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n", " cell.notebook.kernel.execute(cmd);\n", " }\n", " }\n", "\n", " /**\n", " * Handle when a new output is added\n", " */\n", " function handleAddOutput(event, handle) {\n", " var output_area = handle.output_area;\n", " var output = handle.output;\n", "\n", " // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n", " if ((output.output_type != \"display_data\") || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", " return\n", " }\n", "\n", " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", "\n", " if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n", " toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n", " // store reference to embed id on output_area\n", " output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", " }\n", " if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", " var bk_div = document.createElement(\"div\");\n", " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", " var script_attrs = bk_div.children[0].attributes;\n", " for (var i = 0; i < script_attrs.length; i++) {\n", " toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n", " toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n", " }\n", " // store reference to server id on output_area\n", " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", " }\n", " }\n", "\n", " function register_renderer(events, OutputArea) {\n", "\n", " function append_mime(data, metadata, element) {\n", " // create a DOM node to render to\n", " var toinsert = this.create_output_subarea(\n", " metadata,\n", " CLASS_NAME,\n", " EXEC_MIME_TYPE\n", " );\n", " this.keyboard_manager.register_events(toinsert);\n", " // Render to node\n", " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", " render(props, toinsert[toinsert.length - 1]);\n", " element.append(toinsert);\n", " return toinsert\n", " }\n", "\n", " /* Handle when an output is cleared or removed */\n", " events.on('clear_output.CodeCell', handleClearOutput);\n", " events.on('delete.Cell', handleClearOutput);\n", "\n", " /* Handle when a new output is added */\n", " events.on('output_added.OutputArea', handleAddOutput);\n", "\n", " /**\n", " * Register the mime type and append_mime function with output_area\n", " */\n", " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", " /* Is output safe? */\n", " safe: true,\n", " /* Index of renderer in `output_area.display_order` */\n", " index: 0\n", " });\n", " }\n", "\n", " // register the mime type if in Jupyter Notebook environment and previously unregistered\n", " if (root.Jupyter !== undefined) {\n", " var events = require('base/js/events');\n", " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", "\n", " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", " register_renderer(events, OutputArea);\n", " }\n", " }\n", "\n", " \n", " if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n", " root._bokeh_timeout = Date.now() + 5000;\n", " root._bokeh_failed_load = false;\n", " }\n", "\n", " var NB_LOAD_WARNING = {'data': {'text/html':\n", " \"
\\n\"+\n", " \"

\\n\"+\n", " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n", " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n", " \"

\\n\"+\n", " \"\\n\"+\n", " \"\\n\"+\n", " \"from bokeh.resources import INLINE\\n\"+\n", " \"output_notebook(resources=INLINE)\\n\"+\n", " \"\\n\"+\n", " \"
\"}};\n", "\n", " function display_loaded() {\n", " var el = document.getElementById(\"1001\");\n", " if (el != null) {\n", " el.textContent = \"BokehJS is loading...\";\n", " }\n", " if (root.Bokeh !== undefined) {\n", " if (el != null) {\n", " el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n", " }\n", " } else if (Date.now() < root._bokeh_timeout) {\n", " setTimeout(display_loaded, 100)\n", " }\n", " }\n", "\n", "\n", " function run_callbacks() {\n", " try {\n", " root._bokeh_onload_callbacks.forEach(function(callback) {\n", " if (callback != null)\n", " callback();\n", " });\n", " } finally {\n", " delete root._bokeh_onload_callbacks\n", " }\n", " console.debug(\"Bokeh: all callbacks have finished\");\n", " }\n", "\n", " function load_libs(css_urls, js_urls, callback) {\n", " if (css_urls == null) css_urls = [];\n", " if (js_urls == null) js_urls = [];\n", "\n", " root._bokeh_onload_callbacks.push(callback);\n", " if (root._bokeh_is_loading > 0) {\n", " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", " return null;\n", " }\n", " if (js_urls == null || js_urls.length === 0) {\n", " run_callbacks();\n", " return null;\n", " }\n", " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", " root._bokeh_is_loading = css_urls.length + js_urls.length;\n", "\n", " function on_load() {\n", " root._bokeh_is_loading--;\n", " if (root._bokeh_is_loading === 0) {\n", " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", " run_callbacks()\n", " }\n", " }\n", "\n", " function on_error() {\n", " console.error(\"failed to load \" + url);\n", " }\n", "\n", " for (var i = 0; i < css_urls.length; i++) {\n", " var url = css_urls[i];\n", " const element = document.createElement(\"link\");\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.rel = \"stylesheet\";\n", " element.type = \"text/css\";\n", " element.href = url;\n", " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", " document.body.appendChild(element);\n", " }\n", "\n", " const hashes = {\"https://cdn.bokeh.org/bokeh/release/bokeh-2.2.1.min.js\": \"qkRvDQVAIfzsJo40iRBbxt6sttt0hv4lh74DG7OK4MCHv4C5oohXYoHUM5W11uqS\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.2.1.min.js\": \"Sb7Mr06a9TNlet/GEBeKaf5xH3eb6AlCzwjtU82wNPyDrnfoiVl26qnvlKjmcAd+\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.2.1.min.js\": \"HaJ15vgfmcfRtB4c4YBOI4f1MUujukqInOWVqZJZZGK7Q+ivud0OKGSTn/Vm2iso\"};\n", "\n", " for (var i = 0; i < js_urls.length; i++) {\n", " var url = js_urls[i];\n", " var element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error;\n", " element.async = false;\n", " element.src = url;\n", " if (url in hashes) {\n", " element.crossOrigin = \"anonymous\";\n", " element.integrity = \"sha384-\" + hashes[url];\n", " }\n", " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", " document.head.appendChild(element);\n", " }\n", " };\n", "\n", " function inject_raw_css(css) {\n", " const element = document.createElement(\"style\");\n", " element.appendChild(document.createTextNode(css));\n", " document.body.appendChild(element);\n", " }\n", "\n", " \n", " var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.2.1.min.js\"];\n", " var css_urls = [];\n", " \n", "\n", " var inline_js = [\n", " function(Bokeh) {\n", " Bokeh.set_log_level(\"info\");\n", " },\n", " function(Bokeh) {\n", " \n", " \n", " }\n", " ];\n", "\n", " function run_inline_js() {\n", " \n", " if (root.Bokeh !== undefined || force === true) {\n", " \n", " for (var i = 0; i < inline_js.length; i++) {\n", " inline_js[i].call(root, root.Bokeh);\n", " }\n", " if (force === true) {\n", " display_loaded();\n", " }} else if (Date.now() < root._bokeh_timeout) {\n", " setTimeout(run_inline_js, 100);\n", " } else if (!root._bokeh_failed_load) {\n", " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", " root._bokeh_failed_load = true;\n", " } else if (force !== true) {\n", " var cell = $(document.getElementById(\"1001\")).parents('.cell').data().cell;\n", " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n", " }\n", "\n", " }\n", "\n", " if (root._bokeh_is_loading === 0) {\n", " console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n", " run_inline_js();\n", " } else {\n", " load_libs(css_urls, js_urls, function() {\n", " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", " run_inline_js();\n", " });\n", " }\n", "}(window));" ], "application/vnd.bokehjs_load.v0+json": "\n(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n \n\n \n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n var NB_LOAD_WARNING = {'data': {'text/html':\n \"
\\n\"+\n \"

\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"

\\n\"+\n \"\\n\"+\n \"\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n var el = document.getElementById(\"1001\");\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n const hashes = {\"https://cdn.bokeh.org/bokeh/release/bokeh-2.2.1.min.js\": \"qkRvDQVAIfzsJo40iRBbxt6sttt0hv4lh74DG7OK4MCHv4C5oohXYoHUM5W11uqS\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.2.1.min.js\": \"Sb7Mr06a9TNlet/GEBeKaf5xH3eb6AlCzwjtU82wNPyDrnfoiVl26qnvlKjmcAd+\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.2.1.min.js\": \"HaJ15vgfmcfRtB4c4YBOI4f1MUujukqInOWVqZJZZGK7Q+ivud0OKGSTn/Vm2iso\"};\n\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n if (url in hashes) {\n element.crossOrigin = \"anonymous\";\n element.integrity = \"sha384-\" + hashes[url];\n }\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n \n var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.2.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.2.1.min.js\"];\n var css_urls = [];\n \n\n var inline_js = [\n function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\n function(Bokeh) {\n \n \n }\n ];\n\n function run_inline_js() {\n \n if (root.Bokeh !== undefined || force === true) {\n \n for (var i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n if (force === true) {\n display_loaded();\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n var cell = $(document.getElementById(\"1001\")).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));" }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import re\n", "import asyncio\n", "import time\n", "\n", "import numpy as np\n", "import pandas as pd\n", "\n", "import serial\n", "import serial.tools.list_ports\n", "\n", "import bokeh.plotting\n", "import bokeh.io\n", "import bokeh.driving\n", "bokeh.io.output_notebook()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "In this lesson, you will learn how to use Python's built-in asynchronous capabilities to constantly receive data from Arduino without blocking so that you can use the Python interpreter to do other tasks." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup\n", " \n", "The setup for this lesson is the same as the previous one. If you want to skip to the section after the setup, execute the code cell below that contains the utility functions and then [click here](#Why-do-we-need-asynchrony?)." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def find_arduino(port=None):\n", " \"\"\"Get the name of the port that is connected to Arduino.\"\"\"\n", " if port is None:\n", " ports = serial.tools.list_ports.comports()\n", " for p in ports:\n", " if p.manufacturer is not None and \"Arduino\" in p.manufacturer:\n", " port = p.device\n", " return port\n", "\n", "\n", "def handshake_arduino(\n", " arduino, sleep_time=1, print_handshake_message=False, handshake_code=0\n", "):\n", " \"\"\"Make sure connection is established by sending\n", " and receiving bytes.\"\"\"\n", " # Close and reopen\n", " arduino.close()\n", " arduino.open()\n", "\n", " # Chill out while everything gets set\n", " time.sleep(sleep_time)\n", "\n", " # Set a long timeout to complete handshake\n", " timeout = arduino.timeout\n", " arduino.timeout = 2\n", " \n", " # Read and discard everything that may be in the input buffer\n", " _ = arduino.read_all()\n", "\n", " # Send request to Arduino\n", " arduino.write(bytes([handshake_code]))\n", "\n", " # Read in what Arduino sent\n", " handshake_message = arduino.read_until()\n", " \n", " # Send and receive request again\n", " arduino.write(bytes([handshake_code]))\n", " handshake_message = arduino.read_until()\n", " \n", " # Print the handshake message, if desired\n", " if print_handshake_message:\n", " print(\"Handshake message: \" + handshake_message.decode())\n", "\n", " # Reset the timeout\n", " arduino.timeout = timeout" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The schematic we will use is shown below.\n", "\n", "
\n", " \n", "![Arduino data transfer schematic](arduino_data_transfer_schem.svg)\n", " \n", "
\n", "\n", "The sketch is\n", "\n", "```arduino\n", "const int voltagePin = A0;\n", "\n", "const int HANDSHAKE = 0;\n", "const int VOLTAGE_REQUEST = 1;\n", "const int ON_REQUEST = 2;\n", "const int STREAM = 3;\n", "const int READ_DAQ_DELAY = 4;\n", "\n", "// Initially, only send data upon request\n", "int daqMode = ON_REQUEST;\n", "\n", "// Default time between data acquisition is 100 ms\n", "int daqDelay = 100;\n", "\n", "// String to store input of DAQ delay\n", "String daqDelayStr;\n", "\n", "\n", "// Keep track of last data acquistion for delays\n", "unsigned long timeOfLastDAQ = 0;\n", "\n", "\n", "unsigned long printVoltage() {\n", " // Read value from analog pin\n", " int value = analogRead(voltagePin);\n", "\n", " // Get the time point\n", " unsigned long timeMilliseconds = millis();\n", "\n", " // Write the result\n", " if (Serial.availableForWrite()) {\n", " String outstr = String(String(timeMilliseconds, DEC) + \",\" + String(value, DEC));\n", " Serial.println(outstr);\n", " }\n", "\n", " // Return time of acquisition\n", " return timeMilliseconds;\n", "}\n", "\n", "\n", "void setup() {\n", " // Initialize serial communication\n", " Serial.begin(115200);\n", "}\n", "\n", "\n", "void loop() { \n", " // If we're streaming\n", " if (daqMode == STREAM) {\n", " if (millis() - timeOfLastDAQ >= daqDelay) {\n", " timeOfLastDAQ = printVoltage();\n", " }\n", " }\n", " \n", " // Check if data has been sent to Arduino and respond accordingly\n", " if (Serial.available() > 0) {\n", " // Read in request\n", " int inByte = Serial.read();\n", "\n", " // If data is requested, fetch it and write it, or handshake\n", " switch(inByte) {\n", " case VOLTAGE_REQUEST:\n", " timeOfLastDAQ = printVoltage();\n", " break;\n", " case ON_REQUEST:\n", " daqMode = ON_REQUEST;\n", " break;\n", " case STREAM:\n", " daqMode = STREAM;\n", " break;\n", " case READ_DAQ_DELAY:\n", " // Read in delay, knowing it is appended with an x\n", " daqDelayStr = Serial.readStringUntil('x');\n", "\n", " // Convert to int and store\n", " daqDelay = daqDelayStr.toInt();\n", "\n", " break;\n", " case HANDSHAKE:\n", " if (Serial.availableForWrite()) {\n", " Serial.println(\"Message received.\");\n", " }\n", " break;\n", " }\n", " }\n", "}\n", "```\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Why do we need asynchrony?\n", "\n", "In the previous lesson, when we were streaming in data, we had a structure like this:\n", "\n", "```python\n", "while i < n_data:\n", " raw = arduino.read_until()\n", "\n", " try:\n", " t, V = parse_raw(raw)\n", " time_ms[i] = t\n", " voltage[i] = V\n", " i += 1\n", " except:\n", " pass\n", "```\n", "\n", "Most of the time, an iteration of the while loop resulted in a `pass`. We were essentially telling the Python interpreter to constantly be trying to read and parse. Just like `delay()` in Arduino is blocking, so too is this for the Python interpreter. While the while loop is running, the interpreter cannot attend to any other tasks.\n", "\n", "When you are building devices, there are plenty of other tasks you want the Python interpreter to be doing while it is acquiring data. At the very least, you may want it to be listening for more user input to *stop* acquiring data. But you may also want to perform calculations on the incoming data (such as digital filtering), control and/or receive data from other connected devices, or even just mess around in your Jupyter notebook.\n", "\n", "In order to do these things, you want the data acquisition to happen asynchronously. You want the interpreter to occasionally read and parse data, but be free to do whatever else you want it to do when it is not reading and parsing data." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Reading data in chunks\n", "\n", "As a first step toward asynchrony, we will write a function to read data in **chunks**. Instead of constantly monitoring the data coming in over the serial connection, we would rather occasionally check the serial connection to see if there are any data in the input buffer. If there is, we read in whatever is in the input buffer to clear it, go off and process that, and then wait a while before checking again. During that waiting time, you can have the interpreter do other tasks.\n", "\n", "**Warning**: Don't wait too long to read, though! You do not want to overrun the USB input buffer size on your computer. Arduino's output buffer is 64 bytes, and computers can have default input buffer sizes as low as 64 bytes as well. (I think most computers these days have input buffer sizes around 1024 bytes, but it does vary from machine to machine.)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Chunk reading for non-corrupted data\n", "\n", "The function below reads in all of the data that is in the input buffer and returns the data as a byte string. We should specify a short timeout so that reading will stop before the buffer starts filling up again." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def read_all(ser, read_buffer=b\"\", **args):\n", " \"\"\"Read all available bytes from the serial port\n", " and append to the read buffer.\n", "\n", " Parameters\n", " ----------\n", " ser : serial.Serial() instance\n", " The device we are reading from.\n", " read_buffer : bytes, default b''\n", " Previous read buffer that is appended to.\n", "\n", " Returns\n", " -------\n", " output : bytes\n", " Bytes object that contains read_buffer + read.\n", " \n", " Notes\n", " -----\n", " .. `**args` appears, but is never used. This is for \n", " compatibility with `read_all_newlines()` as a \n", " drop-in replacement for this function.\n", " \"\"\"\n", " # Set timeout to None to make sure we read all bytes\n", " previous_timeout = ser.timeout\n", " ser.timeout = None\n", "\n", " in_waiting = ser.in_waiting\n", " read = ser.read(size=in_waiting)\n", "\n", " # Reset to previous timeout\n", " ser.timeout = previous_timeout\n", "\n", " return read_buffer + read" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For our present application, in which we read in comma-delimited time-voltage data, the byte string returned from this function might look like this:\n", "\n", " b'1032,541\\r\\n1052,542\\r\\n1073,554\\r\\n1093,5'\n", " \n", "Note that it does not end in a carriage return and newline. Those characters might not be in the read buffer yet, and since we are not using `read_until()`, we will not keep reading until we get those terminating characters. So, if we are parsing the output of this function, we should keep the last incomplete part of the data (in this case, `b'1093,5'` around for the next read.\n", "\n", "Here is a parser that returns both the times and voltages as lists, as well as the remaining bytes that we will pass as the `read_buffer` kwarg in the `read_all()` function. There is some error checking. The only allowed characters are carriage returns, new lines, commas, and digits. Any message having other characters is discarded." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def parse_read(read):\n", " \"\"\"Parse a read with time, voltage data\n", "\n", " Parameters\n", " ----------\n", " read : byte string\n", " Byte string with comma delimited time/voltage\n", " measurements.\n", "\n", " Returns\n", " -------\n", " time_ms : list of ints\n", " Time points in milliseconds.\n", " voltage : list of floats\n", " Voltages in volts.\n", " remaining_bytes : byte string\n", " Remaining, unparsed bytes.\n", " \"\"\"\n", " time_ms = []\n", " voltage = []\n", "\n", " # Separate independent time/voltage measurements\n", " pattern = re.compile(b\"\\d+|,\")\n", " raw_list = [\n", " b\"\".join(pattern.findall(raw)).decode() \n", " for raw in read.split(b\"\\r\\n\")\n", " ]\n", " \n", " for raw in raw_list[:-1]:\n", " try:\n", " t, V = raw.split(\",\")\n", " time_ms.append(int(t))\n", " voltage.append(int(V) * 5 / 1023)\n", " except:\n", " pass\n", "\n", " if len(raw_list) == 0:\n", " return time_ms, voltage, b\"\"\n", " else:\n", " return time_ms, voltage, raw_list[-1].encode()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Chunk reading with corrupted data\n", "\n", "We discovered that on Windows machines, reads made with pySerial can sometimes result in corrupted bytes. This makes the read-in string unusable, and in many cases un-parsable because the resulting bytes do not correspond to any characters in ASCII. I am not sure exactly why this happens, but I suspect it is due to the read of a given byte being incomplete, with the read being interrupted before the stop bit. To counteract this, we can instead read chunks that *must* terminate in a newline using `read_until()`. This blocks all other processes until the complete newline byte is read. This also ensures that all bytes preceding the newline are read in their entirety as well." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "def read_all_newlines(ser, read_buffer=b\"\", n_reads=4):\n", " \"\"\"Read data in until encountering newlines.\n", "\n", " Parameters\n", " ----------\n", " ser : serial.Serial() instance\n", " The device we are reading from.\n", " n_reads : int\n", " The number of reads up to newlines\n", " read_buffer : bytes, default b''\n", " Previous read buffer that is appended to.\n", " \n", " Returns\n", " -------\n", " output : bytes\n", " Bytes object that contains read_buffer + read.\n", " \n", " Notes\n", " -----\n", " .. This is a drop-in replacement for read_all().\n", " \"\"\"\n", " raw = read_buffer\n", " for _ in range(n_reads):\n", " raw += ser.read_until()\n", " \n", " return raw" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## asyncio\n", "\n", "*While this section is not formally a follow-along exercise, I highly recommend running the code in this notebook because you will note delays as the code runs that will help you understand how `asyncio` is working.*\n", "\n", "Python has handy built-in asynchronous capabilities using the `asyncio` module from the standard library. It was introduced recently, in Python 3.5, and has had changes and deprecations since. The version in Python 3.8 has nice high-level functionality and has a stable API, so it is important that you are using Python 3.8.\n", "\n", "I will give a brief overview of how it works here, but you would be well-served to [read the documentation](https://docs.python.org/3/library/asyncio.html), most importantly the [coroutines and tasks section](https://docs.python.org/3/library/asyncio-task.html).\n", "\n", "At the center of asyncio's high-level functionality are **awaitables**. An awaitable is a process that the interpreter can suspend such that it is not blocking. A very important awaitable is `asyncio.sleep()`, which is one we will put to use.\n", "\n", "Aside from sleeping, the awaitables we will use are **coroutines** and **tasks**. You can think of a coroutine as a function that you can start and stop and start again. A task runs a coroutine. As usual, this is best seen by example.\n", "\n", "We will start by making a coroutine that is a greeting in English. It says \"hello\" and then waits one second to say \"world.\" It returns a string describing what the message was. We would do this in a synchronous way (so it is a function and *not* a coroutine) like this:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def english(exclaim=False):\n", " print(\"Hello, \")\n", " time.sleep(1)\n", " print(\"world\" + (\"!\" if exclaim else \".\"))\n", " \n", " return \"The message was a greeting to the world.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can run this function, and it works as expected." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello, \n", "world!\n" ] } ], "source": [ "message = english(exclaim=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The problem is that the function, like all functions in Python, blocked. While waiting for a second to see \"world,\" the Python interpreter was busy.\n", "\n", "Now, let's write an asynchronous version, that is a coroutine." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "async def english_async(exclaim=False):\n", " print(\"Hello, \")\n", " await asyncio.sleep(1)\n", " print(\"world\" + (\"!\" if exclaim else \".\"))\n", " \n", " return \"The message was a greeting to the world.\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `async def` keyword signifies that this is not a function, but a coroutine. That means the interpreter can start running the coroutine, leave it and do something else, and then run it again. It can only leave the coroutine where an awaitable is run. To run an awaitable within a coroutine, we use the `await` keyword. So, when we run `await asyncio.sleep(1)`, the Python interpreter turns its attention away from the `english_async()` coroutine until `asyncio.sleep()` returns, which will happen after one second.\n", "\n", "We cannot just run a coroutine like it is a function. Look:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "english_async(exclaim=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We get back a coroutine. To run it, we need a running [event loop](https://en.wikipedia.org/wiki/Event_loop). An event loop enables asynchronous computing by listening for requests to do something, and then dispatching resources to do the requested calculation. Each thread (which you can think of for our purposes as one core of your CPU) can have either zero or one event loops. If you are running a Jupyter notebook, there is an active event loop; that is how JupyterLab runs, waiting for you to execute a cell. If you are not in a Jupyter notebook, you probably do not have an event loop running, so you need to start one. We will discuss how to start and run an event loop outside of JupyterLab [later in this lesson](#Running-without-an-existing-event-loop). For now, we will assume you have a running event loop, as you do in a Jupyter notebook.\n", "\n", "To run the coroutine, you can create a task using `asyncio.create_task()`. Note that \"calling\" a coroutine like a function returns a coroutine, so the argument you pass into `asyncio.create_task()` is how you would call the coroutine, including all arguments and keyword arguments. Upon creation, the coroutine is run." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello, \n", "world!\n" ] } ], "source": [ "task_english = asyncio.create_task(english_async(exclaim=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You can access the return value of the coroutine using the `result()` method of the task. Of course, you should first check the `done()` method of the task to see if it has completed." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "task_english.done()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And we can safely retrieve the result." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'The message was a greeting to the world.'" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "task_english.result()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, let's make another coroutine that says the same greeting in Spanish. For demonstration purposes, this function will only wait a half second between the two words." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "async def spanish_async(exclaim=False):\n", " print((\" ¡\" if exclaim else \" \") + \"Hola, \")\n", " await asyncio.sleep(0.5)\n", " print(\" mundo\" + (\"!\" if exclaim else \".\"))\n", "\n", " return(\"El mensaje fue un saludo al mundo.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can run this coroutine as we did for the English one." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " ¡Hola, \n", " mundo!\n" ] } ], "source": [ "task_spanish = asyncio.create_task(spanish_async(exclaim=True))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With asynchronous computing, we can run the two coroutines *concurrently*! There are several ways to do this. First, we can create tasks one after another. The first task is created, \"Hello,\" is printed, and then the second task is created. (This time, we won't exclaim.)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello, \n", " Hola, \n", " mundo.\n", "world.\n" ] } ], "source": [ "task_english = asyncio.create_task(english_async())\n", "task_spanish = asyncio.create_task(spanish_async())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because the delay is shorter for the Spanish version, the entire message gets printed before the English message is complete.\n", "\n", "As another option, we can gather the coroutines together using `asyncio.gather()`." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello, \n", " Hola, \n", " mundo.\n", "world.\n" ] } ], "source": [ "task_english_spanish = asyncio.gather(english_async(), spanish_async())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To get the return values, we gain use `task_english_spanish.result()`." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['The message was a greeting to the world.',\n", " 'El mensaje fue un saludo al mundo.']" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "task_english_spanish.result()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the result is the return values from the two coroutines as a list." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Canceling a task\n", "\n", "Once a task is created, it may be interrupted and canceled using the `cancel()` method of the task. For example, we can cancel the English greeting before the second word comes out." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello, \n" ] } ], "source": [ "# Create the task\n", "task_english = asyncio.create_task(english_async())\n", "\n", "# Wait a half second\n", "await asyncio.sleep(0.5)\n", "\n", "# Cancel the task\n", "successfully_canceled = task_english.cancel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the `cancel()` method requests a cancellation, but cancellation is not guaranteed. You should read the asyncio documentation for more information.\n", "\n", "A canceled job will both be marked as done and canceled." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(True, True)" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "task_english.done(), task_english.cancelled()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since it was not allowed to return, though, the result will be a `CancelledError`." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "ename": "CancelledError", "evalue": "", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mCancelledError\u001b[0m Traceback (most recent call last)", "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mtask_english\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", "\u001b[0;31mCancelledError\u001b[0m: " ] } ], "source": [ "task_english.result()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Running without an existing event loop\n", "\n", "If you do not have a running event loop on your thread, which will typically be the case if you are running outside of JupyterLab, you need to start an event loop. Fortunately, `asynchio` provides a convenient way to start (and automatically terminate upon completion of all coroutines) with its `asyncio.run()` function. To use it, define a coroutine that awaits all of the tasks you want to run and then pass that coroutine as an argument to `asyncio.run()`. For example, to run the English and Spanish greetings concurrently, do the following.\n", "\n", "```python\n", "async def main():\n", " gathered = asyncio.gather(english_async(), spanish_async())\n", " await gathered\n", " \n", " return gathered.result()\n", "\n", "\n", "asyncio.run(main())\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Receiving data asynchronously\n", "\n", "Now that we understand how asynchrony works in Python, let's receive some data! We'll of course start by shaking hands with Arduino." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Handshake message: Message received.\n", "\n" ] } ], "source": [ "HANDSHAKE = 0\n", "VOLTAGE_REQUEST = 1\n", "ON_REQUEST = 2;\n", "STREAM = 3;\n", "READ_DAQ_DELAY = 4;\n", "\n", "# Windows users may need to give COM port for find_arduino()\n", "port = find_arduino()\n", "\n", "# Connect and handshake\n", "arduino = serial.Serial(port, baudrate=115200)\n", "handshake_arduino(arduino, print_handshake_message=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can write a coroutine to acquire data. This is very much like the `daq_stream()` function we encountered in the last lesson, except for a couple key differences. \n", "\n", "1. We will read the data in chunks using the functions we wrote at the beginning of this lesson.\n", "2. I read in the first few messages sent from Arduino after turning on the stream and discard them, just to ensure the input buffer of my computer is cleared and we're getting good clean reads. \n", "3. We sleep between acquisitions. I choose to sleep about 80% of the time of the acquisitions. This ensures that I will never have too many bytes in the input buffer, but I am still not checking as often as I could be. (Note that the `read_all_newlines()` function will take longer to run than the `read_all()` function because it has to wait until Arduino sends its final newline. It is blocking while it is waiting. This should not be a major slowdown, though.)\n", "4. The function takes an input `reader`, which specifies which function we want to use to read in the serial data. By default, we use `read_all_newlines()` because it does not have the aforementioned issues on Windows.\n", "5. I set up a dictionary that gets updated with data as it is read." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "# Set up data dictionary\n", "data = dict(time_ms=[], voltage=[])\n", "\n", "\n", "async def daq_stream_async(\n", " arduino,\n", " data,\n", " n_data=100,\n", " delay=20,\n", " n_trash_reads=5,\n", " n_reads_per_chunk=4,\n", " reader=read_all_newlines,\n", "):\n", " \"\"\"Obtain `n_data` data points from an Arduino stream\n", " with a delay of `delay` milliseconds between each.\"\"\"\n", " # Specify delay\n", " arduino.write(bytes([READ_DAQ_DELAY]) + (str(delay) + \"x\").encode())\n", "\n", " # Turn on the stream\n", " arduino.write(bytes([STREAM]))\n", "\n", " # Read and throw out first few reads\n", " i = 0\n", " while i < n_trash_reads:\n", " _ = arduino.read_until()\n", " i += 1\n", "\n", " # Receive data\n", " read_buffer = [b\"\"]\n", " while len(data[\"time_ms\"]) < n_data:\n", " # Read in chunk of data\n", " raw = reader(arduino, read_buffer=read_buffer[0], n_reads=n_reads_per_chunk)\n", "\n", " # Parse it, passing if it is gibberish\n", " try:\n", " t, V, read_buffer[0] = parse_read(raw)\n", "\n", " # Update data dictionary\n", " data[\"time_ms\"] += t\n", " data[\"voltage\"] += V\n", " except:\n", " pass\n", "\n", " # Sleep 80% of the time before we need to start reading chunks\n", " await asyncio.sleep(0.8 * n_reads_per_chunk * delay / 1000)\n", "\n", " # Turn off the stream\n", " arduino.write(bytes([ON_REQUEST]))\n", "\n", " return pd.DataFrame(\n", " {\"time (ms)\": data[\"time_ms\"][:n_data], \"voltage (V)\": data[\"voltage\"][:n_data]}\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To acquire data using this coroutine, we create a task." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "daq_task = asyncio.create_task(daq_stream_async(arduino, data, n_data=1000, delay=20))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can retrieve the data frame from the task's result and make a plot." ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "\n", "\n", "\n", "\n", "\n", "
\n" ] }, "metadata": {}, "output_type": "display_data" }, { "data": { "application/javascript": [ "(function(root) {\n", " function embed_document(root) {\n", " \n", " var docs_json = {\"1e294dcf-27e0-4e0c-8108-55cad1a9ebcb\":{\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1011\"}],\"center\":[{\"id\":\"1014\"},{\"id\":\"1018\"}],\"frame_height\":175,\"frame_width\":500,\"left\":[{\"id\":\"1015\"}],\"renderers\":[{\"id\":\"1037\"}],\"title\":{\"id\":\"1039\"},\"toolbar\":{\"id\":\"1026\"},\"x_range\":{\"id\":\"1003\"},\"x_scale\":{\"id\":\"1007\"},\"y_range\":{\"id\":\"1005\"},\"y_scale\":{\"id\":\"1009\"}},\"id\":\"1002\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{},\"id\":\"1023\",\"type\":\"ResetTool\"},{\"attributes\":{\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (sec)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"1035\",\"type\":\"Line\"},{\"attributes\":{\"overlay\":{\"id\":\"1025\"}},\"id\":\"1021\",\"type\":\"BoxZoomTool\"},{\"attributes\":{\"data_source\":{\"id\":\"1033\"},\"glyph\":{\"id\":\"1035\"},\"hover_glyph\":null,\"muted_glyph\":null,\"nonselection_glyph\":{\"id\":\"1036\"},\"selection_glyph\":null,\"view\":{\"id\":\"1038\"}},\"id\":\"1037\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"data\":{\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999],\"time (ms)\":[11722,11742,11762,11782,11802,11822,11842,11862,11882,11902,11922,11942,11962,11982,12002,12022,12042,12062,12082,12102,12122,12142,12162,12182,12203,12223,12243,12263,12283,12303,12323,12343,12363,12383,12403,12423,12443,12463,12483,12503,12523,12544,12564,12584,12604,12624,12644,12664,12684,12704,12724,12744,12764,12784,12804,12824,12844,12864,12884,12904,12924,12944,12964,12984,13004,13024,13044,13064,13084,13104,13124,13144,13164,13184,13204,13224,13244,13264,13284,13304,13324,13344,13364,13384,13404,13424,13444,13464,13484,13504,13524,13544,13564,13584,13604,13624,13644,13664,13684,13704,13724,13744,13764,13784,13804,13824,13844,13864,13884,13904,13924,13944,13964,13984,14004,14024,14044,14064,14084,14104,14124,14144,14164,14184,14204,14224,14244,14264,14284,14304,14324,14344,14364,14384,14404,14424,14444,14464,14484,14504,14524,14544,14564,14584,14604,14624,14644,14664,14684,14704,14724,14744,14764,14784,14804,14824,14844,14864,14884,14904,14924,14944,14964,14984,15004,15024,15044,15064,15084,15104,15124,15144,15164,15184,15204,15224,15244,15264,15284,15304,15324,15344,15364,15384,15404,15424,15444,15464,15484,15504,15524,15544,15564,15584,15604,15624,15644,15664,15684,15704,15724,15744,15764,15784,15804,15824,15844,15864,15884,15904,15924,15944,15964,15984,16004,16024,16044,16064,16084,16104,16124,16144,16164,16184,16204,16224,16244,16264,16284,16304,16324,16344,16364,16384,16404,16424,16444,16464,16484,16504,16524,16544,16564,16584,16604,16624,16644,16664,16684,16704,16724,16744,16764,16784,16804,16824,16844,16864,16884,16904,16924,16944,16964,16984,17004,17024,17044,17064,17084,17104,17124,17144,17164,17184,17204,17224,17244,17264,17284,17304,17324,17344,17364,17384,17404,17424,17444,17464,17484,17504,17524,17544,17564,17584,17604,17624,17644,17664,17684,17704,17724,17744,17764,17784,17804,17824,17844,17864,17884,17904,17924,17944,17964,17984,18004,18024,18044,18064,18084,18104,18124,18144,18164,18184,18204,18224,18244,18264,18284,18304,18324,18344,18364,18384,18404,18424,18444,18464,18484,18504,18524,18544,18564,18584,18604,18624,18644,18664,18684,18704,18724,18744,18764,18784,18804,18824,18844,18864,18884,18904,18924,18944,18964,18984,19004,19024,19044,19064,19084,19104,19124,19144,19164,19184,19204,19224,19244,19264,19284,19304,19324,19344,19364,19384,19404,19424,19444,19464,19484,19504,19524,19544,19564,19584,19604,19624,19644,19664,19684,19704,19724,19744,19764,19784,19804,19824,19844,19864,19884,19904,19924,19944,19964,19984,20004,20024,20044,20064,20084,20104,20124,20144,20164,20184,20204,20224,20244,20264,20284,20304,20324,20344,20364,20384,20404,20424,20444,20464,20484,20504,20524,20544,20564,20584,20604,20624,20644,20664,20684,20704,20724,20744,20764,20784,20804,20824,20844,20864,20884,20904,20924,20944,20964,20984,21004,21024,21044,21064,21084,21104,21124,21144,21164,21184,21204,21224,21244,21264,21284,21304,21324,21344,21364,21384,21404,21424,21444,21464,21484,21504,21524,21544,21564,21584,21604,21624,21644,21664,21684,21704,21724,21744,21764,21784,21804,21824,21844,21864,21884,21904,21924,21944,21964,21984,22004,22024,22044,22064,22084,22104,22124,22144,22164,22184,22204,22224,22244,22264,22284,22304,22324,22344,22364,22384,22404,22424,22444,22464,22484,22504,22524,22544,22564,22584,22604,22624,22644,22664,22684,22704,22724,22744,22764,22784,22804,22824,22844,22864,22884,22904,22924,22944,22964,22984,23004,23024,23044,23064,23084,23104,23124,23144,23164,23184,23204,23224,23244,23264,23284,23304,23324,23344,23364,23384,23404,23424,23444,23464,23484,23504,23524,23544,23564,23584,23604,23624,23644,23664,23684,23704,23724,23744,23764,23784,23804,23824,23844,23864,23884,23904,23924,23944,23964,23984,24004,24024,24044,24064,24084,24104,24124,24144,24164,24184,24204,24224,24244,24264,24284,24304,24324,24344,24364,24384,24404,24424,24444,24464,24484,24504,24524,24544,24564,24584,24604,24624,24644,24664,24684,24704,24724,24744,24764,24784,24804,24824,24844,24864,24884,24904,24924,24944,24964,24984,25004,25024,25044,25064,25084,25104,25124,25144,25164,25184,25204,25224,25244,25264,25284,25304,25324,25344,25364,25384,25404,25424,25444,25464,25484,25504,25524,25544,25564,25584,25604,25624,25644,25664,25684,25704,25724,25744,25764,25784,25804,25824,25844,25864,25884,25904,25924,25944,25964,25984,26004,26024,26044,26064,26084,26104,26124,26144,26164,26184,26204,26224,26244,26264,26284,26304,26324,26344,26364,26384,26404,26424,26444,26464,26484,26504,26524,26544,26564,26584,26604,26624,26644,26664,26684,26704,26724,26744,26764,26784,26804,26824,26844,26864,26884,26904,26924,26944,26964,26984,27004,27024,27044,27064,27084,27104,27124,27144,27164,27184,27204,27224,27244,27264,27284,27304,27324,27344,27364,27384,27404,27424,27444,27464,27484,27504,27524,27544,27564,27584,27604,27624,27644,27664,27684,27704,27724,27744,27764,27784,27804,27824,27844,27864,27884,27904,27924,27944,27964,27984,28004,28024,28044,28064,28084,28104,28124,28144,28164,28184,28204,28224,28244,28264,28284,28304,28324,28344,28364,28384,28404,28424,28444,28464,28484,28504,28524,28544,28564,28584,28604,28624,28644,28664,28684,28704,28724,28744,28764,28784,28804,28824,28844,28864,28884,28904,28924,28944,28964,28984,29004,29024,29044,29064,29084,29104,29124,29144,29164,29184,29204,29224,29244,29264,29284,29304,29324,29344,29364,29384,29404,29424,29444,29464,29484,29504,29524,29544,29564,29584,29604,29624,29644,29664,29684,29704,29724,29744,29764,29784,29804,29824,29844,29864,29884,29904,29924,29944,29964,29984,30004,30024,30044,30064,30084,30104,30124,30144,30164,30184,30204,30224,30244,30264,30284,30304,30324,30344,30364,30384,30404,30424,30444,30464,30484,30504,30524,30544,30564,30584,30604,30624,30644,30664,30684,30704,30724,30744,30764,30784,30804,30824,30844,30864,30884,30904,30924,30944,30964,30984,31004,31024,31044,31064,31084,31104,31124,31144,31164,31184,31204,31224,31244,31264,31284,31304,31324,31344,31364,31384,31404,31424,31444,31464,31484,31504,31524,31544,31564,31584,31604,31624,31644,31664,31684,31704],\"time (sec)\":{\"__ndarray__\":\"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\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[1000]},\"voltage (V)\":{\"__ndarray__\":\"77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDllVdeeeUFQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDllVdeeeUFQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQOWVV1555QVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQOWVV1555QVA77333nvvBUDllVdeeeUFQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQOWVV1555QVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQOWVV1555QVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDllVdeeeUFQO+999577wVA77333nvvBUDllVdeeeUFQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA77333nvvBUDvvffee+8FQO+999577wVA5ZVXXnnlBUDvvffee+8FQOWVV1555QVA5ZVXXnnlBUD55ZdffvkFQNFFF1100QVADjbYYIMNBkDvvffee+8FQDbWWGONNQZAxx133HHHBUByxhlnnHEGQJpmmmmmmQZAmmaaaaaZBkDgfvvtt98GQOB+++233wZArrbaaqutBkD0zjvvvPMGQMwuu+yyywZA4H777bffBkDgfvvtt98GQMwuu+yyywZACB988MEHB0DWVltttdUGQNZWW2211QZA6qabbrrpBkDCBhtssMEGQDrnnHPOOQdACB988MEHB0BON910000HQHbXXXfddQdAgP/9999/B0DaZ5999tkHQOSPP/744wdAhBBCCCGECEBqqaWWWmoJQG+66aabbgpAMcQQQwwxDEBnnXXWWWcNQLzuuuuuuw5AyieffPLJD0BxxBFHHHEQQBFFFFFEERFAiSWWWGKJEUDUUUcdddQRQAcaaKCBBhJANM4444wzEkBcbrnlllsSQGuqqaaaahJAooYaaqihEkCihhpqqKESQKKGGmqooRJAttZaa621EkCdcsopp5wSQFxuueWWWxJAAgYYYIABEkC22WabbbYRQFxxxRVXXBFA2mijjTbaEEB77LHHHnsQQOiff/755w9AlE466aSTDkCFFVZYYYUNQLnjjjvuuAtAUUIJJZRQCkB66KGHHnoIQKSOOuqoowZAO+200047BUChgw466KADQNliiy222AJA8sknn3zyAUACCSSQQAIBQBxwwAEHHABAY4wxxhhj/D9FFVVUUUX1P3755Zdffuk/Z5111lln3T9HGmmkkUbKP0gfffTRR78/xxxzzDHHvD+HG2644Ya7P0QQQQQRRMA/RRVVVFFFxT9nnXXWWWfNP+iff/75598/77zzzjvv7D8ihhhiiCH2P1977bXXXvs/n3zyySef/D9nnXXWWWf9P4ssssgii/w/Y4wxxhhj/D/rq6+++ur7P9NKK6200vo/xhdffPHF9z800UQTTTTxPwUUUEABBeQ/SB999NFHvz8FFFBAAQV0PwUUUEABBXQ/BRRQQAEFdD8FFFBAAQV0PwUUUEABBXQ/BRRQQAEFdD8GGWSQQQa5P4YWWmihhdY/5ZVXXnnl5T+ooIIKKqjwPzHFFFNMMfU/BhlkkEEG+T8LKqigggr6P1977bXXXvs/N9tss802+z+HG2644Yb7P1NNNdVUU/0/bK655ppr/j922GGHHXYAQCCBBBJIIAFAyimnnHLKAUB/+umnn34CQJ1yyimnnAJAM8sss8wyA0ALK6ywwgoDQBVTTDHFFANARxtttNFGA0AVU0wxxRQDQFFDDTXUUANAFVNMMcUUA0Apo4wyyigDQCmjjDLKKANAFVNMMcUUA0BRQw011FADQAsrrLDCCgNAM8sss8wyA0A988wzzzwDQB977LHHHgNAW2uttdZaA0D32muvvfYCQCmjjDLKKANAM8sss8wyA0APPPDAAw8EQMcdd9xxxwVA7rfffvvtB0AjiyyyyCILQHC//fbbbw9Attlmm222EUAaZ5xxxhkTQNhff/311xNA2F9//fXXE0DTSy+99NITQNNLL7300hNA00svvfTSE0DYX3/99dcTQNhff/311xNAxA8//PDDE0Bba6211loTQKyuuuqqqxJAddVVV111EUAmmGCCCSYQQA011FBDDQ1AaaONNtpoC0BRQgkllFAKQNhhhx122AlAKaKIIoooCkD22WefffYJQDPKKKOMMgpARxpppJFGCkDdcsstt9wKQNtss8022wxAqJ566qmnDkBJJJFEEkkQQLfccssttxBA6aSTTjrpEEAWWWSRRRYRQBZZZJFFFhFATTXVVFNNEUCJJZZYYokRQAIGGGCAARJAf/rpp59+EkABAwwwwAATQI0zzjjjjBNA00svvfTSE0DTSy+99NITQNNLL7300hNA00svvfTSE0DTSy+99NITQNNLL7300hNA00svvfTSE0DTSy+99NITQNhff/311xNA00svvfTSE0DTSy+99NITQNhff/311xNA00svvfTSE0DTSy+99NITQNNLL7300hNA00svvfTSE0Cwv/76668TQGWTTTbZZBNABhdccMEFE0CihhpqqKESQBFCCCGEEBJAcMEFF1xwEUCUUEIJJZQQQI433njjjQ9AnnbaaaedDkDppZdeeukNQBxuuOGGGw5A6aWXXnrpDUDBBRdccMENQOmll1566Q1AU0011VRTDUBJJZVUUkkNQNFEE0000QxAJ5xwwgknDED1008//fQLQF977bXXXgtAI4ssssgiC0BlkkkmmWQKQMQRRxxxxAlAGmmkkUYaCUA++OCDDz4IQOSPP/744wdAML/88ssvB0C43nrrrbcGQED++OOPPwZA9dRTTz31BEAttNBCCy0EQOOKK6644gJA3nnnnXfeAUAWWWSRRRYBQJhffvnll/8/0D777LPP/j8DDTTQQAP9P9dbb7311vs/l1pqqaWW+j/eeOONN974P1JIIYUUUvg//vbbb7/99j9ON9100033P9ZWW2211fY/rrbaaqut9j9ON9100033P8IGG2ywwfY/Yocddthh9z8SRxxxxBH3P+qmm2666fY/Yocddthh9z/qpptuuun2PzrnnHPOOfc//vbbb7/99j8SRxxxxBH3P3bXXXfddfc//vbbb7/99j9ON9100033PxJHHHHEEfc/EkccccQR9z9ON9100033PxJHHHHEEfc/Kqigggoq+D8GGWSQQQb5P5daaqmllvo/Y4wxxhhj/D+jjTbaaKP9P6yvvvrqq/8/5JBDDjnkAEDPOuuss84CQP/8888//wRAaJ555plnBkAcb7zxxhsHQNZWW2211QZA6qabbrrpBkDgfvvtt98GQJpmmmmmmQZAuN566623BkAEDjjggAMGQJVVVllllQVA4YQTTjjhBEDnm2+++eYDQGWTTTbZZANAQwoppJBCAkBmmWWWWWYBQIoooogiigBAIH/88ccf/z9YXnnllVf+P4ssssgii/w/n3zyySef/D9PPPHEE0/8P2OMMcYYY/w/K6200kor/T9nnXXWWWf9P/jee++99/4/CCCAAAIIAECKKKKIIooAQAwxxBBDDAFA+OCDDz74AEB66aWXXnoBQKKJJppoogFABxpooIEGAkCxwgorrLACQO2yyy677AJABRRQQAEFBEB99NFHH30EQGONNdZYYwVABA444IADBkAYXnjhhRcGQMwuu+yyywZA4H777bffBkBihx122GEHQNA///zzzwdA7rfffvvtB0CssMIKK6wIQPzwww8//AhA4oknnnjiCUCRQw455JALQK211lprrQ1A6J9//vnnD0BnnHHGGWcQQJ544oknnhBAnnjiiSeeEECeeOKJJ54QQLLIIossshBAjzzyyCOPEECZZJJJJpkQQHbYYYcddhBANdRQQw01EEAXXHDBBRcQQHC//fbbbw9ANM8888wzD0CA/vnnn38OQPPNN9988w1AmWWWWWaZDUDvvPPOO+8MQJ988sknnwxA66uvvvrqC0Bfe+21114LQKuqqqqqqgpAYIEFFlhgCUB66KGHHnoIQDC//PLLLwdADjbYYIMNBkAxxRRTTDEFQIcccsghhwRAueSSSy65BEBppJFGGmkEQIcccsghhwRAhxxyyCGHBEA33HDDDTcEQCOMMMIIIwRAySOPPPLIA0Crq6666qoDQFtrrbXWWgNAxRJLLLHEAkC76qqrrroCQBFCCCGEEAJArLHGGmusAUBSSSWVVFIBQMYYY4wxxgBAqKCCCiqoAEDon3/++ef/P4QPPvjgg/8/lE466aST/j9TTTXVVFP9P4ssssgii/w/++qrr776+j/iiSeeeOL5PxZYYIEFFvg/csYZZ5xx9j999NFHH330P3XSSSeddPI/wAEHHHDA8T+88MILL7zwP+iff/755+8/gP75559/7j+HG2644YbrP3755Zdffuk/vfXWW2+95T/88ccff/zhP3fccccdd9w/9dRTTz311D+onnrqqafOP+WVV1555cU/5JBDDjnkwD9HGmmkkUa6P8YXX3zxxbc/xhdffPHFtz/GF1988cW3PwYZZJBBBrk/xxxzzDHHvD8lkkgiiSTCP0QQQQQRRNA/N9tss8022z9lk0022WTjPx966KGHHuo/jz322GOP7T+ooIIKKqjwP1xxxRVXXPE/ddJJJ5108j9RQw011FDzP6GDDjrooPM/aaSRRhpp9D9BBBFEEEH0P3300UcfffQ/McUUU0wx9T9ZZZVVVln1P3LGGWeccfY/1lZbbbXV9j8WWGCBBRb4P7rppptuuvk/v/rqq6+++j9jjDHGGGP8P8ccc8wxx/w/U0011VRT/T+onnrqqaf+P5hffvnll/8/vPDCCy+8AEBcccUVV1wBQPLJJ5988gFAiSKKKKKIAkC76qqrrroCQIMLLrjgggNAgwsuuOCCA0Chgw466KADQNNLL7300gNAjTPOOOOMA0D766+//voDQL/77rvvvgNA00svvfTSA0Dxww8//PADQKurrrrqqgNA++uvv/76A0Crq6666qoDQMkjjzzyyANAySOPPPLIA0CNM84444wDQOebb7755gNAoYMOOuigA0C/++67774DQL/77rvvvgNAeeONN954A0DTSy+99NIDQJdbbrnllgNAq6uuuuqqA0Chgw466KADQFtrrbXWWgNAoYMOOuigA0A988wzzzwDQDPLLLPMMgNAscIKK6ywAkCsscYaa6wBQFJJJZVUUgFAOuiggw46AEDUTz/99NP/P+SOO+644/4/HG644YYb/j+87rrrrrv+PzC++OKLL/4/qJ566qmn/j/kjjvuuOP+P0gfffTRR/8/JphgggkmAECUUEIJJZQAQJhhhhlmmAFAEUIIIYQQAkDPOuuss84CQJdbbrnllgNA3XPPPffcA0DDDDPMMMMEQHfddddddwVABA444IADBkDMLrvssssGQOB+++233wZARA899NBDB0Bsr7322msHQID//ffffwdA0D///PPPB0Dut99+++0HQJhgggkmmAhAyiijjDLKCEAuueSSSy4JQOyxxx577AlAUUIJJZRQCkA322yzzTYLQMMLL7zwwgtA9dNPP/30C0BjjDHGGGMMQEUUUUQRRQxAs8wyyyyzDECzzDLLLLMMQL300ksvvQxAAw000EADDUC99NJLL70MQCGFFFJIIQ1AK6200korDUBnnXXWWWcNQP31119//Q1A/fXXX3/9DUBsrrnmmmsOQID++eeffw5AlE466aSTDkDaZpttttkOQNA+++yzzw5AFldcccUVD0Agf/zxxx8PQDTPPPPMMw9Aeuedd955D0Bwv/32228PQOiff/755w9ACCCAAAIIEEArrLDCCisQQEkkkUQSSRBATjjhhBNOEECPPPLII48QQJ544oknnhBA1VRTTTXVEED99NNPP/0QQAIJJJBAAhFAV1111VVXEUCOOeaYY44RQLvttttuuxFABxpooIEGEkA54ogjjjgSQJ1yyimnnBJA3nbbbbfdEkAkjzzyyCMTQFtrrbXWWhNAgwsuuOCCE0Crq6666qoTQM4333zzzRNA00svvfTSE0DON998880TQM4333zzzRNA00svvfTSE0DTSy+99NITQM4333zzzRNAzjfffPPNE0DTSy+99NITQNNLL7300hNA00svvfTSE0DTSy+99NITQNNLL7300hNA00svvfTSE0DTSy+99NITQNNLL7300hNA00svvfTSE0DTSy+99NITQNNLL7300hNA00svvfTSE0DTSy+99NITQNhff/311xNA00svvfTSE0CNM84444wTQDjffPPNNxNAwP7666+/EkCJIooooogSQEMKKaSQQhJAIH744YcfEkAMLrjgggsSQNlll1122RFAu+222267EUBhhRVWWGERQAIJJJBAAhFAt9xyyy23EEBiiCGGGGIQQBxwwAEHHBBAmF9++eWXD0AML7zwwgsPQAIHHHDAAQ9AlE466aSTDkCyxhprrLEOQE422WSTTQ5AHG644YYbDkAcbrjhhhsOQLfddttttw1AwQUXXHDBDUBJJZVUUkkNQCuttNJKKw1A77zzzjvvDECffPLJJ58MQJ988sknnwxAO+ywww47DEAJJJBAAgkMQM0zzzzzzAtAN9tss802C0Bfe+21114LQPvqq6+++gpA8cILL7zwCkDxwgsvvPAKQKuqqqqqqgpA55prrrnmCkBvuummm24KQIMKKqigggpAW2qppZZaCkALKqigggoKQBVSSCGFFApAdNFFF110CUAGGWSQQQYJQJhgggkmmAhADDDAAAMMCEAWWGCBBRYIQJRPPvnkkwdAnnfeeeedB0Bihx122GEHQDrnnHPOOQdAWF999dVXB0DqpptuuukGQPTOO++88wZAfO655557BkBUTjnllFMGQDbWWGONNQZAgQUWWGCBBUB33XXXXXcFQOGEE0444QRAueSSSy65BECRRBJJJJEEQBlkkEEGGQRAQQQRRBBBBEDTSy+99NIDQKurrrrqqgNAeeONN954A0DPOuuss84CQNliiy222AJABxpooIEGAkDeeeedd94BQD755JNPPgFAdthhhx12AEBYYIEFFlgAQFxvvfXWW/8/hA8++OCD/z/43nvvvff+PxxuuOGGG/4/gP75559//j+jjTbaaKP9P0QOOeSQQ/4/880333zz/T/LLbfccsv9P4D++eeff/4/CB544IEH/j8ML7zwwgv/P7zuuuuuu/4/SB999NFH/z8wwAADDDAAQCaYYIIJJgBAKqmkkkoqAUBwwQUXXHABQPLJJ5988gFAV1pppZVWAkBNMskkk0wCQFFDDTXUUANAeeONN954A0DJI4888sgDQPHDDz/88ANAoYMOOuigA0AjjDDCCCMEQG+77bbbbgNAW2uttdZaA0DZYostttgCQBtqqKGGGgJA1FFHHXXUAUDkkEMOOeQAQLzwwgsvvABAWGCBBRZYAED877///vv/P1hggQUWWABA6J9//vnn/z9YYIEFFlgAQE444YQTTgBAWGCBBRZYAED44IMPPvgAQD755JNPPgFAa6qppppqAkA988wzzzwDQEssscQSSwRAMcUUU0wxBUAnnXTSSScFQIEFFlhggQVATz311FNPBUBjjTXWWGMFQIsttthiiwVAWWWVVVZZBUCfffbZZ58FQGONNdZYYwVAbbXVVlttBUCBBRZYYIEFQE899dRTTwVAs80222yzBUBPPfXUU08FQJ999tlnnwVAn3322WefBUBjjTXWWGMFQMcdd9xxxwVAY4011lhjBUCzzTbbbLMFQKmlllpqqQVAd9111113BUDHHXfccccFQGONNdZYYwVAn3322WefBUCVVVZZZZUFQGONNdZYYwVAlVVWWWWVBUDXXHPNNdcEQM0000wzzQRAX3zxxRdfBEAPPPDAAw8EQMkjjzzyyANAUUMNNdRQA0D32muvvfYCQHXSSSeddAJABxpooIEGAkDUUUcdddQBQFJJJZVUUgFAXHHFFVdcAUDkkEMOOeQAQKigggoqqABAbLDBBhtsAEDA//7777//P6yvvvrqq/8/vO666667/j9EDjnkkEP+P7fddtttt/0/iyyyyCKL/D9jjDHGGGP8PzfbbLPNNvs/l1pqqaWW+j8feuihhx76Py655JJLLvk/VllllVVW+T+22GKLLbb4P7bYYosttvg/Zphhhhlm+D/aZ5999tn3P7LHHnvssfc/mmaaaaaZ9j+aZpppppn2P+WVV1555fU/CSWUUEIJ9T+55JJLLrn0P7XTTjvttPM/tdNOO+208z/FEkssscTyPxFCCCGEEPI/mGGGGWaY8T9EEEEEEUTwP0QQQQQRRPA/SB999NFH7z9YXnnllVfuP4D++eeff+4/77zzzjvv7D/HHHPMMcfsPw877LDDDus/H3rooYce6j/eeOONN97oPzbWWGONNeY/LbTQQgst5D9NMskkk0ziPzTRRBNNNOE/HHDAAQcc4D8IHnjggQfeP1heeeWVV94/11tvvfXW2z8322yzzTbbP4cbbrjhhts/l1pqqaWW2j8nnHDCCSfcP4cbbrjhhts/d9xxxx133D8XXXTRRRfdP1heeeWVV94/XHHFFVdc4T+NM84444zjP2aYYYYZZug/F1100UUX7T800UQTTTTxP91zzz333PM/IoYYYogh9j+yxx577LH3Py655JJLLvk/GmmkkUYa+T8LKqigggr6P+KJJ5544vk/b7rppptu+j+/+uqrr776P0srrbTSSvs/iyyyyCKL/D+ffPLJJ5/8P99999133/0/vO666667/j9wv/3222//P4AAAggggABADDHEEEMMAUDKKaeccsoBQMUSSyyxxAJAq6uuuuqqA0CBBRZYYIEFQDbWWGONNQZArrbaaqutBkB87rnnnnsGQFROOeWUUwZArrbaaqutBkBonnnmmWcGQKSOOuqoowZArrbaaqutBkCaZpppppkGQNZWW2211QZArrbaaqutBkD+9ttvv/0GQOB+++233wZArrbaaqutBkCuttpqq60GQLPNNttsswVAhxxyyCGHBED88ccff/wBQLfddtttt/0/H3rooYce+j8CCCCAAAL4P7bYYosttvg/Pvjggw8++D9SSCGFFFL4P8ooo4wyyvg/Kqigggoq+D8uueSSSy75P1ZZZZVVVvk/W2qppZZa+j+ba6655pr7P8MLL7zwwvs/F1100UUX/T8IHnjggQf+PwwvvPDCC/8/HHDAAQccAEBssMEGG2wAQNpoo4022gBAAgkkkEACAUCYYYYZZpgBQBtqqKGGGgJAf/rpp59+AkALK6ywwgoDQKGDDjrooANAkUQSSSSRBEDvvffee+8FQDC//PLLLwdABhlkkEEGCUA98sgjjzwKQLnjjjvuuAtA0UQTTTTRDEAXXXTRRRcNQF111VVXXQ1ADTXUUEMNDUBdddVVV10NQEkllVRSSQ1AF1100UUXDUA//fTTTz8NQNFEE0000QxADTXUUEMNDUDRRBNNNNEMQIEEEkgggQxAiyyyyCKLDEDrq6+++uoLQKWTTjrppAtAGWOMMcYYC0CNMsooo4wKQFtqqaWWWgpA7LHHHnvsCUALKqigggoKQJxxxhlnnAlATDHFFFNMCUAkkUQSSSQJQKKIIooooghAhBBCCCGECEDkjz/++OMHQHbXXXfddQdAJpdccsklB0CQPvroo48GQK622mqrrQZAVE455ZRTBkA=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[1000]}},\"selected\":{\"id\":\"1046\"},\"selection_policy\":{\"id\":\"1045\"}},\"id\":\"1033\",\"type\":\"ColumnDataSource\"},{\"attributes\":{\"axis\":{\"id\":\"1015\"},\"dimension\":1,\"ticker\":null},\"id\":\"1018\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1020\",\"type\":\"WheelZoomTool\"},{\"attributes\":{},\"id\":\"1022\",\"type\":\"SaveTool\"},{\"attributes\":{\"axis_label\":\"time (s)\",\"formatter\":{\"id\":\"1043\"},\"ticker\":{\"id\":\"1012\"}},\"id\":\"1011\",\"type\":\"LinearAxis\"},{\"attributes\":{},\"id\":\"1019\",\"type\":\"PanTool\"},{\"attributes\":{},\"id\":\"1045\",\"type\":\"UnionRenderers\"},{\"attributes\":{},\"id\":\"1046\",\"type\":\"Selection\"},{\"attributes\":{\"active_drag\":\"auto\",\"active_inspect\":\"auto\",\"active_multi\":null,\"active_scroll\":\"auto\",\"active_tap\":\"auto\",\"tools\":[{\"id\":\"1019\"},{\"id\":\"1020\"},{\"id\":\"1021\"},{\"id\":\"1022\"},{\"id\":\"1023\"},{\"id\":\"1024\"}]},\"id\":\"1026\",\"type\":\"Toolbar\"},{\"attributes\":{\"end\":31.704,\"start\":11.722},\"id\":\"1003\",\"type\":\"Range1d\"},{\"attributes\":{},\"id\":\"1024\",\"type\":\"HelpTool\"},{\"attributes\":{},\"id\":\"1041\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"axis\":{\"id\":\"1011\"},\"ticker\":null},\"id\":\"1014\",\"type\":\"Grid\"},{\"attributes\":{\"axis_label\":\"voltage (V)\",\"formatter\":{\"id\":\"1041\"},\"ticker\":{\"id\":\"1016\"}},\"id\":\"1015\",\"type\":\"LinearAxis\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (sec)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"1036\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"1012\",\"type\":\"BasicTicker\"},{\"attributes\":{\"source\":{\"id\":\"1033\"}},\"id\":\"1038\",\"type\":\"CDSView\"},{\"attributes\":{},\"id\":\"1043\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{},\"id\":\"1016\",\"type\":\"BasicTicker\"},{\"attributes\":{\"text\":\"\"},\"id\":\"1039\",\"type\":\"Title\"},{\"attributes\":{},\"id\":\"1007\",\"type\":\"LinearScale\"},{\"attributes\":{},\"id\":\"1005\",\"type\":\"DataRange1d\"},{\"attributes\":{\"bottom_units\":\"screen\",\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"top_units\":\"screen\"},\"id\":\"1025\",\"type\":\"BoxAnnotation\"},{\"attributes\":{},\"id\":\"1009\",\"type\":\"LinearScale\"}],\"root_ids\":[\"1002\"]},\"title\":\"Bokeh Application\",\"version\":\"2.2.1\"}};\n", " var render_items = [{\"docid\":\"1e294dcf-27e0-4e0c-8108-55cad1a9ebcb\",\"root_ids\":[\"1002\"],\"roots\":{\"1002\":\"0549c1e5-d5b6-41e4-9f51-85b7415ee03d\"}}];\n", " root.Bokeh.embed.embed_items_notebook(docs_json, render_items);\n", "\n", " }\n", " if (root.Bokeh !== undefined) {\n", " embed_document(root);\n", " } else {\n", " var attempts = 0;\n", " var timer = setInterval(function(root) {\n", " if (root.Bokeh !== undefined) {\n", " clearInterval(timer);\n", " embed_document(root);\n", " } else {\n", " attempts++;\n", " if (attempts > 100) {\n", " clearInterval(timer);\n", " console.log(\"Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing\");\n", " }\n", " }\n", " }, 10, root)\n", " }\n", "})(window);" ], "application/vnd.bokehjs_exec.v0+json": "" }, "metadata": { "application/vnd.bokehjs_exec.v0+json": { "id": "1002" } }, "output_type": "display_data" } ], "source": [ "# Get the data from from the result\n", "df = daq_task.result()\n", "\n", "# Convert milliseconds to seconds\n", "df['time (sec)'] = df['time (ms)'] / 1000\n", "\n", "# Plot!\n", "p = bokeh.plotting.figure(\n", " x_axis_label='time (s)',\n", " y_axis_label='voltage (V)',\n", " frame_height=175,\n", " frame_width=500,\n", " x_range=[df['time (sec)'].min(), df['time (sec)'].max()],\n", ")\n", "p.line(source=df, x='time (sec)', y='voltage (V)')\n", "\n", "bokeh.io.show(p)" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [], "source": [ "arduino.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computing environment" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPython 3.8.5\n", "IPython 7.18.1\n", "\n", "numpy 1.19.1\n", "pandas 1.1.3\n", "serial 3.4\n", "bokeh 2.2.1\n", "jupyterlab 2.2.6\n" ] } ], "source": [ "%load_ext watermark\n", "%watermark -v -p numpy,pandas,serial,bokeh,jupyterlab" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.8" } }, "nbformat": 4, "nbformat_minor": 4 }