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

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 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 is False.

  • We instantiate a toggle with bokeh.models.Toggle. The button_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 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. 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.

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

  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

[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