{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 15. Streaming plots with Bokeh\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()\n", "\n", "notebook_url = \"localhost:8888\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "The setup for this lesson is the same as the previous one. We also have an ever-growing list of utility functions. If you want to skip to the section after the setup, execute the code cell below that contains the utility function and then [click here](#Bokeh-apps)." ] }, { "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\n", "\n", "\n", "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\n", "\n", "\n", "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\n", "\n", "\n", "def parse_read(read):\n", " \"\"\"Parse a read with time, volage 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()\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": [ "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": [ "## Bokeh apps\n", "\n", "*Though this section is not a follow-along exercise, you should run it on your machine so you can see the dynamics.*\n", "\n", "Thus far, we have used Bokeh to make zoom-able, pan-able, save-able JavaScript-based plots of data. We also used it to make widgets that we could use to control Arduino. But [Bokeh](http://bokeh.pydata.org/) allows much more interactivity. We can update plots based on results of calculation by the Python interpreter. In our case, we want to update a plot of voltages coming off of the Arduino board in real time.\n", "\n", "As an example of how a Bokeh app can be used to update a plot I build one below to dynamically plot a random walk. The walk will proceed with a dot doing the walk and the trail behind it represented as a line. To build a Bokeh app, we need to write a function that controls the app. The function for the random walker is shown below with an explanation following immediately." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "def random_walk(doc):\n", " \"\"\"Bokeh app for a dynamic random walk of 1000 steps.\"\"\"\n", " rg = np.random.default_rng(3252)\n", "\n", " p = bokeh.plotting.figure(\n", " frame_width=200,\n", " frame_height=200,\n", " x_range=[-20, 20],\n", " y_range=[-20, 20],\n", " )\n", "\n", " # Use ColumnDataSources for data for populating glyphs\n", " source_line = bokeh.models.ColumnDataSource({\"x\": [0], \"y\": [0]})\n", " source_dot = bokeh.models.ColumnDataSource({\"x\": [0], \"y\": [0]})\n", " line = p.line(source=source_line, x=\"x\", y=\"y\")\n", " dot = p.circle(source=source_dot, x=\"x\", y=\"y\", color=\"tomato\", size=7)\n", "\n", " @bokeh.driving.linear()\n", " def update(step):\n", " if step > 1000:\n", " doc.remove_periodic_callback(pc)\n", " else:\n", " theta = rg.uniform(0, 2 * np.pi)\n", " new_position = {\n", " \"x\": [source_dot.data[\"x\"][0] + np.cos(theta)],\n", " \"y\": [source_dot.data[\"y\"][0] + np.sin(theta)],\n", " }\n", " source_line.stream(new_position)\n", " source_dot.data = new_position\n", "\n", " doc.add_root(p)\n", "\n", " # Add a periodic callback to be run every 20 milliseconds\n", " pc = doc.add_periodic_callback(update, 20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The argument of the function (traditionally called doc) is accessed to add any plots (or other Bokeh) to app and to add the callbacks. We first set up the figure. Next, we set up the data sources for the dot and line using Bokeh's `ColumnDataSource`. This data type may be dynamically updated in a Bokeh app, which is exactly what we want. After the data sources are set up, we set up an update function (we call it `update()` here, but it could have any name). This function is called each time a **callback** is triggered. At the end of the function defining the app, we add a periodic callback that calls the update function every 20 milliseconds. (Note that unlike `time.sleep()` and `asyncio.sleep()`, the time units for Bokeh's periodic callbacks are millseconds.) In this case, we decorate the callback function with `@bokeh.driving.linear()`. This results in the argument of `update()`, `step`, being advanced by one every time the function is called. This way we can keep track of how many steps were taken. In the update function, if we have exceeded the number of desired steps, we cancel the periodic callbacks. Otherwise, we compute the next step of the random walk by computing a random angle for the step. We update the position of the walker by adding the step to it. Finally, we update the data sources for the dot and line. For the line, we use the `stream()` method. This results in Bokeh only appending new data to the data source instead of pushing through the whole data set for the plot each time. As the size of the data set being plotted grows, this give much better performance. For the dot, since it is only plotted as a single position, we update the source data to be the dot position.\n", "\n", "To run our app, we use `bokeh.io.show()`. We should also include the URL of the notebook (specified above in the input cell; you can see the URL by looking at the top of your browser).\n", "\n", "**Note**: Bokeh apps, relying on a Python engine to run, do not render in the static HTML version (i.e., on the course website) of this lesson. So, if you are reading this on the website, you will not see the plot below." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/vnd.bokehjs_exec.v0+json": "", "text/html": [ "\n", "" ] }, "metadata": { "application/vnd.bokehjs_exec.v0+json": { "server_id": "e5b72648b4ab4315898682c03027ea7d" } }, "output_type": "display_data" } ], "source": [ "bokeh.io.show(random_walk, notebook_url=notebook_url)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Follow-along exercise 11: Plotting streaming data\n", "\n", "We will expand on the work we did in the last lesson to acquire data asynchronously and push the data to a Bokeh plot for updating. To start with, as usual, we need to shake hands with Arduino." ] }, { "cell_type": "code", "execution_count": 5, "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", "port = find_arduino()\n", "arduino = serial.Serial(port, baudrate=115200)\n", "handshake_arduino(arduino, print_handshake_message=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Our strategy for building our app is this:\n", "\n", "- Set up a dictionary containing lists of data\n", "- Asynchronously collect data from Arduino that updates the data dictionary\n", "- Set up a periodic callback so Bokeh updates the plot from the data dictionary\n", "\n", "To do this, we need to keep track of which data are included on the plot and which are new. Therefore, the data dictionary also contains a variable to remember how long the time point and voltage lists were the last time the plot was rendered." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "# Set up data dictionary\n", "data = dict(prev_array_length=0, time_ms=[], voltage=[])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we build the plotting app. Because the app must have a call signature `app(doc)`, I like to write a function that returns an app. This allows me to have a more convenient API for specifying properties of the app. This app is essentially like the random walk app, except that we pull data out of the data dictionary as needed. We also have a **rollover** parameter, which specifies the maximum number of data points to be displayed on the plot. Only the most recent data points are displayed. For time series data, like we're plotting here, this results in a \"scroll\" across the plot, kind of like a stock ticker.\n", "\n", "I have also included a keyword argument for the delay between plot updates. If the delay is too short, your computer will struggle trying to render the Bokeh plot at a high rate. In my experience, plots that are updated every 100 ms or less look like essentially continuous updates to the eye, so I use a plot delay of 90 ms." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "def potentiometer_app(data, n_data=100, rollover=400, plot_update_delay=90):\n", " \"\"\"Return a function defining a Bokeh app for streaming\n", " data up to `n_data` data points. A maximum of `rollover`\n", " data points are shown at a time.\n", " \"\"\"\n", " def _app(doc):\n", " # Instatiate figures\n", " p = bokeh.plotting.figure(\n", " frame_width=500,\n", " frame_height=175,\n", " x_axis_label=\"time (s)\",\n", " y_axis_label=\"voltage (V)\",\n", " y_range=[-0.2, 5.2],\n", " )\n", "\n", " # No padding on x_range makes data flush with end of plot\n", " p.x_range.range_padding = 0\n", "\n", " # Start with an empty column data source with time and voltage\n", " source = bokeh.models.ColumnDataSource({\"t\": [], \"V\": []})\n", "\n", " # Put a line glyph\n", " r = p.line(source=source, x=\"t\", y=\"V\")\n", "\n", " @bokeh.driving.linear()\n", " def update(step):\n", " # Shut off periodic callback if we have plotted all of the data\n", " if step > n_data:\n", " doc.remove_periodic_callback(pc)\n", " else:\n", " # Update plot by streaming in data\n", " source.stream(\n", " {\n", " \"t\": list(np.array(data['time_ms'][data['prev_array_length']:]) / 1000),\n", " \"V\": data['voltage'][data['prev_array_length']:],\n", " },\n", " rollover,\n", " )\n", " data['prev_array_length'] = len(data['time_ms'])\n", "\n", " doc.add_root(p)\n", " pc = doc.add_periodic_callback(update, plot_update_delay)\n", " \n", " \n", " return _app" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, to put the app to use! We need to show the app, and then create a task to acquire the data. The plot is then updated live! (Note that this is not viewable in the static HTML version of this lesson.)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "application/vnd.bokehjs_exec.v0+json": "", "text/html": [ "\n", "" ] }, "metadata": { "application/vnd.bokehjs_exec.v0+json": { "server_id": "d6e1b360368046f4b91b8291e819ba5a" } }, "output_type": "display_data" } ], "source": [ "n_data = 1000\n", "\n", "bokeh.io.show(potentiometer_app(data, n_data=n_data), notebook_url=notebook_url)\n", "daq_task = asyncio.create_task(daq_stream_async(arduino, data, n_data=n_data, delay=20))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Although only the last 400 data points are visible on the live-updated plot, we still have all the data available and can retrieve them to the task." ] }, { "cell_type": "code", "execution_count": 9, "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 = {\"80e4b975-cc90-4c13-b8f9-fd8c306a0d63\":{\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"3638\"}],\"center\":[{\"id\":\"3641\"},{\"id\":\"3645\"}],\"frame_height\":175,\"frame_width\":500,\"left\":[{\"id\":\"3642\"}],\"renderers\":[{\"id\":\"3664\"}],\"title\":{\"id\":\"3666\"},\"toolbar\":{\"id\":\"3653\"},\"x_range\":{\"id\":\"3630\"},\"x_scale\":{\"id\":\"3634\"},\"y_range\":{\"id\":\"3632\"},\"y_scale\":{\"id\":\"3636\"}},\"id\":\"3629\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"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)\":[8693,8713,8733,8753,8773,8793,8813,8833,8853,8873,8893,8913,8933,8953,8973,8993,9013,9033,9053,9073,9093,9113,9133,9153,9174,9194,9214,9234,9254,9274,9294,9314,9334,9354,9374,9394,9414,9434,9454,9474,9494,9515,9535,9555,9575,9595,9615,9635,9655,9675,9695,9715,9735,9755,9775,9795,9815,9835,9856,9876,9896,9916,9936,9956,9976,9996,10016,10036,10056,10076,10096,10116,10136,10156,10176,10196,10216,10236,10256,10276,10296,10316,10336,10356,10376,10396,10416,10436,10456,10476,10496,10516,10536,10556,10576,10596,10616,10636,10656,10676,10696,10716,10736,10756,10776,10796,10816,10836,10856,10876,10896,10916,10936,10956,10976,10996,11016,11036,11056,11076,11096,11116,11136,11156,11176,11196,11216,11236,11256,11276,11296,11316,11336,11356,11376,11396,11416,11436,11456,11476,11496,11516,11536,11556,11576,11596,11616,11636,11656,11676,11696,11716,11736,11756,11776,11796,11816,11836,11856,11876,11896,11916,11936,11956,11976,11996,12016,12036,12056,12076,12096,12116,12136,12156,12176,12196,12216,12236,12256,12276,12296,12316,12336,12356,12376,12396,12416,12436,12456,12476,12496,12516,12536,12556,12576,12596,12616,12636,12656,12676,12696,12716,12736,12756,12776,12796,12816,12836,12856,12876,12896,12916,12936,12956,12976,12996,13016,13036,13056,13076,13096,13116,13136,13156,13176,13196,13216,13236,13256,13276,13296,13316,13336,13356,13376,13396,13416,13436,13456,13476,13496,13516,13536,13556,13576,13596,13616,13636,13656,13676,13696,13716,13736,13756,13776,13796,13816,13836,13856,13876,13896,13916,13936,13956,13976,13996,14016,14036,14056,14076,14096,14116,14136,14156,14176,14196,14216,14236,14256,14276,14296,14316,14336,14356,14376,14396,14416,14436,14456,14476,14496,14516,14536,14556,14576,14596,14616,14636,14656,14676,14696,14716,14736,14756,14776,14796,14816,14836,14856,14876,14896,14916,14936,14956,14976,14996,15016,15036,15056,15076,15096,15116,15136,15156,15176,15196,15216,15236,15256,15276,15296,15316,15336,15356,15376,15396,15416,15436,15456,15476,15496,15516,15536,15556,15576,15596,15616,15636,15656,15676,15696,15716,15736,15756,15776,15796,15816,15836,15856,15876,15896,15916,15936,15956,15976,15996,16016,16036,16056,16076,16096,16116,16136,16156,16176,16196,16216,16236,16256,16276,16296,16316,16336,16356,16376,16396,16416,16436,16456,16476,16496,16516,16536,16556,16576,16596,16616,16636,16656,16676,16696,16716,16736,16756,16776,16796,16816,16836,16856,16876,16896,16916,16936,16956,16976,16996,17016,17036,17056,17076,17096,17116,17136,17156,17176,17196,17216,17236,17256,17276,17296,17316,17336,17356,17376,17396,17416,17436,17456,17476,17496,17516,17536,17556,17576,17596,17616,17636,17656,17676,17696,17716,17736,17756,17776,17796,17816,17836,17856,17876,17896,17916,17936,17956,17976,17996,18016,18036,18056,18076,18096,18116,18136,18156,18176,18196,18216,18236,18256,18276,18296,18316,18336,18356,18376,18396,18416,18436,18456,18476,18496,18516,18536,18556,18576,18596,18616,18636,18656,18676,18696,18716,18736,18756,18776,18796,18816,18836,18856,18876,18896,18916,18936,18956,18976,18996,19016,19036,19056,19076,19096,19116,19136,19156,19176,19196,19216,19236,19256,19276,19296,19316,19336,19356,19376,19396,19416,19436,19456,19476,19496,19516,19536,19556,19576,19596,19616,19636,19656,19676,19696,19716,19736,19756,19776,19796,19816,19836,19856,19876,19896,19916,19936,19956,19976,19996,20016,20036,20056,20076,20096,20116,20136,20156,20176,20196,20216,20236,20256,20276,20296,20316,20336,20356,20376,20396,20416,20436,20456,20476,20496,20516,20536,20556,20576,20596,20616,20636,20656,20676,20696,20716,20736,20756,20776,20796,20816,20836,20856,20876,20896,20916,20936,20956,20976,20996,21016,21036,21056,21076,21096,21116,21136,21156,21176,21196,21216,21236,21256,21276,21296,21316,21336,21356,21376,21396,21416,21436,21456,21476,21496,21516,21536,21556,21576,21596,21616,21636,21656,21676,21696,21716,21736,21756,21776,21796,21816,21836,21856,21876,21896,21916,21936,21956,21976,21996,22016,22036,22056,22076,22096,22116,22136,22156,22176,22196,22216,22236,22256,22276,22296,22316,22336,22356,22376,22396,22416,22436,22456,22476,22496,22516,22536,22556,22576,22596,22616,22636,22656,22676,22696,22716,22736,22756,22776,22796,22816,22836,22856,22876,22896,22916,22936,22956,22976,22996,23016,23036,23056,23076,23096,23116,23136,23156,23176,23196,23216,23236,23256,23276,23296,23316,23336,23356,23376,23396,23416,23436,23456,23476,23496,23516,23536,23556,23576,23596,23616,23636,23656,23676,23696,23716,23736,23756,23776,23796,23816,23836,23856,23876,23896,23916,23936,23956,23976,23996,24016,24036,24056,24076,24096,24116,24136,24156,24176,24196,24216,24236,24256,24276,24296,24316,24336,24356,24376,24396,24416,24436,24456,24476,24496,24516,24536,24556,24576,24596,24616,24636,24656,24676,24696,24716,24736,24756,24776,24796,24816,24836,24856,24876,24896,24916,24936,24956,24976,24996,25016,25036,25056,25076,25096,25116,25136,25156,25176,25196,25216,25236,25256,25276,25296,25316,25336,25356,25376,25396,25416,25436,25456,25476,25496,25516,25536,25556,25576,25596,25616,25636,25656,25676,25696,25716,25736,25756,25776,25796,25816,25836,25856,25876,25896,25916,25936,25956,25976,25996,26016,26036,26056,26076,26096,26116,26136,26156,26176,26196,26216,26236,26256,26276,26296,26316,26336,26356,26376,26396,26416,26436,26456,26476,26496,26516,26536,26556,26576,26596,26616,26636,26656,26676,26696,26716,26736,26756,26776,26796,26816,26836,26856,26876,26896,26916,26936,26956,26976,26996,27016,27036,27056,27076,27096,27116,27136,27156,27176,27196,27216,27236,27256,27276,27296,27316,27336,27356,27376,27396,27416,27436,27456,27476,27496,27516,27536,27556,27576,27596,27616,27636,27656,27676,27696,27716,27736,27756,27776,27796,27816,27836,27856,27876,27896,27916,27936,27956,27976,27996,28016,28036,28056,28076,28096,28116,28136,28156,28176,28196,28216,28236,28256,28276,28296,28316,28336,28356,28376,28396,28416,28436,28456,28476,28496,28516,28536,28556,28576,28596,28616,28636,28656,28676],\"time (s)\":{\"__ndarray__\":\"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\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[1000]},\"voltage (V)\":{\"__ndarray__\":\"RxpppJFGCkAfeuihhx4KQDPKKKOMMgpAKaKIIoooCkAzyiijjDIKQEcaaaSRRgpAH3rooYceCkA98sgjjzwKQB966KGHHgpAPfLII488CkD22WefffYJQCmiiCKKKApA4oknnnjiCUD22WefffYJQLrppptuuglAuummm266CUCcccYZZ5wJQKaZZppppglAkkkmmWSSCUCIIYYYYogJQJxxxhlnnAlAfvnll19+CUCcccYZZ5wJQDjhhBNOOAlAQgkllFBCCUDyyCOPPPIIQAYZZJBBBglArLDCCiusCECssMIKK6wIQEgggQQSSAhANNBAAw00CEDGF1988cUHQJRPPvnkkwdAEkccccQRB0CkjjrqqKMGQBheeOGFFwZAn3322WefBUBZZZVVVlkFQPXUU0899QRA9dRTTz31BECvvPLKK68EQOGEE0444QRApZRSSimlBEC55JJLLrkEQHPMMccccwRAkUQSSSSRBEBffPHFF18EQFVUUUUVVQRA55tvvvnmA0AZZJBBBhkEQNNLL7300gNAv/vuu+++A0B544033ngDQGWTTTbZZANAPfPMM888A0Dtsssuu+wCQM8666yzzgJAQwoppJBCAkAbaqihhhoCQMABBxxwwAFAmGGGGWaYAUAqqaSSSioBQAwxxBBDDAFAiiiiiCKKAECAAAIIIIAAQOiff/755/8/mF9++eWX/z+onnrqqaf+P6ieeuqpp/4/HG644YYb/j+33Xbbbbf9Pz/99NNPP/0/n3zyySef/D8TTDDBBBP8P1977bXXXvs/55prrrnm+j/OOeecc875P5JJJplkkvk/Kqigggoq+D+KJ5544on3PzbWWGONNfY/0UUXXXTR9T/hhBNOOOH0P80000wzzfQ/kUQSSSSR9D8dddRRRx31P0UVVVRRRfU/gQUWWGCB9T9KJplkkkn2P+qmm2666fY/tthiiy22+D+DCiqooIL6P3vttddee/0/IH/88ccf/z+KKKKIIooAQNBAAw000ABA1FFHHXXUAUBhggkmmGACQEcbbbTRRgNAq6uuuuqqA0BLLLHEEksEQMMMM8wwwwRAE0000UQTBUCppZZaaqkFQO+999577wVAuN566623BkD+9ttvv/0GQLzvvvvuuwdA+N9///33B0DKKKOMMsoIQCSRRBJJJAlAFVJIIYUUCkCNMsooo4wKQF977bXXXgtA//vvv//+C0DllFNOOeUMQHHFFVdccQ1A880333zzDUDGFltsscUOQHC//fbbbw9AOuiggw46EECttNJKK60QQAwxxBBDDBFAG2200UYbEUA++eSTTz4RQCWVVFJJJRFAJZVUUkklEUAHHXTQQQcRQPjggw8++BBArbTSSiutEEBJJJFEEkkQQKKHHnrooQ9ARA455JBDDkBxxRVXXHENQFlkkUUWWQxAzTPPPPPMC0DxwgsvvPAKQI0yyiijjApAzjnnnHPOCUBCCSWUUEIJQFJIIYUUUghA7rfffvvtB0Aml1xyySUHQKSOOuqoowZABA444IADBkBttdVWW20FQOuss8466wRAc8wxxxxzBEB99NFHH30EQEssscQSSwRAm2yyySabBECvvPLKK68EQDHFFFNMMQVAn3322WefBUD0zjvvvPMGQCqooIIKKghAKaKIIoooCkBzyy233HILQMccc8wxxwxAe+211157DUAcbrjhhhsOQJ522mmnnQ5AvO666667DkDutttuu+0OQKieeuqppw5AnnbaaaedDkDppZdeeukNQMccc8wxxwxASyuttNJKC0AzyiijjDIKQBBBBBFEEAlAtthiiy22CEAMMMAAAwwIQKiffvrppwdAbK+99tprB0AIH3zwwQcHQLjeeuuttwZAhhZaaKGFBkCuttpqq60GQHzuueeeewZAwgYbbLDBBkDMLrvssssGQID//ffffwdA7rfffvvtB0CYYIIJJpgIQNRQQw011AhAdNFFF110CUDssccee+wJQGWSSSaZZApAv/rqq6++CkCHG2644YYLQDHEEEMMMQxA77zzzjvvDECFFVZYYYUNQN9999133w1AWF555ZVXDkCKJppoookOQNA+++yzzw5AnnbaaaedDkCyxhprrLEOQGKGGWaYYQ5AwQUXXHDBDUCLLLLIIosMQGmjjTbaaAtAW2qppZZaCkAuueSSSy4JQHDAAQcccAhAsscee+yxB0CKJ5544okHQDrnnHPOOQdARA899NBDB0AIH3zwwQcHQDC//PLLLwdAML/88ssvB0CKJ5544okHQID//ffffwdA7rfffvvtB0BmmGGGGWYIQPLII4888ghApplmmmmmCUAzyiijjDIKQI0yyiijjApA3XLLLbfcCkAZY4wxxhgLQAUTTDDBBAtA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cMEFF1xwAUCsr7766qv/P6ONNtpoo/0/q6qqqqqq+j8feuihhx76P3755Zdffvk/9tlnn332+T+mmWaaaab5P/bZZ5999vk/l1pqqaWW+j9jjDHGGGP8P1heeeWVV/4/Ouiggw46AECiiSaaaKIBQMUSSyyxxAJAffTRRx99BECuttpqq60GQJJJJplkkglAvfTSSy+9DEBsrrnmmmsOQCqnnHLKKQ9AcL/99ttvD0CEDz744IMPQHrnnXfeeQ9AhA8++OCDD0Bml1122WUPQHC//fbbbw9ASB999NFHD0AWV1xxxRUPQAgeeOCBBw5AK6200korDUDNM88888wLQJdaaqmllgpAaqmlllpqCUD433///fcHQP7222+//QZAvfXWW2+9BUCHHHLIIYcEQLHCCiussAJAjjnmmGOOAUAmmGCCCSYAQNA+++yzz/4/F1100UUX/T//+++///77P9NKK6200vo/uummm266+T8CCCCAAAL4P6mlllpqqfU/7bLLLrvs8j9YXnnllVfuPw422GCDDeY/+N5777333j/llVdeeeXVP7XTTjvttNM/VVRRRRVV1D+100477bTTP1VUUUUVVdQ/BRRQQAEF1D+llFJKKaXUP6WUUkoppdQ/pZRSSiml1D+llFJKKaXUP6WUUkoppdQ/1lZbbbXV1j9211133XXXP/bZZ5999tk/55prrrnm2j9IH3300UffP1xxxRVXXOE/BRRQQAEF5D821lhjjTXmP2aYYYYZZug/b7rppptu6j/nmmuuuebqP9dbb7311us/J5xwwgkn7D8//fTTTz/tPz/99NNPP+0/WF555ZVX7j8gf/zxxx/vP0QQQQQRRPA/bLDBBhts8D8MMcQQQwzxP7HCCiussPI/oYMOOuig8z/11FNPPfX0P5VVVllllfU/hhZaaKGF9j821lhjjTX2P4YWWmihhfY/NtZYY4019j9yxhlnnHH2P0ommWSSSfY/SiaZZJJJ9j8ihhhiiCH2P9FFF1100fU/qaWWWmqp9T8ttNBCCy30P8USSyyxxPI/JZJIIokk8j910kknnXTyP00yySSTTPI/ddJJJ5108j+JIooooojyP3njjTfeePM/kUQSSSSR9D+99dZbb731P9ZWW2211fY/EkccccQR9z/GF1988cX3P5533nnnnfc/Kqigggoq+D/ut99+++33P2aYYYYZZvg/Kqigggoq+D+KJ5544on3P/nll19++fU/1FFHHXXU8T/QPvvss8/uP1977bXXXus/hxtuuOGG6z+HG2644YbrP8ccc8wxx+w/+N5777337j+xwgorrLDyP/HDDz/88PM/VVRRRRVV9D8ttNBCCy30P40zzjjjjPM/FVNMMcUU8z9wwQUXXHDxPzDAAAMMMPA/WF555ZVX7j+A/vnnn3/uP4D++eeff+4/wP/++++/7z9EEEEEEUTwP/zxxx9//PE/KaOMMsoo8z8JJZRQQgn1P6622mqrrfY/nnfeeeed9z/KKKOMMsr4PwYZZJBBBvk/M8ooo4wy+j+HG2644Yb7P9tss8022/w/33333Xff/T8ML7zwwgv/P8D//vvvv/8/Ouiggw46AEDGGGOMMcYAQCqppJJKKgFATTLJJJNMAkBRQw011FADQJFEEkkkkQRACSWUUEIJBUC99dZbb70FQO+999577wVASiaZZJJJBkAihhhiiCEGQDbWWGONNQZAGF544YUXBkAihhhiiCEGQDbWWGONNQZAGF544YUXBkA21lhjjTUGQPnll19++QVASiaZZJJJBkAONthggw0GQEommWSSSQZA+eWXX375BUAYXnjhhRcGQNttt9122wVA+eWXX375BUDHHXfccccFQHfddddddwVA9dRTTz31BECXW2655ZYDQEMKKaSQQgJAlFBCCSWUAEAwwAADDDAAQDTPPPPMM/8/cL/99ttv/z8gf/zxxx//P3C//fbbb/8/SB999NFH/z8SSCCBBBIAQIAAAggggABAXHHFFVdcAUB10kknnXQCQBVTTDHFFANABRRQQAEFBECbbLLJJpsEQHfddddddwVAqaWWWmqpBUBedtlll10GQIYWWmihhQZAOuecc845B0Ced955550HQDTQQAMNNAhArLDCCiusCEBqqaWWWmoJQD3yyCOPPApAv/rqq6++CkC544477rgLQDHEEEMMMQxA+eSTTz75DEAhhRRSSCENQI899thjjw1Ajz322GOPDUCttdZaa60NQKONNtpoow1Ao4022mijDUCttdZaa60NQJlllllmmQ1ArbXWWmutDUDLLbfccssNQBxuuOGGGw5ARA455JBDDkAgf/zxxx8PQJhffvnllw9ANdRQQw01EEBiiCGGGGIQQIUUUkghhRBAlFBCCSWUEECZZJJJJpkQQKOMMsoooxBAlFBCCSWUEECjjDLKKKMQQI888sgjjxBAo4wyyiijEECUUEIJJZQQQJlkkkkmmRBAgAACCCCAEECKKKKIIooQQGKIIYYYYhBAP/zwww8/EEAIIIAAAggQQMD//vvvvw9AXG+99dZbD0DQPvvss88OQID++eeffw5A6aWXXnrpDUCZZZZZZpkNQAMNNNBAAw1As8wyyyyzDEAxxBBDDDEMQOGDDz744AtAr7vuuuuuC0Bzyy233HILQGmjjTbaaAtASyuttNJKC0Bzyy233HILQC2zzDLLLAtAaaONNtpoC0AjiyyyyCILQDfbbLPNNgtAGWOMMcYYC0Ats8wyyywLQC2zzDLLLAtALbPMMsssC0A322yzzTYLQPvqq6+++gpABRNMMMEEC0Ddcsstt9wKQCOLLLLIIgtA8cILL7zwCkAZY4wxxhgLQPvqq6+++gpAI4ssssgiC0AZY4wxxhgLQCOLLLLIIgtAI4ssssgiC0AZY4wxxhgLQDfbbLPNNgtADzvssMMOC0BBAw000EALQA877LDDDgtAQQMNNNBAC0AZY4wxxhgLQC2zzDLLLAtADzvssMMOC0AZY4wxxhgLQA877LDDDgtABRNMMMEEC0DnmmuuueYKQGWSSSaZZApACyqooIIKCkAuueSSSy4JQI444ogjjghAlE8++eSTB0DCBhtssMEGQIsttthiiwVAm2yyySabBECrq6666qoDQLHCCiussAJA3nnnnXfeAUAggQQSSCABQNpoo4022gBAgAACCCCAAECooIIKKqgAQIAAAggggABAvPDCCy+8AECyyCKLLLIAQAwxxBBDDAFArLHGGmusAUB10kknnXQCQG+77bbbbgNAkUQSSSSRBEDbbbfddtsFQP7222+//QZAAggggAACCEBSSCGFFFIIQNRQQw011AhAGmmkkUYaCUABAggggAAKQKGCCiqooApA9dNPP/30C0BdddVVV10NQKieeuqppw5ArK+++uqrD0AhhBBCCCEQQGywwQYbbBBAhRRSSCGFEECZZJJJJpkQQIUUUkghhRBAiiiiiCKKEEBssMEGG2wQQE444YQTThBASB999NFHD0ASRhhhhBEOQDHEEEMMMQxAb7rppptuCkDAAAMMMMAIQCaXXHLJJQdASiaZZJJJBkAxxRRTTDEFQJtssskmmwRAl1tuueWWA0ALK6ywwgoDQHXSSSeddAJAddJJJ510AkBrqqmmmmoCQGGCCSaYYAJABxpooIEGAkDuuOOOO+4AQKieeuqpp/4/CyqooIIK+j8ihhhiiCH2P40zzjjjjPM/jTPOOOOM8z8po4wyyijzP3njjTfeePM/UUMNNdRQ8z+hgw466KDzP8kjjzzyyPM/VVRRRRVV9D/llVdeeeX1PzrnnHPOOfc/aqmlllpq+T/76quvvvr6PwMNNNBAA/0/qJ566qmn/j8ccMABBxwAQIoooogiigBAUkkllVRSAUDyySeffPIBQLHCCiussAJAUUMNNdRQA0DJI4888sgDQEssscQSSwRASyyxxBJLBEBzzDHHHHMEQC200EILLQRASyyxxBJLBEC100477bQDQAsrrLDCCgNA6KGHHnroAUACCSSQQAIBQOiff/755/8/DC+88MIL/z+UTjrppJP+P5ROOumkk/4/vO666667/j+A/vnnn3/+P/jee++99/4/5I477rjj/j8IIIAAAggAQJRQQgkllABA1FFHHXXUAUDjiiuuuOICQLnkkksuuQRABA444IADBkBihx122GEHQDTQQAMNNAhAJJFEEkkkCUBqqaWWWmoJQEwxxRRTTAlAnHHGGWecCUBggQUWWGAJQJxxxhlnnAlAfvnll19+CUCSSSaZZJIJQGqppZZaaglAmGCCCSaYCEA++OCDDz4IQID//ffffwdAzC677LLLBkDRRRdddNEFQEUVVVRRRQVAueSSSy65BEC55JJLLrkEQIcccsghhwRAzTTTTDPNBECvvPLKK68EQOGEE0444QRA9dRTTz31BECfffbZZ58FQLjeeuuttwZAOuecc845B0BIIIEEEkgIQMAAAwwwwAhAVllllVVWCUBCCSWUUEIJQDPKKKOMMgpAeeKJJ554CkDxwgsvvPAKQKGCCiqooApA3XLLLbfcCkCrqqqqqqoKQNNKK6200gpAySKLLLLICkBvuummm24KQB966KGHHgpAYIEFFlhgCUCiiCKKKKIIQLzvvvvuuwdAdtddd911B0Acb7zxxhsHQOB+++233wZAQP74448/BkAihhhiiCEGQOWVV1555QVA0UUXXXTRBUAONthggw0GQAQOOOCAAwZAcsYZZ5xxBkC43nrrrbcGQGKHHXbYYQdAzC677LLLBkAaaaSRRhoJQIMKKqigggpAX3vttddeC0Bzyy233HILQP/777///gtAZ5111llnDUAwvvjiiy8OQPjee++99w5ASB999NFHD0B655133nkPQGaXXXbZZQ9AjjfeeOOND0Bml1122WUPQEgfffTRRw9AgP75559/DkB77bXXXnsNQEUUUUQRRQxAjTLKKKOMCkALKqigggoKQEgggQQSSAhAgP/9999/B0Cuttpqq60GQCKGGGKIIQZAMcUUU0wxBUCllFJKKaUEQNNLL7300gNACyussMIKA0BXWmmllVYCQD755JNPPgFAnnjiiSeeAED877///vv/P8D//vvvv/8/qJ566qmn/j+onnrqqaf+P6ONNtpoo/0/P/30008//T9jjDHGGGP8P3PLLbfccvs/v/rqq6+++j9baqmlllr6P5daaqmllvo/W2qppZZa+j9LK6200kr7PzfbbLPNNvs/22yzzTbb/D/LLbfccsv9P9RPP/300/8/0EADDTTQAEDooYceeugBQEcbbbTRRgNAm2yyySabBECLLbbYYosFQHfddddddwVAiy222GKLBUBFFVVUUUUFQE899dRTTwVA11xzzTXXBEAjjDDCCCMEQIkiiiiiiAJAPvnkk08+AUAccMABBxwAQKieeuqpp/4/ML744osv/j+33Xbbbbf9PwgeeOCBB/4/jz322GOP/T8IHnjggQf+P6ONNtpoo/0/HG644YYb/j8wvvjiiy/+PyB//PHHH/8/bLDBBhtsAEB66aWXXnoBQMUSSyyxxAJAm2yyySabBECuttpqq60GQAgffPDBBwdA3njjjTfeCEAaaaSRRhoJQKaZZppppglAkkkmmWSSCUCcccYZZ5wJQNhhhx122AlA4oknnnjiCUBvuummm24KQBVSSCGFFApAgwoqqKCCCkBMMcUUU0wJQHDAAQcccAhA6qabbrrpBkDbbbfddtsFQM0000wzzQRAaaSRRhppBEBBBBFEEEEEQC200EILLQRASyyxxBJLBEBLLLHEEksEQMMMM8wwwwRATz311FNPBUA=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[1000]}},\"selected\":{\"id\":\"3673\"},\"selection_policy\":{\"id\":\"3672\"}},\"id\":\"3660\",\"type\":\"ColumnDataSource\"},{\"attributes\":{},\"id\":\"3643\",\"type\":\"BasicTicker\"},{\"attributes\":{\"axis\":{\"id\":\"3642\"},\"dimension\":1,\"ticker\":null},\"id\":\"3645\",\"type\":\"Grid\"},{\"attributes\":{\"range_padding\":0},\"id\":\"3630\",\"type\":\"DataRange1d\"},{\"attributes\":{\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (s)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"3662\",\"type\":\"Line\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (s)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"3663\",\"type\":\"Line\"},{\"attributes\":{},\"id\":\"3672\",\"type\":\"UnionRenderers\"},{\"attributes\":{},\"id\":\"3634\",\"type\":\"LinearScale\"},{\"attributes\":{\"source\":{\"id\":\"3660\"}},\"id\":\"3665\",\"type\":\"CDSView\"},{\"attributes\":{},\"id\":\"3673\",\"type\":\"Selection\"},{\"attributes\":{},\"id\":\"3651\",\"type\":\"HelpTool\"},{\"attributes\":{\"active_drag\":\"auto\",\"active_inspect\":\"auto\",\"active_multi\":null,\"active_scroll\":\"auto\",\"active_tap\":\"auto\",\"tools\":[{\"id\":\"3646\"},{\"id\":\"3647\"},{\"id\":\"3648\"},{\"id\":\"3649\"},{\"id\":\"3650\"},{\"id\":\"3651\"}]},\"id\":\"3653\",\"type\":\"Toolbar\"},{\"attributes\":{\"overlay\":{\"id\":\"3652\"}},\"id\":\"3648\",\"type\":\"BoxZoomTool\"},{\"attributes\":{},\"id\":\"3646\",\"type\":\"PanTool\"},{\"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\":\"3652\",\"type\":\"BoxAnnotation\"},{\"attributes\":{},\"id\":\"3669\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{},\"id\":\"3636\",\"type\":\"LinearScale\"},{\"attributes\":{},\"id\":\"3650\",\"type\":\"ResetTool\"},{\"attributes\":{\"end\":5.2,\"start\":-0.2},\"id\":\"3632\",\"type\":\"Range1d\"},{\"attributes\":{\"data_source\":{\"id\":\"3660\"},\"glyph\":{\"id\":\"3662\"},\"hover_glyph\":null,\"muted_glyph\":null,\"nonselection_glyph\":{\"id\":\"3663\"},\"selection_glyph\":null,\"view\":{\"id\":\"3665\"}},\"id\":\"3664\",\"type\":\"GlyphRenderer\"},{\"attributes\":{},\"id\":\"3639\",\"type\":\"BasicTicker\"},{\"attributes\":{\"axis_label\":\"time (s)\",\"formatter\":{\"id\":\"3671\"},\"ticker\":{\"id\":\"3639\"}},\"id\":\"3638\",\"type\":\"LinearAxis\"},{\"attributes\":{},\"id\":\"3649\",\"type\":\"SaveTool\"},{\"attributes\":{\"axis_label\":\"voltage (V)\",\"formatter\":{\"id\":\"3669\"},\"ticker\":{\"id\":\"3643\"}},\"id\":\"3642\",\"type\":\"LinearAxis\"},{\"attributes\":{\"text\":\"\"},\"id\":\"3666\",\"type\":\"Title\"},{\"attributes\":{\"axis\":{\"id\":\"3638\"},\"ticker\":null},\"id\":\"3641\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"3647\",\"type\":\"WheelZoomTool\"},{\"attributes\":{},\"id\":\"3671\",\"type\":\"BasicTickFormatter\"}],\"root_ids\":[\"3629\"]},\"title\":\"Bokeh Application\",\"version\":\"2.2.1\"}};\n", " var render_items = [{\"docid\":\"80e4b975-cc90-4c13-b8f9-fd8c306a0d63\",\"root_ids\":[\"3629\"],\"roots\":{\"3629\":\"a532c146-33a1-4994-8a74-bb2e31ab4a69\"}}];\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": "3629" } }, "output_type": "display_data" } ], "source": [ "# Retrieve data from the task\n", "df = daq_task.result()\n", "\n", "# Convert time to seconds\n", "df[\"time (s)\"] = df[\"time (ms)\"] / 1000\n", "\n", "# Plot the full time-voltage trace\n", "p = bokeh.plotting.figure(\n", " frame_width=500,\n", " frame_height=175,\n", " x_axis_label=\"time (s)\",\n", " y_axis_label=\"voltage (V)\",\n", " y_range=[-0.2, 5.2],\n", ")\n", "\n", "p.x_range.range_padding = 0\n", "\n", "p.line(source=df, x=\"time (s)\", y=\"voltage (V)\")\n", "\n", "bokeh.io.show(p)" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "arduino.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Do-it-yourself exercise 6: Etch-A-Sketch\n", "\n", "An [Etch-A-Sketch](https://en.wikipedia.org/wiki/Etch_A_Sketch) is a classic toy wherein a child (or adult!) turns knobs to draw lines. To get an idea of how it works, check out [this video](https://www.youtube.com/watch?v=q4CTyWwQrMo).\n", "\n", "In this exercise, make an Etch-A-Sketch by using two potentiometers as the \"knobs\". The voltages measured from analog inputs are then sent to Python and interpreted as positions in an x-y plane. You should have a Bokeh plot that gets updated as the knobs are turned.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computing environment" ] }, { "cell_type": "code", "execution_count": 11, "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 }