10. Serial communication with Python


[1]:
import time

import serial
import serial.tools.list_ports

So far, we have programmed Arduino using the Arduino IDE and have used the Serial Monitor and Serial Plotter of the IDE to display signals from Arduino. The Serial Monitor and Plotter are quite limited in their capabilities, and we would like to have much richer control of Arduino.

In the next several lessons, we will learn how to control Arduino using Python-based tools. Ultimately, we will build browser-based control panels for the devices you build. For now, we will learn how to send an instruction to Arduino from Python and have it respond.

We will start by building a circuit with tactile control. That is, you control the circuit with a physical toggle and a physical pushbutton. Then we will move to electronic control from your browser.


Follow-along exercise 6: Tactile control of an LED

There is no sketch required for this exercise. Wire up the circuit shown below.

PWM LED schematic

For convenience, here is a pictorial schematic.

PWM LED schematic

Note that the three-pin toggle switches we have do not look like the one shown in the schematic, but rather this is the part. The pushbutton in the schematic is one of the two-lead pushbuttons we have in lab.

For now, you can ignore LED2 (the one toward the top of the pictorial schematic). That is the LED we will control with buttons in the browser. LED1 (the one toward the right of the pictorial schematic) is under tactile control.

Start with the toggle in the middle (upright position). The LED should be off. Now, depress the pushbutton. You will see that the LED comes on. When you release the button, it turns off.

Next, switch the toggle to turn on the LED. Then switch the toggle to turn off the LED. Note the key operational difference between a toggle and a pushbutton. The state of a toggle (on or off) is preserved, whereas the state of a pushbutton is ephemeral. The LED is on only if the pushbutton is depressed.

There is no need to submit this follow-along exercise.


You have now controlled the on-off behavior of an LED with a toggle and a pushbutton. We will proceed to control those using Python-based control. Our objective is to turn the LED on and off using a virtual button in your browser. We will use a Jupyter notebook, so launch a fresh Jupyter notebook.


Follow-along exercise 7: Electronic control of an LED

In this rather long follow-along exercise, we will introduce you to control of Arduino using Python. We will extensively use PySerial. This is the most commonly used package for communicating with devices over USB. Note that in this lesson, and in all subsequent lessons, all of the imports are at the top of the notebook.

Finding the port

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.

[2]:
ports = serial.tools.list_ports.comports()

# Take a look
ports
[2]:
[<serial.tools.list_ports_common.ListPortInfo at 0x7fe9a236c580>,
 <serial.tools.list_ports_common.ListPortInfo at 0x7fe9a236cb20>]

On my machine, there are two ports open. We can look at the manufacturer of the devices attached to the ports to find Arduino.

[3]:
[port.manufacturer for port in ports]
[3]:
[None, 'Arduino (www.arduino.cc)']

Clearly, the second port is Arduino. We can get a string for the port associated with the device.

[4]:
ports[1].device
[4]:
'/dev/cu.usbmodem146301'

On Windows, the manufacturer might not appear as Arduino. In this case, you should take a look at the devices for each port.

[5]:
[port.device for port in ports]
[5]:
['/dev/cu.Bluetooth-Incoming-Port', '/dev/cu.usbmodem146301']

The appropriate port for Windows will be something like 'COM3'.

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.

[6]:
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

We’ll use it to get the port for Arduino.

[7]:
port = find_arduino()

print(port)
/dev/cu.usbmodem146301

Arduino sketch

Now, we can write a sketch to allow Arduino to communicate with Python. Our strategy is to use PySerial to set a byte to Arduino. Depending on which byte was received, Arduino will take appropriate action. For the present device, there are three possible bytes we will send. We will send a byte instructing Arduino to turn the LED on, a byte to turn the LED off, and also a byte for handshaking. When first connecting with Arduino, it is prudent to send a byte and receive a byte to make sure everything is working ok; this is called handshaking. In my sketches, I always have 0 be a handshaking byte, and I have Arduino respond with a string, "Message received.".

Here is the code for the sketch, which I describe in more detail below.

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;
    }
  }
}

Some comments:

  • I have set global variables (LED_ON, LED_OFF, and HANDSHAKE) to be the integers corresponding to the bytes sent by Python to Arduino. Though they are bytes, the return type of Serial.read() is an int, so we declare these global variables to also be ints.

  • I set the pinMode of the pin controlling the LED to be OUTPUT. This is important because an internal pull-up resistor is enabled on the pin if the pinMode is note set to OUTPUT.

  • Within the loop() function, we first check to see how many bytes are in the read buffer using Serial.available(). If any bytes are available, we proceed to read them.

  • The Serial.read() function reads in a single byte from the read buffer. When it is read, the byte vanishes from the read buffer.

  • We next have a set of cases that determine what Arduino will do, depending on what byte was sent. If the sent byte does not match any of those specified by the global variables, it is ignored.

  • When we check for handshaking, we check to make sure the write buffer is not full. The Serial.availableForWrite() function returns the number of bytes in the write buffer (out of the total of 64) that are available for writing. We then proceed to write the handshake message.

I always like to keep the same global codes for messages in my Python code as well. It helps avoid bugs.

[8]:
HANDSHAKE = 0
LED_OFF = 1
LED_ON = 2

Thinking exercise 4: if vs. while

As a quick aside, think about this: I used if (Serial.available() > 0). Could I have used while (Serial.available() > 0)? What is the difference?

The answer to this thinking exercise is at the bottom of this lesson.

Opening a connection

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.

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

[9]:
# Open port
arduino = serial.Serial(port, baudrate=115200, timeout=1)


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


# Call the handshake function
handshake_arduino(arduino, print_handshake_message=True)
Handshake message: Message received.

A few comments about the above code, bearing in mind that the Python variable arduino is a serial.Serial instance.

  • The arduino.timeout attribute sets the maximum time in seconds to wait for serial communication.

  • 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 ints and converts them to bytes. So, the signal we would send for code 0 is bytes([0]).

  • The arduino.read_all() function reads all bytes that are in the input buffer on the Python side.

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

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

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.

[10]:
arduino.close()

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.

[11]:
with serial.Serial(port, baudrate=115200, timeout=1) as arduino:
    handshake_arduino(arduino)

    # And the rest of what you want to do follows....

Turning on the LED

To turn on the red LED, we need to send code 2 as bytes to the Arduino. Let’s open up a port to Arduino and send a signal to turn on the red LED.

[12]:
with serial.Serial(port, baudrate=115200, timeout=1) as arduino:
    handshake_arduino(arduino)

    # Turn on the LED
    arduino.write(bytes([LED_ON]))

Now that we know how to turn an LED on (and off), we can make a little disco party!

[13]:
with serial.Serial(port, baudrate=115200, timeout=1) as arduino:
    handshake_arduino(arduino)

    # Flash the LEDs
    for _ in range(40):
        arduino.write(bytes([LED_ON]))
        time.sleep(0.05)
        arduino.write(bytes([LED_OFF]))
        time.sleep(0.05)

Answer to if vs. while exercise

I could have used a while loop instead of the if statement. In the case of a while loop, it keeps reading, byte by byte, what is in the input buffer to Arduino until all bytes are read. However, the if statement achieves the same because the loop() function is called over and over again. So, the if statement and the while loop are in practice equivalent.

Computing environment

[14]:
%load_ext watermark
%watermark -v -p serial,panel,jupyterlab
CPython 3.8.5
IPython 7.18.1

serial 3.4
panel 0.9.7
jupyterlab 2.2.6