{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lesson 28: Control of external devices\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", " const 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", " const JS_MIME_TYPE = 'application/javascript';\n", " const HTML_MIME_TYPE = 'text/html';\n", " const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n", " const CLASS_NAME = 'output_bokeh rendered_html';\n", "\n", " /**\n", " * Render data to the DOM node\n", " */\n", " function render(props, node) {\n", " const 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", " const cell = handle.cell;\n", "\n", " const id = cell.output_area._bokeh_element_id;\n", " const 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", " const cmd_clean = \"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_clean, {\n", " iopub: {\n", " output: function(msg) {\n", " const 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", " const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n", " cell.notebook.kernel.execute(cmd_destroy);\n", " }\n", " }\n", "\n", " /**\n", " * Handle when a new output is added\n", " */\n", " function handleAddOutput(event, handle) {\n", " const output_area = handle.output_area;\n", " const output = handle.output;\n", "\n", " // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n", " if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n", " return\n", " }\n", "\n", " const 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", " const bk_div = document.createElement(\"div\");\n", " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", " const script_attrs = bk_div.children[0].attributes;\n", " for (let 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", " const 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", " const 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", " const events = require('base/js/events');\n", " const 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", " const 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", " const el = document.getElementById(\"1002\");\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(url) {\n", " console.error(\"failed to load \" + url);\n", " }\n", "\n", " for (let i = 0; i < css_urls.length; i++) {\n", " const url = css_urls[i];\n", " const element = document.createElement(\"link\");\n", " element.onload = on_load;\n", " element.onerror = on_error.bind(null, url);\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", " for (let i = 0; i < js_urls.length; i++) {\n", " const url = js_urls[i];\n", " const element = document.createElement('script');\n", " element.onload = on_load;\n", " element.onerror = on_error.bind(null, url);\n", " element.async = false;\n", " element.src = url;\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", " const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-2.4.2.min.js\"];\n", " const css_urls = [];\n", " \n", "\n", " const 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 (let 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", " const cell = $(document.getElementById(\"1002\")).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 const 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 const 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 const el = document.getElementById(\"1002\");\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(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\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 for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\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 const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.2.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-2.4.2.min.js\"];\n const css_urls = [];\n \n\n const 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 (let 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 const cell = $(document.getElementById(\"1002\")).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 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 iqplot\n", "\n", "import bokeh.layouts\n", "import bokeh.models\n", "import bokeh.io\n", "\n", "notebook_url = \"localhost:8888\"\n", "bokeh.io.output_notebook()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "In this lesson, we introduce control of external devices using a serial connection. The important package for this purpose is [PySerial](https://pythonhosted.org/pyserial/), which you can install if you haven't already using\n", "\n", " conda install pyserial\n", " \n", "It enables communication with devices over a serial port, often a USB port. When you import it, you import it by the name `serial`.\n", "\n", "*Because this lesson connects to a physical device, it really must be run in a live Jupyter notebook with a device connected.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Arduino\n", "\n", "Of course to do this lesson, we should have an actual device with which to communicate. Therefore, I encourage you to actually build the device below and connect it to your computer and work through this lesson running a live Jupyter notebook.\n", "\n", "For our device in this example, we will use an [Arduino Uno board](https://store.arduino.cc/usa/arduino-uno-rev3). These ≈$20 boards offer great functionality and are fun to just play with. Beyond, that, they are actually quite useful in research applications and have widespread use in research labs across disciplines, including in the biological sciences. I encourage you to check out [this tour Arduino](https://be189.github.io/lessons/02/tour_of_arduino.html) to familiarize yourself with the board.\n", "\n", "We will use our board with a simple circuit, shown below.\n", "\n", "
\n", "\n", "![Arduino](arduino_setup.svg)\n", " \n", "
\n", "\n", "At the left of the Arduino Uno board in the drawing is a USB-A port, which you should connect to your computer. The breadboard features a potentiometer, whose output is connected to analog input pin A0, and two LEDs that are controlled by digital pins 2 and 6. The LEDs are connected to ground via 220 Ω resistors. We will demonstrate how to communicate with Arduino via Python by reading voltages from pin A0 and by turning the LEDs on and off.\n", "\n", "The Arduino board is loaded with compiled code written in a flavor of C++. The code for this circuit is shown below. The code basically tells Arduino which pins do what, and then listens for signals coming from its serial connection. Based on what signal it gets, it will either send the measurement of voltage out the serial connection or turn the LEDs on or off. (We will not go over how to code Arduino in this bootcamp, but rather focus on how to interface with an external device using Python. You can check out [this introduction on programming Arduino](https://be189.github.io/lessons/05/programming_arduino.html).)\n", "\n", "```C++\n", "const int voltagePin = A0;\n", "\n", "const int redLEDPin = 6;\n", "const int yellowLEDPin = 2;\n", "\n", "const int HANDSHAKE = 0;\n", "const int VOLTAGE_REQUEST = 1;\n", "const int RED_LED_ON = 2;\n", "const int RED_LED_OFF = 3;\n", "const int YELLOW_LED_ON = 4;\n", "const int YELLOW_LED_OFF = 5;\n", "\n", "const int ON_REQUEST = 6;\n", "const int STREAM = 7;\n", "const int READ_DAQ_DELAY = 8;\n", "\n", "String daqDelayStr;\n", "\n", "int inByte = 0;\n", "int daqMode = ON_REQUEST;\n", "int daqDelay = 100; // delay between acquisitions in milliseconds\n", "\n", "int value;\n", "unsigned long time_ms;\n", "\n", "\n", "void printVoltage() {\n", " // read value from analog pin\n", " value = analogRead(voltagePin);\n", " time_ms = millis();\n", " \n", " // Write the result\n", " if (Serial.availableForWrite()) {\n", " String outstr = String(String(time_ms, DEC) + \",\" + String(value, DEC));\n", " Serial.println(outstr);\n", " }\n", "}\n", "\n", "\n", "void setup() {\n", " // Set LEDs to off\n", " pinMode(redLEDPin, OUTPUT);\n", " pinMode(yellowLEDPin, OUTPUT);\n", " digitalWrite(redLEDPin, LOW);\n", " digitalWrite(yellowLEDPin, LOW);\n", "\n", " // initialize serial communication\n", " Serial.begin(115200);\n", "}\n", "\n", "\n", "void loop() {\n", " // If we're auto-transferring data (streaming mode)\n", " if (daqMode == STREAM) {\n", " printVoltage();\n", " delay(daqDelay); \n", " }\n", "\n", " // Check if data has been sent to Arduino and respond accordingly\n", " if (Serial.available() > 0) {\n", " // Read in request\n", " inByte = Serial.read();\n", " \n", " // Handshake\n", " if (inByte == HANDSHAKE){\n", " if (Serial.availableForWrite()) {\n", " Serial.println(\"Handshake message received.\");\n", " }\n", " }\n", " \n", " // If data is requested, fetch it and write it\n", " else if (inByte == VOLTAGE_REQUEST) printVoltage();\n", "\n", " // Switch daqMode\n", " else if (inByte == ON_REQUEST) daqMode = ON_REQUEST;\n", " else if (inByte == STREAM) daqMode = STREAM;\n", "\n", " // Read in DAQ delay\n", " else if (inByte == READ_DAQ_DELAY) {\n", " while (Serial.available() == 0) ;\n", " daqDelayStr = Serial.readStringUntil('x');\n", " daqDelay = daqDelayStr.toInt();\n", " }\n", "\n", " // else, turn LEDs on or off\n", " else if (inByte == RED_LED_ON) digitalWrite(redLEDPin, HIGH);\n", " else if (inByte == RED_LED_OFF) digitalWrite(redLEDPin, LOW);\n", " else if (inByte == YELLOW_LED_ON) digitalWrite(yellowLEDPin, HIGH);\n", " else if (inByte == YELLOW_LED_OFF) digitalWrite(yellowLEDPin, LOW);\n", " }\n", "}\n", "```\n", "\n", "According to this code, if we send integers over the serial connection to Arduino, it will behave according to the table below.\n", "\n", "|signal to Arduino | action|\n", "|-----|-----|\n", "|0|Handshake (establish and check connection with Arduino)|\n", "|1|Send voltage from port A0|\n", "|2|Turn red LED on|\n", "|3|Turn red LED off|\n", "|4|Turn yellow LED on|\n", "|5|Turn yellow LED off|\n", "|6|Toggle data acquisition mode to on request|\n", "|7|Toggle data acquisition mode to stream|\n", "|8|Alert Arduino that the next input is streaming delay|\n", "\n", "For the rest of this lesson, we will assume you have an Arduino connected to your machine configured as in the photo above, with the above Arduino code loaded in.\n", "\n", "For convenience, we can declare variables matching these integer codes for this notebook." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "HANDSHAKE = 0\n", "VOLTAGE_REQUEST = 1\n", "RED_LED_ON = 2\n", "RED_LED_OFF = 3\n", "YELLOW_LED_ON = 4\n", "YELLOW_LED_OFF = 5\n", "ON_REQUEST = 6\n", "STREAM = 7\n", "READ_DAQ_DELAY = 8" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Arduino will send data back to us in one of two modes, either on demand or as a stream. For on-demand data, it waits until it gets a signal from Python asking for data and then sends the time stamp and the voltage measured at analog input A0. For streaming data, it automatically sends data with a specified delay between data points." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Finding the port\n", "\n", "With your Arduino plugged into your computer via a USB connection, you first need to find out which port it is. The names of the ports will differ based on your operating system and when you plugged it in (your OS may assign the ports different names). You can get a list of ports using the `serial.tools.list_ports.comports()` function." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ,\n", " ]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ports = serial.tools.list_ports.comports()\n", "\n", "# Take a look\n", "ports" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On my machine, there are two ports open. We can look at the manufacturer of the devices attached to the ports to find Arduino." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[None, None, 'Arduino (www.arduino.cc)']" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[port.manufacturer for port in ports]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Clearly, the third port is Arduino. We can get then get a string for the port associated with the device." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'/dev/cu.usbmodem14101'" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ports[2].device" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On Windows, the manufacturer might not appear as Arduino. In this case, you should take a look at the devices for each port." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['/dev/cu.BLTH', '/dev/cu.Bluetooth-Incoming-Port', '/dev/cu.usbmodem14101']" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[port.device for port in ports]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The appropriate port for Windows will be something like `'COM3'`.\n", "\n", "For convenience, we can write a function to find Arduino. Called without arguments, it will give the port for Arduino if the manufacturer comes up as Arduino in the query. Otherwise, you can provide a string (like `'COM7'`) for the port, and it will connect to that port." ] }, { "cell_type": "code", "execution_count": 7, "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" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We’ll use it to get the port for Arduino. (Note that you may need to explicitly give the port, especially if you are using Windows.)" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "port = find_arduino()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Doing nothing\n", "\n", "Before we begin working with Arduino, we are going to need a function to tell the Python interpreter to wait and do nothing. This is usually referred to as **sleeping**. There is a function `time.sleep()` that accomplishes this, but it is not the most accurate timing available. The function `time.perf_counter()` provides the more accurate timing. We therefore write our own sleep function. According to [the documentation](https://docs.python.org/3.9/library/time.html#time.perf_counter), \"the reference point of the returned value is undefined, so that only the difference between the results of consecutive calls is valid.\" So, we call `time.perf_counter()` at the beginning of sleep and store the result. We then continuously call it until the difference between the return value of a call and the stored value exceeds the desired sleep time.\n", "\n", "To maintain consistency with Python's other timing function, we will require the duration to be in units of second, even though this is inconsistent with Arduino's units of milliseconds." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def sleep(duration):\n", " \"\"\"Sleep for `duration` seconds.\"\"\"\n", " now = time.perf_counter()\n", " end = now + duration\n", "\n", " while now < end:\n", " now = time.perf_counter()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Opening a connection\n", "\n", "When opening a connection with Python, you cannot have the Serial Monitor nor Serial Plotter of the Arduino IDE open, since they will keep the port busy and Python cannot communicate with Arduino.\n", "\n", "To open a connection to the device, we instantiate a `serial.Serial` instance. When a port is first opened, there is some handshaking between the device and the computer that needs to happen. To be safe, I always close the port and reopen it to get an open port, and then wait one second using `sleep()` before I send or receive data from it. I then send and receive data packets. The first input/output from Arduino Uno board is nonsense (unique to that board, I think). I then send and receive a handshake message again to make sure everything is working properly." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Handshake message: Handshake message received.\n", "\n" ] } ], "source": [ "# Open port\n", "arduino = serial.Serial(port, baudrate=115200, timeout=1)\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", " 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", "# Call the handshake function\n", "handshake_arduino(arduino, print_handshake_message=True, handshake_code=HANDSHAKE)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A few comments about the above code, bearing in mind that the Python variable `arduino` is a `serial.Serial` instance.\n", "\n", "- The `arduino.timeout` attribute sets the maximum time in seconds to wait for serial communication.\n", "- To handshake, we need to send code `0` to the Arduino. It must be sent as **bytes**, machine numbers Arduino can understand. To convert an integer to bytes in Python, we use the built-in `bytes()` function. It accepts an iterable (like a list of tuple) of `int`s and converts them to bytes. So, the signal we would send for code `0` is `bytes([0])`.\n", "- The `arduino.read_all()` function reads all bytes that are in the input buffer on the Python side.\n", "- The `arduino.read_until()` function reads from the input buffer on the Python side until a newline character is encountered. This is convenient because the Python interpreter moves along way faster than Arduino can perform calculations and write data over USB. By asking Python to read until it hits a newline character, it has to wait until the complete message is sent by Arduino.\n", "- The message sent from Arduino is a `bytesarray`. To convert it to a string, use the `decode()` method, as we did with `handshake_message.decode()` in the above function.\n", "\n", "The port is currently open, and I'm not going to anything with it now, so I am going to close it. This is very important: *Make sure you close your serial connection when you are done with it.*" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "arduino.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When possible, it is good practice to instead use context management when opening a serial connection. That way, it is always guaranteed to close, even when things may go awry." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "with serial.Serial(port, baudrate=115200, timeout=1) as arduino:\n", " handshake_arduino(arduino)\n", " \n", " # And the rest of what you want to do follows...." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## \"Hello, world,\" a.k.a. turning on an LED\n", "\n", "The \"Hello, world\" of electronic circuits is turning on an LED. To turn on the red LED, we need to send code `2` to the Arduino. It must be sent as bytes, machine numbers Arduino can understand. So, the signal we would send for code `2` is:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "b'\\x02'" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "bytes([2])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's open up a port to Arduino and send a signal to turn on the red LED. " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "with serial.Serial(port, baudrate=115200, timeout=1) as arduino:\n", " handshake_arduino(arduino)\n", " \n", " # Turn on the red LED\n", " arduino.write(bytes([RED_LED_ON]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we know how to turn an LED on (and off), we can make a little disco party!" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "with serial.Serial(port, baudrate=115200, timeout=1) as arduino:\n", " handshake_arduino(arduino)\n", " \n", " # Flash the LEDs\n", " for _ in range(40):\n", " arduino.write(bytes([RED_LED_ON]))\n", " sleep(0.05)\n", " arduino.write(bytes([YELLOW_LED_ON]))\n", " sleep(0.05)\n", " arduino.write(bytes([RED_LED_OFF]))\n", " sleep(0.05)\n", " arduino.write(bytes([YELLOW_LED_OFF]))\n", " sleep(0.05)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Controlling Arduino with widgets\n", "\n", "While calling a function to send and retrieve data from an external device (in this case our Arduino board) is useful, it is more convenient to enable control of the device using widgets.\n", "\n", "To do so, we need to open the connection to the Arduino and leave it open (outside of context management). Since we will want to open and close these ports manually, including having all of the (un)pleasantries of connecting and sending and receiving the first few test packets of data, we will write a function to give us an open serial connection to the Arduino (knowing of course that we will need to close it)." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "def open_arduino(port, baudrate=115200, timeout=1):\n", " \"\"\"Open a connection with an Arduino device and\n", " handshake to get ready for use.\"\"\"\n", " # Open port\n", " arduino = serial.Serial(port, baudrate=baudrate, timeout=timeout)\n", "\n", " # Close and reopen\n", " arduino.close()\n", " arduino.open()\n", "\n", " handshake_arduino(arduino)\n", " \n", " return arduino" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's go ahead and open the connection." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "arduino = open_arduino(port)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Widgets and callbacks\n", "\n", "As a first step in setting up control of the device, we will make toggle buttons to turn the red and yellow LEDs on and off. We can make the buttons using `bokeh.models.Toggle()`. We can build a small Bokeh app to allow turning the lights on and off." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "def LED_app(doc):\n", " \"\"\"Make a toggle for turning LEDs on and off\"\"\"\n", "\n", " def red_callback(attr, old, new):\n", " if new:\n", " arduino.write(bytes([RED_LED_ON]))\n", " else:\n", " arduino.write(bytes([RED_LED_OFF]))\n", "\n", " def yellow_callback(attr, old, new):\n", " if new:\n", " arduino.write(bytes([YELLOW_LED_ON]))\n", " else:\n", " arduino.write(bytes([YELLOW_LED_OFF]))\n", "\n", " # Set up toggles\n", " red_LED_toggle = bokeh.models.Toggle(\n", " label=\"Red LED\", button_type='danger', width=100,\n", " )\n", " yellow_LED_toggle = bokeh.models.Toggle(\n", " label=\"Yellow LED\", button_type=\"warning\", width=100,\n", " )\n", "\n", " # Link callbacks\n", " red_LED_toggle.on_change(\"active\", red_callback)\n", " yellow_LED_toggle.on_change(\"active\", yellow_callback)\n", "\n", " # Lay out the toggles\n", " layout = bokeh.layouts.row(\n", " red_LED_toggle, bokeh.layouts.Spacer(width=15), yellow_LED_toggle\n", " )\n", "\n", " doc.add_root(layout)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Some comments:\n", "\n", "- We use the `on_change()` method of a toggle widget. The callback function for on-change behavior must have call signature `callback(attr, old, new)`, where `attr` is an attribute of the widget, `old` is the pre-change value of that attribute, and `new` is the post-change value of the attribute.\n", "- We instantiate a toggle with `bokeh.models.Toggle`. Instead of `title` like we would use with sliders, we use the kwarg `label` for a toggle.\n", "- A toggle has an `active` attribute, which is `True` when the toggle is on and `False` when off. Whenever that value changes, the callback is triggered.\n", "- We use `button_type=\"danger\"` to give us a red button and `button_type=\"warning\"` to give us an orange-ish button to approximate yellow.\n", "\n", "To view the widget in the notebook, use `bokeh.io.show()`. Note that we called `bokeh.io.output_notebook()` earlier in this notebook, which means that the app will display in the notebook. We also defined the `notebook_url`, which can be found by looking in your browser’s address bar. In my case, the `notebook_url` is `\"localhost:8888\"`. Note also that Bokeh apps will not be displayed in the static HTML rendering of this notebook, so if you are reading this from the course website, you will see no output from the cell below." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "application/vnd.bokehjs_exec.v0+json": "", "text/html": [ "\n", "" ] }, "metadata": { "application/vnd.bokehjs_exec.v0+json": { "server_id": "d54fe51d56fb471ba9886bfe21ab9a93" } }, "output_type": "display_data" } ], "source": [ "bokeh.io.show(LED_app, notebook_url=notebook_url)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The LEDs on the Arduino can then be toggled using those two buttons.\n", "\n", "As we shift gears to retrieving data from Arduino, we will keep the port open." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Retrieving data from Arduino\n", "\n", "So far, we have used our serial connection to toggle LEDs on and off, but ultimately, we would like to receive data from the device. To do this, we can send data requests and then read what comes back.\n", "\n", "Based on the above Arduino code, when we are in ON_REQUEST mode, Ardunio waits for a request for data to come through its serial connection to the computer. Upon receipt of a request, it sends data back. The time and voltage data come in as a [byte string](https://docs.python.org/3/library/stdtypes.html#bytes) (Python data type `bytes`) containing the time and voltage separated by a comma. The byte string ends with both a carriage return (`\\r`) and a newline (`\\n`). To convert the byte string to numbers, we need to strip the carriage return and newline and decode the byte string into the Python strings we are used to using the `decode()` method. Finally, we can split the resulting string at the comma to get the voltage and time point for each.\n", "\n", "Arduino values from analog inputs are 10 bit unsigned integers, with values ranging from 0 to 1023. So, when the voltage is written out, it will be an integer, which we need to convert to a voltage, knowing that the voltage on the Arduino Uno goes from zero to five volts. The time stamps are in milliseconds, transmitted as 32-bit unsigned integers. So, we should convert the voltage number we receive to Volts. The function below parses a byte string sent from Arduino." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "def parse_raw(raw):\n", " \"\"\"Parse bytes output from Arduino.\"\"\"\n", " raw = raw.decode()\n", " if raw[-1] != \"\\n\":\n", " raise ValueError(\n", " \"Input must end with newline, otherwise message is incomplete.\"\n", " )\n", "\n", " t, V = raw.rstrip().split(\",\")\n", "\n", " return int(t), int(V) * 5 / 1023" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can make a button that requests data from Arduino when pressed. We will link it to a function that sends a request and gets data, appending the results to lists. First, we'll set up the data acquisition function. It must take a single argument (which we will ignore) to have the correct call signature to be used when linked to a Panel button." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "def request_single_voltage(arduino):\n", " \"\"\"Ask Arduino for a single data point\"\"\"\n", " # Ask Arduino for data\n", " arduino.write(bytes([VOLTAGE_REQUEST]))\n", "\n", " # Read in the data\n", " raw = arduino.read_until()\n", "\n", " # Parse and return\n", " return parse_raw(raw)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can call the function to get single time, voltage pairs." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(17909, 3.724340175953079)" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "request_single_voltage(arduino)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we like, we can connect this function to a button and grab voltages and time points to store in a list. To do so, we set up the lists and then append to them in a callback. \n", "\n", "Note that a **button** is different from a toggle button. A toggle button stays pressed when pushed, and then stays unpressed when pushed again. This is like a power button. A button, on the other hand, just registers a click, like a mouse button. Therefore,for a button, we use an `on_click()` method instead of `on_change()` to link to the callback.\n", "\n", "Again, Bokeh apps will only appear in a running notebook, so the button below will not appear in the static rendering of this notebook." ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "application/vnd.bokehjs_exec.v0+json": "", "text/html": [ "\n", "" ] }, "metadata": { "application/vnd.bokehjs_exec.v0+json": { "server_id": "24513fa38db9499287f65004c1e09310" } }, "output_type": "display_data" } ], "source": [ "time_ms = []\n", "voltage = []\n", "\n", "def daq_app(doc):\n", " \"\"\"Button for acquiring time stamp and voltage and store in lists.\"\"\"\n", "\n", " def daq_callback(event):\n", " t, V = request_single_voltage(arduino)\n", " time_ms.append(t)\n", " voltage.append(V)\n", " \n", " # Set up the button\n", " daq_button = bokeh.models.Button(label=\"Acquire\", button_type=\"primary\")\n", " daq_button.on_click(daq_callback)\n", " \n", " doc.add_root(daq_button)\n", " \n", " \n", "bokeh.io.show(daq_app, notebook_url=notebook_url)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "After hitting the button a few times (while turning the knob on the potentiometer), I got updated lists." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "time (ms) voltage (V)\n", "23715 3.724340175953079\n", "24083 3.919843597262952\n", "24371 4.8240469208211145\n", "24707 5.0\n", "24954 4.1006842619745845\n", "25194 3.7047898338220917\n", "25546 3.059628543499511\n", "25834 2.9912023460410557\n", "26290 2.8299120234604107\n", "26577 2.321603128054741\n", "26882 1.6911045943304008\n", "27234 1.1974584555229717\n", "27570 1.2023460410557185\n", "27906 0.5767350928641252\n", "28233 0.0\n", "28529 0.0\n", "28817 0.24926686217008798\n", "29216 1.1681329423264908\n", "29497 1.2658846529814272\n", "30032 2.096774193548387\n", "30328 2.697947214076246\n", "30777 2.790811339198436\n", "31112 3.6608015640273703\n", "31399 4.335288367546432\n" ] } ], "source": [ "print(\"time (ms) voltage (V)\")\n", "for t, V in zip(time_ms, voltage):\n", " print(t, ' ', V)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Continuously receiving data\n", "\n", "We may want to get data over some time interval. To do so, we can put the our voltage requests in a loop. For example, to acquire data over the course of ten seconds with a sample every 20 ms, we could do the following." ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [], "source": [ "time_ms = []\n", "voltage = []\n", "\n", "time_to_acquire = 10 # seconds\n", "daq_delay = 0.02 # seconds\n", "\n", "for i in range(int(time_to_acquire / daq_delay)):\n", " # Request and append\n", " t, V = request_single_voltage(arduino)\n", " time_ms.append(t)\n", " voltage.append(V)\n", "\n", " # Wait till next acquisition\n", " sleep(daq_delay)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Once we have acquired our data, we can store them in a data frame for saving and plotting." ] }, { "cell_type": "code", "execution_count": 28, "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", " const docs_json = {\"a12b9c39-644c-49af-9541-7f6dc046ab2e\":{\"defs\":[],\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1054\"}],\"center\":[{\"id\":\"1057\"},{\"id\":\"1061\"}],\"frame_height\":200,\"frame_width\":400,\"left\":[{\"id\":\"1058\"}],\"renderers\":[{\"id\":\"1080\"}],\"title\":{\"id\":\"1082\"},\"toolbar\":{\"id\":\"1069\"},\"x_range\":{\"id\":\"1046\"},\"x_scale\":{\"id\":\"1050\"},\"y_range\":{\"id\":\"1048\"},\"y_scale\":{\"id\":\"1052\"}},\"id\":\"1045\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{},\"id\":\"1062\",\"type\":\"PanTool\"},{\"attributes\":{},\"id\":\"1086\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1088\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{},\"id\":\"1055\",\"type\":\"BasicTicker\"},{\"attributes\":{},\"id\":\"1052\",\"type\":\"LinearScale\"},{\"attributes\":{},\"id\":\"1089\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1065\",\"type\":\"SaveTool\"},{\"attributes\":{},\"id\":\"1085\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"overlay\":{\"id\":\"1068\"}},\"id\":\"1064\",\"type\":\"BoxZoomTool\"},{\"attributes\":{},\"id\":\"1050\",\"type\":\"LinearScale\"},{\"attributes\":{},\"id\":\"1066\",\"type\":\"ResetTool\"},{\"attributes\":{\"axis_label\":\"voltage (V)\",\"coordinates\":null,\"formatter\":{\"id\":\"1085\"},\"group\":null,\"major_label_policy\":{\"id\":\"1086\"},\"ticker\":{\"id\":\"1059\"}},\"id\":\"1058\",\"type\":\"LinearAxis\"},{\"attributes\":{},\"id\":\"1067\",\"type\":\"HelpTool\"},{\"attributes\":{},\"id\":\"1046\",\"type\":\"DataRange1d\"},{\"attributes\":{},\"id\":\"1059\",\"type\":\"BasicTicker\"},{\"attributes\":{},\"id\":\"1063\",\"type\":\"WheelZoomTool\"},{\"attributes\":{\"axis\":{\"id\":\"1058\"},\"coordinates\":null,\"dimension\":1,\"group\":null,\"ticker\":null},\"id\":\"1061\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1090\",\"type\":\"UnionRenderers\"},{\"attributes\":{},\"id\":\"1091\",\"type\":\"Selection\"},{\"attributes\":{\"bottom_units\":\"screen\",\"coordinates\":null,\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"group\":null,\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"syncable\":false,\"top_units\":\"screen\"},\"id\":\"1068\",\"type\":\"BoxAnnotation\"},{\"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],\"time (s)\":{\"__ndarray__\":\"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\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[500]},\"voltage (V)\":{\"__ndarray__\":\"XHHFFVdcEUBmmWWWWWYRQFxxxRVXXBFAYYUVVlhhEUBhhRVWWGERQFxxxRVXXBFAYYUVVlhhEUBhhRVWWGERQFxxxRVXXBFAYYUVVlhhEUBcccUVV1wRQFxxxRVXXBFAYYUVVlhhEUBcccUVV1wRQGGFFVZYYRFAZplllllmEUBcccUVV1wRQGGFFVZYYRFAYYUVVlhhEUBhhRVWWGERQFxxxRVXXBFAYYUVVlhhEUBmmWWWWWYRQGGFFVZYYRFAYYUVVlhhEUBcccUVV1wRQGGFFVZYYRFAZplllllmEUBhhRVWWGERQFxxxRVXXBFAYYUVVlhhEUBhhRVWWGERQGaZZZZZZhFAYYUVVlhhEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBcccUVV1wRQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBcccUVV1wRQFxxxRVXXBFAZplllllmEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBmmWWWWWYRQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBcccUVV1wRQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBcccUVV1wRQGGFFVZYYRFAZplllllmEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBcccUVV1wRQFxxxRVXXBFAZplllllmEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBmmWWWWWYRQGGFFVZYYRFAYYUVVlhhEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBcccUVV1wRQGaZZZZZZhFAYYUVVlhhEUBhhRVWWGERQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGGFFVZYYRFAYYUVVlhhEUBhhRVWWGERQGGFFVZYYRFAZplllllmEUBcccUVV1wRQGGFFVZYYRFAXHHFFVdcEUBhhRVWWGERQGuttdZaaxFAYYUVVlhhEUBmmWWWWWYRQGuttdZaaxFAeumll156EUBwwQUXXHARQHDBBRdccBFAa6211lprEUBwwQUXXHARQHDBBRdccBFAeumll156EUCJJZZYYokRQKeddtpppxFAzz333HPPEUD33XffffcRQDniiCOOOBJAV1pppZVWEkBSRhlllFESQFdaaaWVVhJAXG655ZZbEkBhggkmmGASQGaWWWaZZRJAddJJJ510EkCTSiqppJISQMD++uuvvxJAFVNMMcUUE0Bba6211loTQI0zzjjjjBNAxA8//PDDE0Dsr7/++usTQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAAAAAAAAAFEAAAAAAAAAUQAAAAAAAABRAoYMOOuigE0AGF1xwwQUTQJ1yyimnnBJAf/rpp59+EkBcbrnlllsSQDTOOOOMMxJA6KGHHnroEUCdddZZZ50RQFJJJZVUUhFA6aSTTjrpEECUUEIJJZQQQFhggQUWWBBAMMAAAwwwEEAmmGCCCSYQQOiff/755w9AUkcdddRRD0D43nvvvfcOQNpmm2222Q5A2mabbbbZDkDQPvvss88OQNpmm2222Q5A2mabbbbZDkDQPvvss88OQMYWW2yxxQ5AxhZbbLHFDkDQPvvss88OQMYWW2yxxQ5AxhZbbLHFDkC87rrrrrsOQNA+++yzzw5AvO666667DkDGFltsscUOQMYWW2yxxQ5A0D777LPPDkDGFltsscUOQNA+++yzzw5AqJ566qmnDkCedtppp50OQHbWWWeddQ5AOuaYY445DkDppZdeeukNQKONNtpoow1AP/30008/DUD55JNPPvkMQL300ksvvQxAiyyyyCKLDEBZZJFFFlkMQCeccMIJJwxA66uvvvrqC0Cba6655poLQGmjjTbaaAtABRNMMMEEC0C10korrbQKQD3yyCOPPApAkkkmmWSSCUAkkUQSSSQJQKKIIooooghAPvjggw8+CEDut99+++0HQMYXX3zxxQdAdtddd911B0Acb7zxxhsHQMwuu+yyywZAXnbZZZddBkD55ZdffvkFQJ999tlnnwVAMcUUU0wxBUCllFJKKaUEQBlkkEEGGQRAgwsuuOCCA0DjiiuuuOICQHXSSSeddAJAG2qooYYaAkDeeeedd94BQI455phjjgFANNFEE000AUDaaKONNtoAQKigggoqqABAiiiiiCKKAECUUEIJJZQAQJRQQgkllABAnnjiiSeeAECeeOKJJ54AQKigggoqqABAqKCCCiqoAECooIIKKqgAQPjggw8++ABAmGGGGWaYAUA54ogjjjgCQJNKKqmkkgJAFVNMMcUUA0Blk0022WQDQLXTTjvttANAGWSQQQYZBEBffPHFF18EQKWUUkoppQRA//zzzz//BEB33XXXXXcFQPnll19++QVApI466qijBkAml1xyySUHQIonnnjiiQdA7rfffvvtB0BccMEFF1wIQKKIIooooghA1FBDDTXUCEAkkUQSSSQJQGCBBRZYYAlAsMEGG2ywCUBHGmmkkUYKQKGCCiqooApAI4ssssgiC0Bzyy233HILQOurr7766gtAd9xxxx13DEDvvPPOO+8MQHvttddeew1AHG644YYbDkCA/vnnn38OQNpmm2222Q5AFldcccUVD0AWV1xxxRUPQPjee++99w5A5I477rjjDkDaZpttttkOQIommmiiiQ5AOuaYY445DkDzzTfffPMNQGedddZZZw1ADTXUUEMNDUB33HHHHXcMQK+77rrrrgtA++qrr776CkAzyiijjDIKQJxxxhlnnAlAEEEEEUQQCUDKKKOMMsoIQHrooYceeghA+N9///33B0Bihx122GEHQKSOOuqoowZA5ZVXXnnlBUAnnXTSSScFQJFEEkkkkQRA++uvv/76A0Bvu+22224DQO2yyy677AJAa6qppppqAkAvuuiiiy4CQOihhx566AFAjjnmmGOOAUAqqaSSSioBQNBAAw000ABAssgiiyyyAECKKKKIIooAQE444YQTTgBARBBBBBFEAEAwwAADDDAAQDDAAAMMMABARBBBBBFEAEBOOOGEE04AQE444YQTTgBATjjhhBNOAEBOOOGEE04AQEQQQQQRRABATjjhhBNOAEBEEEEEEUQAQE444YQTTgBATjjhhBNOAEBOOOGEE04AQE444YQTTgBARBBBBBFEAEBEEEEEEUQAQEQQQQQRRABA/O+///77/z+EDz744IP/P/jee++99/4/0D777LPP/j/QPvvss8/+P7zuuuuuu/4/vO666667/j+87rrrrrv+P7zuuuuuu/4/gP75559//j8wvvjiiy/+P6ONNtpoo/0/F1100UUX/T/rq6+++ur7Pw877LDDDvs/b7rppptu+j/iiSeeeOL5P2qppZZaavk/GmmkkUYa+T+OOOKII474P1JIIYUUUvg/sscee+yx9z8655xzzjn3P8IGG2ywwfY/SiaZZJJJ9j+99dZbb731Px111FFHHfU/ueSSSy659D8FFFBAAQX0PxVTTDHFFPM/scIKK6yw8j9NMskkk0zyP8ABBxxwwPE/cMEFF1xw8T/QQAMNNNDwP5hffvnll+8/t91222237T933HHHHXfsP2+66aabbuo/fvnll19+6T9mmGGGGWboP8YXX3zxxec/1lZbbbXV5j+uttpqq63mP4YWWmihheY/hhZaaKGF5j+uttpqq63mP6622mqrreY//vbbb7/95j/ut99+++3nP/bZZ5999uk///vvv//+6z9YXnnllVfuP1hggQUWWPA/cMEFF1xw8T/ZYostttjyPxlkkEEGGfQ/RRVVVFFF9T821lhjjTX2PyaXXHLJJfc/AggggAAC+D/yyCOPPPL4P+KJJ5544vk/q6qqqqqq+j8PO+ywww77P6+77rrrrvs///vvv//++z877LDDDjv8P7PMMssss/w/77zzzjvv/D9nnXXWWWf9P6ONNtpoo/0/CB544IEH/j9YXnnllVf+P/jee++99/4/wP/++++//z9OOOGEE04AQJ544oknngBA5JBDDjnkAEACCSSQQAIBQDTRRBNNNAFAPvnkk08+AUBmmWWWWWYBQIQRRhhhhAFAwAEHHHDAAUD88ccff/wBQDniiCOOOAJATTLJJJNMAkB/+umnn34CQJ1yyimnnAJAxRJLLLHEAkD32muvvfYCQCmjjDLKKANAPfPMM888A0Blk0022WQDQIMLLrjgggNAq6uuuuqqA0Crq6666qoDQN1zzz333ANAGWSQQQYZBEB99NFHH30EQKWUUkoppQRA9dRTTz31BEAnnXTSSScFQE899dRTTwVAY4011lhjBUCLLbbYYosFQMcdd9xxxwVADjbYYIMNBkBKJplkkkkGQHzuueeeewZA1lZbbbXVBkAcb7zxxhsHQGyvvfbaawdAqJ9++umnB0Dut99+++0HQCqooIIKKghAcMABBxxwCECiiCKKKKIIQMooo4wyyghA3njjjTfeCEAGGWSQQQYJQC655JJLLglAQgkllFBCCUBMMcUUU0wJQGCBBRZYYAlAfvnll19+CUCcccYZZ5wJQLrppptuuglA4oknnnjiCUAzyiijjDIKQFFCCSWUUApAjTLKKKOMCkChggoqqKAKQOeaa6655gpADzvssMMOC0A322yzzTYLQEsrrbTSSgtAVVNNNdVUC0Bfe+21114LQHPLLbfccgtAaaONNtpoC0Bzyy233HILQIcbbrjhhgtAm2uuueaaC0Clk0466aQLQMMLL7zwwgtAzTPPPPPMC0DXW2+99dYLQP/777///gtA9dNPP/30C0D/+++///4LQBNMMMEEEwxAJ5xwwgknDEBFFFFEEUUMQGOMMcYYYwxAgQQSSCCBDECBBBJIIIEMQIEEEkgggQxAiyyyyCKLDECffPLJJ58MQLPMMsssswxAxxxzzDHHDEDRRBNNNNEMQA==\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[500]}},\"selected\":{\"id\":\"1091\"},\"selection_policy\":{\"id\":\"1090\"}},\"id\":\"1044\",\"type\":\"ColumnDataSource\"},{\"attributes\":{\"line_alpha\":0.2,\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (s)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"1079\",\"type\":\"Line\"},{\"attributes\":{\"axis_label\":\"time (s)\",\"coordinates\":null,\"formatter\":{\"id\":\"1088\"},\"group\":null,\"major_label_policy\":{\"id\":\"1089\"},\"ticker\":{\"id\":\"1055\"}},\"id\":\"1054\",\"type\":\"LinearAxis\"},{\"attributes\":{\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (s)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"1077\",\"type\":\"Line\"},{\"attributes\":{\"source\":{\"id\":\"1044\"}},\"id\":\"1081\",\"type\":\"CDSView\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1044\"},\"glyph\":{\"id\":\"1077\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1079\"},\"nonselection_glyph\":{\"id\":\"1078\"},\"view\":{\"id\":\"1081\"}},\"id\":\"1080\",\"type\":\"GlyphRenderer\"},{\"attributes\":{},\"id\":\"1048\",\"type\":\"DataRange1d\"},{\"attributes\":{\"coordinates\":null,\"group\":null},\"id\":\"1082\",\"type\":\"Title\"},{\"attributes\":{\"tools\":[{\"id\":\"1062\"},{\"id\":\"1063\"},{\"id\":\"1064\"},{\"id\":\"1065\"},{\"id\":\"1066\"},{\"id\":\"1067\"}]},\"id\":\"1069\",\"type\":\"Toolbar\"},{\"attributes\":{\"line_alpha\":0.1,\"line_color\":\"#1f77b4\",\"x\":{\"field\":\"time (s)\"},\"y\":{\"field\":\"voltage (V)\"}},\"id\":\"1078\",\"type\":\"Line\"},{\"attributes\":{\"axis\":{\"id\":\"1054\"},\"coordinates\":null,\"group\":null,\"ticker\":null},\"id\":\"1057\",\"type\":\"Grid\"}],\"root_ids\":[\"1045\"]},\"title\":\"Bokeh Application\",\"version\":\"2.4.2\"}};\n", " const render_items = [{\"docid\":\"a12b9c39-644c-49af-9541-7f6dc046ab2e\",\"root_ids\":[\"1045\"],\"roots\":{\"1045\":\"ab9b7610-14fc-424b-bc13-6e60d96a6927\"}}];\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", " let attempts = 0;\n", " const 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": "1045" } }, "output_type": "display_data" } ], "source": [ "df = pd.DataFrame({'time': time_ms, 'voltage (V)': voltage})\n", "\n", "# Convert time to seconds and put start at zero\n", "df['time'] = (df['time'] - df['time'].min()) / 1000\n", "df = df.rename(columns={'time': 'time (s)'})\n", "\n", "source = bokeh.models.ColumnDataSource(df)\n", "\n", "p = bokeh.plotting.figure(\n", " frame_width=400,\n", " frame_height=200,\n", " x_axis_label='time (s)',\n", " y_axis_label='voltage (V)',\n", ")\n", "\n", "p.line(source=source, x='time (s)', y='voltage (V)')\n", "\n", "bokeh.io.show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Warnings about this method of data acquisition\n", "\n", "In looking at the plot above, we see that the maximum time is over twelve seconds. This is because there is a delay in the time at which the data is requested, the request is sent over the serial connection, Arduino receives it, and then sends data back. Furthermore, the timing achievable via Python may not be as accurate as that onboard Arduino, which has its own dedicated clock.\n", "\n", "We can see the variability in the timing by plotting an ECDF of the time difference between data acquisitions (achieved using `np.diff()`)." ] }, { "cell_type": "code", "execution_count": 29, "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", " const docs_json = {\"4d139f8a-8a7c-4f74-941c-75e79fa13313\":{\"defs\":[],\"roots\":{\"references\":[{\"attributes\":{\"below\":[{\"id\":\"1157\"}],\"center\":[{\"id\":\"1160\"},{\"id\":\"1164\"}],\"frame_height\":275,\"frame_width\":375,\"left\":[{\"id\":\"1161\"}],\"renderers\":[{\"id\":\"1184\"}],\"title\":{\"id\":\"1197\"},\"toolbar\":{\"id\":\"1172\"},\"toolbar_location\":\"above\",\"x_range\":{\"id\":\"1149\"},\"x_scale\":{\"id\":\"1153\"},\"y_range\":{\"id\":\"1151\"},\"y_scale\":{\"id\":\"1155\"}},\"id\":\"1148\",\"subtype\":\"Figure\",\"type\":\"Plot\"},{\"attributes\":{\"data\":{\"__ECDF\":{\"__ndarray__\":\"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\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[499]},\"__dummy_cat\":[\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \"],\"__label\":[\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \",\" \"],\"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],\"x\":{\"__ndarray__\":\"AAAAAAAAOEAAAAAAAAA4QP7//////zhAAgAAAAAAOUD+//////83QP7//////zhABgAAAAAAOED+//////84QP7//////zdA/v//////OEAGAAAAAAA5QPb//////zdABgAAAAAAOUAGAAAAAAA4QPf//////zhABgAAAAAAOED3//////84QAYAAAAAADlABgAAAAAAOED3//////84QAYAAAAAADhABgAAAAAAOUDn//////83QAYAAAAAADlABgAAAAAAOUAGAAAAAAA4QAYAAAAAADlABgAAAAAAOEDn//////84QAYAAAAAADlABgAAAAAAOEAGAAAAAAA5QAYAAAAAADhA5///////OEAGAAAAAAA4QAYAAAAAADlABgAAAAAAOUAGAAAAAAA4QOf//////zhABgAAAAAAOEDn//////84QAYAAAAAADhAJgAAAAAAOUDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QCYAAAAAADlABgAAAAAAOEDn//////84QOf//////zhAJQAAAAAAN0AGAAAAAAA6QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhAJgAAAAAAOUAGAAAAAAA4QOf//////zhABgAAAAAAOEDn//////84QCUAAAAAADdA5///////OEAmAAAAAAA5QAYAAAAAADhAyP//////OUAlAAAAAAA3QOf//////zhAJgAAAAAAOUAGAAAAAAA4QOf//////zhABgAAAAAAOEDn//////84QAYAAAAAADhAJgAAAAAAOUDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QCYAAAAAADlAyP//////N0Dn//////84QGQAAAAAADlABgAAAAAAOEDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhA5///////OEAGAAAAAAA4QOf//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhA5///////OEDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhA5///////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEDn//////84QAYAAAAAADhA5///////OEDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhABgAAAAAAOEBkAAAAAAA5QOf//////zhABgAAAAAAOEDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhA5///////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEDn//////84QOf//////zhABgAAAAAAOEDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhAZAAAAAAAOUCo//////82QEUAAAAAADpABgAAAAAAOEAGAAAAAAA4QOf//////zhA5///////OEDn//////84QCUAAAAAADdAyP//////OUAGAAAAAAA4QGQAAAAAADlAif//////N0BkAAAAAAA5QOf//////zhABgAAAAAAOEDn//////84QAYAAAAAADhABgAAAAAAOEDn//////84QAYAAAAAADhA5///////OEAGAAAAAAA4QOf//////zhAZAAAAAAAOUAGAAAAAAA4QAYAAAAAADhA5///////OEBq//////84QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGQAAAAAADlAav//////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEBq//////84QAYAAAAAADhAZAAAAAAAOUBkAAAAAAA5QAYAAAAAADhAav//////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEBq//////84QGQAAAAAADlABgAAAAAAOEBq//////84QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGQAAAAAADlAav//////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEBq//////84QAYAAAAAADhAZAAAAAAAOUBkAAAAAAA5QAYAAAAAADhAav//////OEAGAAAAAAA4QGQAAAAAADlAav//////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEBq//////84QAYAAAAAADhAZAAAAAAAOUBkAAAAAAA5QAYAAAAAADhAav//////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEBq//////84QGQAAAAAADlABgAAAAAAOEBkAAAAAAA5QKj//////zZAyP//////OUAGAAAAAAA4QGQAAAAAADlAav//////OEAGAAAAAAA4QGQAAAAAADlABgAAAAAAOEBq//////84QGQAAAAAADlABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAav//////OEAGAAAAAAA4QGQAAAAAADlAav//////OECiAAAAAAA3QMj//////zlABgAAAAAAOEAGAAAAAAA4QAYAAAAAADhAav//////OEBkAAAAAAA5QAYAAAAAADhAyP//////OUCiAAAAAAA3QGr//////zhABgAAAAAAOEBkAAAAAAA5QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGr//////zhAZAAAAAAAOUAGAAAAAAA4QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAav//////OEBkAAAAAAA5QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGr//////zhAZAAAAAAAOUAGAAAAAAA4QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAZAAAAAAAOUBq//////84QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGr//////zhABgAAAAAAOEBkAAAAAAA5QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGr//////zhAZAAAAAAAOUAGAAAAAAA4QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAZAAAAAAAOUBq//////84QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGr//////zhAZAAAAAAAOUAGAAAAAAA4QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAZAAAAAAAOUBq//////84QAYAAAAAADhAZAAAAAAAOUAGAAAAAAA4QGr//////zhABgAAAAAAOEBkAAAAAAA5QAYAAAAAADhAav//////OEBkAAAAAAA5QKj//////zZAwgAAAAAAOkCo//////82QMj//////zlABgAAAAAAOEBkAAAAAAA5QGr//////zhAogAAAAAAN0DO/v////85QGQAAAAAADlAqP//////NkBkAAAAAAA5QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAZAAAAAAAOUBw/v////84QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0BkAAAAAAA5QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAZAAAAAAAOUBw/v////84QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAZAAAAAAAOUBw/v////84QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0BkAAAAAAA5QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAZAAAAAAAOUBw/v////84QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlAZAAAAAAAOUAM//////83QAABAAAAADhAcP7/////OEBkAAAAAAA5QAABAAAAADhAZAAAAAAAOUAM//////83QGQAAAAAADlADP//////N0AAAQAAAAA4QGQAAAAAADlADP//////N0DI//////85QGQAAAAAADlAqP//////NkBkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QGQAAAAAADlAcP7/////OEAAAQAAAAA4QGQAAAAAADlADP//////N0BkAAAAAAA5QAz//////zdAZAAAAAAAOUBkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QGQAAAAAADlAcP7/////OEAAAQAAAAA4QGQAAAAAADlADP//////N0BkAAAAAAA5QAz//////zdAZAAAAAAAOUBkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QGQAAAAAADlADP//////N0BkAAAAAAA5QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QGQAAAAAADlAcP7/////OEAAAQAAAAA4QGQAAAAAADlADP//////N0BkAAAAAAA5QAz//////zdAZAAAAAAAOUBkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QGQAAAAAADlAcP7/////OEAAAQAAAAA4QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAcP7/////OEBkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QGQAAAAAADlADP//////N0BkAAAAAAA5QGQAAAAAADlADP//////N0BkAAAAAAA5QAABAAAAADhAcP7/////OEAAAQAAAAA4QAz//////zdAZAAAAAAAOUBkAAAAAAA5QAz//////zdAZAAAAAAAOUBkAAAAAAA5QKj//////zZAZAAAAAAAOUBkAAAAAAA5QGQAAAAAADlADP//////N0BkAAAAAAA5QAz//////zdA6P//////PEA=\",\"dtype\":\"float64\",\"order\":\"little\",\"shape\":[499]}},\"selected\":{\"id\":\"1206\"},\"selection_policy\":{\"id\":\"1205\"}},\"id\":\"1179\",\"type\":\"ColumnDataSource\"},{\"attributes\":{},\"id\":\"1206\",\"type\":\"Selection\"},{\"attributes\":{},\"id\":\"1149\",\"type\":\"DataRange1d\"},{\"attributes\":{\"coordinates\":null,\"data_source\":{\"id\":\"1179\"},\"glyph\":{\"id\":\"1181\"},\"group\":null,\"hover_glyph\":null,\"muted_glyph\":{\"id\":\"1183\"},\"nonselection_glyph\":{\"id\":\"1182\"},\"view\":{\"id\":\"1185\"}},\"id\":\"1184\",\"type\":\"GlyphRenderer\"},{\"attributes\":{\"axis_label\":\"time between DAQ (ms)\",\"coordinates\":null,\"formatter\":{\"id\":\"1203\"},\"group\":null,\"major_label_policy\":{\"id\":\"1204\"},\"ticker\":{\"id\":\"1158\"}},\"id\":\"1157\",\"type\":\"LinearAxis\"},{\"attributes\":{},\"id\":\"1166\",\"type\":\"WheelZoomTool\"},{\"attributes\":{\"axis\":{\"id\":\"1157\"},\"coordinates\":null,\"group\":null,\"ticker\":null},\"id\":\"1160\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1170\",\"type\":\"HelpTool\"},{\"attributes\":{},\"id\":\"1165\",\"type\":\"PanTool\"},{\"attributes\":{\"overlay\":{\"id\":\"1171\"}},\"id\":\"1167\",\"type\":\"BoxZoomTool\"},{\"attributes\":{},\"id\":\"1151\",\"type\":\"DataRange1d\"},{\"attributes\":{},\"id\":\"1155\",\"type\":\"LinearScale\"},{\"attributes\":{\"fill_color\":{\"value\":\"#1f77b3\"},\"hatch_color\":{\"value\":\"#1f77b3\"},\"line_color\":{\"value\":\"#1f77b3\"},\"x\":{\"field\":\"x\"},\"y\":{\"field\":\"__ECDF\"}},\"id\":\"1181\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1168\",\"type\":\"SaveTool\"},{\"attributes\":{},\"id\":\"1169\",\"type\":\"ResetTool\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.1},\"fill_color\":{\"value\":\"#1f77b3\"},\"hatch_alpha\":{\"value\":0.1},\"hatch_color\":{\"value\":\"#1f77b3\"},\"line_alpha\":{\"value\":0.1},\"line_color\":{\"value\":\"#1f77b3\"},\"x\":{\"field\":\"x\"},\"y\":{\"field\":\"__ECDF\"}},\"id\":\"1182\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1158\",\"type\":\"BasicTicker\"},{\"attributes\":{\"axis_label\":\"ECDF\",\"coordinates\":null,\"formatter\":{\"id\":\"1200\"},\"group\":null,\"major_label_policy\":{\"id\":\"1201\"},\"ticker\":{\"id\":\"1162\"}},\"id\":\"1161\",\"type\":\"LinearAxis\"},{\"attributes\":{\"coordinates\":null,\"group\":null},\"id\":\"1197\",\"type\":\"Title\"},{\"attributes\":{},\"id\":\"1205\",\"type\":\"UnionRenderers\"},{\"attributes\":{},\"id\":\"1200\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"fill_alpha\":{\"value\":0.2},\"fill_color\":{\"value\":\"#1f77b3\"},\"hatch_alpha\":{\"value\":0.2},\"hatch_color\":{\"value\":\"#1f77b3\"},\"line_alpha\":{\"value\":0.2},\"line_color\":{\"value\":\"#1f77b3\"},\"x\":{\"field\":\"x\"},\"y\":{\"field\":\"__ECDF\"}},\"id\":\"1183\",\"type\":\"Circle\"},{\"attributes\":{},\"id\":\"1162\",\"type\":\"BasicTicker\"},{\"attributes\":{\"axis\":{\"id\":\"1161\"},\"coordinates\":null,\"dimension\":1,\"group\":null,\"ticker\":null},\"id\":\"1164\",\"type\":\"Grid\"},{\"attributes\":{},\"id\":\"1153\",\"type\":\"LinearScale\"},{\"attributes\":{\"bottom_units\":\"screen\",\"coordinates\":null,\"fill_alpha\":0.5,\"fill_color\":\"lightgrey\",\"group\":null,\"left_units\":\"screen\",\"level\":\"overlay\",\"line_alpha\":1.0,\"line_color\":\"black\",\"line_dash\":[4,4],\"line_width\":2,\"right_units\":\"screen\",\"syncable\":false,\"top_units\":\"screen\"},\"id\":\"1171\",\"type\":\"BoxAnnotation\"},{\"attributes\":{},\"id\":\"1201\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1204\",\"type\":\"AllLabels\"},{\"attributes\":{},\"id\":\"1203\",\"type\":\"BasicTickFormatter\"},{\"attributes\":{\"source\":{\"id\":\"1179\"}},\"id\":\"1185\",\"type\":\"CDSView\"},{\"attributes\":{\"tools\":[{\"id\":\"1165\"},{\"id\":\"1166\"},{\"id\":\"1167\"},{\"id\":\"1168\"},{\"id\":\"1169\"},{\"id\":\"1170\"}]},\"id\":\"1172\",\"type\":\"Toolbar\"}],\"root_ids\":[\"1148\"]},\"title\":\"Bokeh Application\",\"version\":\"2.4.2\"}};\n", " const render_items = [{\"docid\":\"4d139f8a-8a7c-4f74-941c-75e79fa13313\",\"root_ids\":[\"1148\"],\"roots\":{\"1148\":\"507487eb-3e0d-494b-8905-9c5508263b98\"}}];\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", " let attempts = 0;\n", " const 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": "1148" } }, "output_type": "display_data" } ], "source": [ "time_diff = np.diff(df['time (s)']) * 1000\n", "\n", "p = iqplot.ecdf(time_diff, x_axis_label='time between DAQ (ms)')\n", "bokeh.io.show(p)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Indeed, most acquisitions come at 24 or 25 ms intervals, not 20 ms.\n", "\n", "There is another watchout. Appending a list is computationally expensive as the lists grow long. If you are going to be reading and stored large amounts of data at high frequency, you should not be appending lists, but should rather pre-allocate Numpy arrays and store the data with indexing, for example as follows." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [], "source": [ "time_to_acquire = 10 # seconds\n", "daq_delay = 0.02 # seconds\n", "\n", "# Allocate Numpy arrays\n", "n_data_points = int(time_to_acquire / daq_delay)\n", "time_ms = np.empty(n_data_points)\n", "voltage = np.empty(n_data_points)\n", "\n", "for i in range(n_data_points):\n", " # Request and append\n", " time_ms[i], voltage[i] = request_single_voltage(arduino)\n", "\n", " # Wait till next acquisition\n", " sleep(daq_delay)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A better option for continuous data acquisition is to allow Arduino to continuously send, or **stream** data, as opposed to only sending data upon request. This requires **asynchronous computing**, and therefore a bit more sophistication. These are discussed in auxiliary lessons." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computing environment" ] }, { "cell_type": "code", "execution_count": 31, "metadata": { "tags": [ "hide-input" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Python implementation: CPython\n", "Python version : 3.9.12\n", "IPython version : 8.3.0\n", "\n", "serial : 3.5\n", "bokeh : 2.4.2\n", "jupyterlab: 3.3.2\n", "\n" ] } ], "source": [ "%load_ext watermark\n", "%watermark -v -p serial,bokeh,jupyterlab" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.9.12" } }, "nbformat": 4, "nbformat_minor": 4 }