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()
Loading BokehJS ...

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).

PWM LED schematic

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 changed

  • new: The new value of the parameter

  • old: The old value of the parameter before the event was triggered

  • type: 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 parameter

  • cls: 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

Data type cannot be displayed:

[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 an 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.

  • We instantiate a toggle with bokeh.models.Toggle. Instead of name, we use the kwarg label.

  • 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 an active attribute, which is True when the toggle is on and False 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.

  1. In the last line, the function that defines the app is called with argument bokeh.plotting.curdoc(), which returns the current document.

  2. 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
CPython 3.8.5
IPython 7.18.1

serial 3.4
bokeh 2.2.1
panel 0.9.7
jupyterlab 2.2.6