11. Controlling Arduino with widgets
[1]:
import time
import serial
import serial.tools.list_ports
import bokeh.models
import bokeh.io
notebook_url = "localhost:8888"
bokeh.io.output_notebook()
For this lesson, we use the same setup as the last time, the schematic of which is shown below (though LED2 is the only device we are interested in for this lesson). Our goal is to control the LED turning on and off via a toggle button (a type of widget, a graphical object that can be manipulated to get responses) in the browser using Bokeh.
We also use the same sketch.
const int ledPin = 9;
const int HANDSHAKE = 0;
const int LED_OFF = 1;
const int LED_ON = 2;
void setup() {
pinMode(ledPin, OUTPUT);
// initialize serial communication
Serial.begin(115200);
}
void loop() {
// Check if data has been sent to Arduino and respond accordingly
if (Serial.available() > 0) {
// Read in request
int inByte = Serial.read();
// Take appropriate action
switch(inByte) {
case LED_ON:
digitalWrite(ledPin, HIGH);
break;
case LED_OFF:
digitalWrite(ledPin, LOW);
break;
case HANDSHAKE:
if (Serial.availableForWrite()) {
Serial.println("Message received.");
}
break;
}
}
}
Finally, the functions for connecting to Arduino from last time are again useful.
[2]:
def find_arduino(port=None):
"""Get the name of the port that is connected to Arduino."""
if port is None:
ports = serial.tools.list_ports.comports()
for p in ports:
if p.manufacturer is not None and "Arduino" in p.manufacturer:
port = p.device
return port
def handshake_arduino(
arduino, sleep_time=1, print_handshake_message=False, handshake_code=0
):
"""Make sure connection is established by sending
and receiving bytes."""
# Close and reopen
arduino.close()
arduino.open()
# Chill out while everything gets set
time.sleep(sleep_time)
# Set a long timeout to complete handshake
timeout = arduino.timeout
arduino.timeout = 2
# Read and discard everything that may be in the input buffer
_ = arduino.read_all()
# Send request to Arduino
arduino.write(bytes([handshake_code]))
# Read in what Arduino sent
handshake_message = arduino.read_until()
# Send and receive request again
arduino.write(bytes([handshake_code]))
handshake_message = arduino.read_until()
# Print the handshake message, if desired
if print_handshake_message:
print("Handshake message: " + handshake_message.decode())
# Reset the timeout
arduino.timeout = timeout
We will be using these functions again and again throughout the course. I thought about putting them in a package, and you may want to do that yourself, but I am not doing that because we may adapt them for specific applications we may consider.
Let’s go ahead and get the port so we have it going forward.
[3]:
port = find_arduino()
As before, we should set the codes for communicating with Arduino.
[4]:
HANDSHAKE = 0
LED_OFF = 1
LED_ON = 2
When we use widgets to control behavior of Arduino within a Jupyter notebook, we need to do it outside of context management lest we have a giant monolithic code cell. So, let’s open the connection, remembering to close it when we are done.
[5]:
arduino = serial.Serial(port, baudrate=115200)
handshake_arduino(arduino, handshake_code=HANDSHAKE)
Follow-along exercise 9: Controlling Arduino with Bokeh
Note that follow-along exercise 8 is omitted.
To build a Bokeh app to use in a Jupyter notebook, we need to write a function of with call signature app(doc)
. Within that function, we build the elements we want in the app, in this case just the toggle and its callback. Once those elements are defined, they need to be added to the doc using doc.add_root()
. The code below accomplishes this.
[6]:
def LED_app(doc):
"""Make a toggle for turning LED on and off"""
def callback(attr, old, new):
if new:
arduino.write(bytes([LED_ON]))
else:
arduino.write(bytes([LED_OFF]))
# Set up toggle
LED_toggle = bokeh.models.Toggle(
label="LED", button_type="danger", width=100,
)
# Link callback
LED_toggle.on_change("active", callback)
doc.add_root(LED_toggle)
Some comments:
In our app, we defined a callback. A callback is a function that is called when the state of a widget changes. See the last bullet point below about its call signature. In this case, if the value of the toggle is
True
(that is,new == True
), then the LED it turned on. It is turned off if the value of the toggle isFalse
.We instantiate a toggle with
bokeh.models.Toggle
. Thebutton_type="danger"
keyword argument simply specifies that the color of the toggle is red; there is no danger!To link the callback function to the toggle, we use the
on_change()
method of a toggle widget. The callback function for on-change behavior must have call signaturecallback(attr, old, new)
, whereattr
is an attribute of the widget,old
is the pre-change value of that attribute, andnew
is the post-change value of the attribute. A toggle has anactive
attribute, which isTrue
when the toggle is on andFalse
when off. Whenever that value changes, the callback is triggered.
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.
[7]:
bokeh.io.show(LED_app, notebook_url=notebook_url)
Finally, as always, we need to close the connection to Arduino.
[8]:
arduino.close()
A stand-alone app in the browser
Interfacing through JupyterLab is convenient for development of dashboard apps for controlling devices. However, it is nice to have a stand-along dashboard for control, i.e., an app that is by itself in a browser tab.
Setting that up requires just a bit more effort than what we have done so far. The code for the app needs to sit in its own .py
file. Below are the contents of a .py
file for the LED app.
import time
import serial
import serial.tools.list_ports
import bokeh.models
import bokeh.plotting
def find_arduino(port=None):
"""Get the name of the port that is connected to Arduino."""
if port is None:
ports = serial.tools.list_ports.comports()
for p in ports:
if p.manufacturer is not None and "Arduino" in p.manufacturer:
port = p.device
return port
def handshake_arduino(
arduino, sleep_time=1, print_handshake_message=False, handshake_code=0
):
"""Make sure connection is established by sending
and receiving bytes."""
# Close and reopen
arduino.close()
arduino.open()
# Chill out while everything gets set
time.sleep(sleep_time)
# Set a long timeout to complete handshake
timeout = arduino.timeout
arduino.timeout = 2
# Read and discard everything that may be in the input buffer
_ = arduino.read_all()
# Send request to Arduino
arduino.write(bytes([handshake_code]))
# Read in what Arduino sent
handshake_message = arduino.read_until()
# Send and receive request again
arduino.write(bytes([handshake_code]))
handshake_message = arduino.read_until()
# Print the handshake message, if desired
if print_handshake_message:
print("Handshake message: " + handshake_message.decode())
# Reset the timeout
arduino.timeout = timeout
port = find_arduino()
HANDSHAKE = 0
LED_OFF = 1
LED_ON = 2
# Open serial connection and leave it open
arduino = serial.Serial(port, baudrate=115200)
handshake_arduino(arduino, handshake_code=HANDSHAKE)
def LED_app(doc):
"""Make a toggle for turning LED on and off"""
def callback(attr, old, new):
if new:
arduino.write(bytes([LED_ON]))
else:
arduino.write(bytes([LED_OFF]))
# Set up toggle
LED_toggle = bokeh.models.Toggle(label="LED", button_type="danger", width=100,)
# Link callback
LED_toggle.on_change("active", callback)
doc.add_root(LED_toggle)
LED_app(bokeh.plotting.curdoc())
As you can see, most of the code is exactly as we have written so far. There are only two differences.
In the last line, the function that defines the app is called with argument
bokeh.plotting.curdoc()
, which returns the current document.The serial connection is opened, but not closed. The reason for this is because the
.py
file will be executed to completion defining the app, and then Bokeh will handle serving it in the browser. The connection must remain open after the.py
file executes, otherwise your widgets will have no affect on Arduino because the serial connection will be broken.
To serve up the app, save the above Python code in a file led_toggle_app.py
and do the following on the command line:
bokeh serve --show led_toggle_app.py
A browser page should open with address http://localhost:5006/led_toggle_app
, which is where the app is running.
This last part of the exercise is all you need to submit. That is, record a video of you clicking the toggle button in the stand-alone app and the LED coming on and off.
Computing environment
[9]:
%load_ext watermark
%watermark -v -p serial,bokeh,panel,jupyterlab
Python implementation: CPython
Python version : 3.9.13
IPython version : 8.4.0
serial : 3.5
bokeh : 2.4.3
panel : 0.13.1
jupyterlab: 3.4.4