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()
import panel as pn
pn.extension()
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).
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 8: Controlling Arduino with Panel widgets
Panel is a Python package that allows you to build dashboards containing widgets, plots, and other graphics and text in an interactive way. It uses the Bokeh plotting package under the hood to generate and connect the widgets. I have important Panel as pn
in this lesson.
Widgets and callbacks
As a first step in setting up control of the device, we will make toggle buttons to turn the red and green LEDs on and off. We can make the buttons using pn.widgets.Toggle()
. We use button_type="danger"
to give us a red button (there actually is no danger!).
[6]:
LED_toggle = pn.widgets.Toggle(
name="LED", value=False, button_type="danger", width=100,
)
The button now exists; to see it, we have to render it, which we will do soon.
Before we lay out the buttons, we need to define callbacks for them. A callback is a function that is called when the state of the widget changes. A callback takes as an argument a Panel Event
object, which is used to signal any kinds of changes in a widget. Event
objects have the following attributes.
name
: The name of the parameter that has changednew
: The new value of the parameterold
: The old value of the parameter before the event was triggeredtype
: The type of event (‘triggered’, ‘changed’, or ‘set’)what
: Describes what about the parameter changed (usually the value but other parameter attributes can also change)obj
: The Parameterized instance that holds the parametercls
: The Parameterized class that holds the parameter
For making a callback to toggle between on and off for the LEDs, we need to evaluate the new
attribute and send the appropriate signal to the Arduino.
[7]:
def toggle_LED(event):
if event.new:
arduino.write(bytes([LED_ON]))
else:
arduino.write(bytes([LED_OFF]))
Now that we have the callback, we need to set up a watcher so that the value of the respective toggles are “watched” and the callback is called when they change. The arguments to the widget.param.watch()
method (where “widget
” is the name of whatever widget you are trying to watch) are the callback function and the name or names of attributes associated with the toggle (almost always 'value'
) whose change is to be watched.
[8]:
LED_watcher = LED_toggle.param.watch(toggle_LED, 'value')
The toggle is now properly watched, so we can render it and put it to use.
[9]:
LED_toggle
[9]:
LED2 on the Arduino can now be toggled using this button.
You are not required to submit this follow-along exercise.
Follow-along exercise 9: Controlling Arduino with base Bokeh
While Panel, as a high-level interface for making Bokeh widgets, is easy to use and quite powerful, the apps you build with base Bokeh can have faster performance and are more configurable. Since we will be building a similar LED button using base Bokeh, we need to disconnect, or “unwatch” the Panel-generated widget.
[10]:
LED_toggle.param.unwatch(LED_watcher)
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.
[11]:
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:
Instead of setting up a watcher like we do with Panel, 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.We instantiate a toggle with
bokeh.models.Toggle
. Instead ofname
, we use the kwarglabel
.Instead of setting up a watcher for the toggle widget, the callback is linked using the
on_change()
method of the toggle itself. 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.
[12]:
bokeh.io.show(LED_app, notebook_url=notebook_url)
Finally, as always, we need to close the connection to Arduino.
[13]:
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
[14]:
%load_ext watermark
%watermark -v -p serial,bokeh,panel,jupyterlab
Python implementation: CPython
Python version : 3.8.8
IPython version : 7.21.0
serial : 3.5
bokeh : 2.3.0
panel : 0.10.3
jupyterlab: 3.0.11