{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 10. Serial communication with Python\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "import serial\n", "import serial.tools.list_ports" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "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.\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Follow-along exercise 6: Tactile control of an LED\n", "\n", "There is no sketch required for this exercise. Wire up the circuit shown below.\n", "\n", "
\n", " \n", "![PWM LED schematic](external_and_mechanical_led_schem.svg)\n", " \n", "
\n", "\n", "For convenience, here is a pictorial schematic.\n", "\n", "\n", "
\n", " \n", "![PWM LED schematic](external_and_mechanical_led_bb.svg)\n", " \n", "
\n", "\n", "Note that the three-pin toggle switches we have do not look like the one shown in the schematic, but rather [this](https://www.digikey.com/product-detail/en/nidec-copal-electronics/ATE1E-2M3-10-Z/563-1159-ND/1792020) is the part. The pushbutton in the schematic is one of the two-lead pushbuttons we have in lab.\n", "\n", "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. \n", "\n", "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.\n", "\n", "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.\n", "\n", "There is no need to submit this follow-along exercise.\n", "\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Follow-along exercise 7: Electronic control of an LED\n", "\n", "In this rather long follow-along exercise, we will introduce you to control of Arduino using Python. We will extensively use [PySerial](https://pyserial.readthedocs.io/). 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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Finding the port\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[,\n", " ]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ports = serial.tools.list_ports.comports()\n", "\n", "# Take a look\n", "ports" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On my machine, there are two ports open. We can look at the manufacturer of the devices attached to the ports to find Arduino." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[None, 'Arduino (www.arduino.cc)']" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[port.manufacturer for port in ports]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Clearly, the second port is Arduino. We can get a string for the port associated with the device." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'/dev/cu.usbmodem146301'" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ports[1].device" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On Windows, the manufacturer might not appear as Arduino. In this case, you should take a look at the devices for each port." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['/dev/cu.Bluetooth-Incoming-Port', '/dev/cu.usbmodem146301']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "[port.device for port in ports]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The appropriate port for Windows will be something like `'COM3'`.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def find_arduino(port=None):\n", " \"\"\"Get the name of the port that is connected to Arduino.\"\"\"\n", " if port is None:\n", " ports = serial.tools.list_ports.comports()\n", " for p in ports:\n", " if p.manufacturer is not None and \"Arduino\" in p.manufacturer:\n", " port = p.device\n", " return port" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We'll use it to get the port for Arduino." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/dev/cu.usbmodem146301\n" ] } ], "source": [ "port = find_arduino()\n", "\n", "print(port)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Arduino sketch\n", "\n", "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.\"`.\n", "\n", "Here is the code for the sketch, which I describe in more detail below.\n", "\n", "```arduino\n", "const int ledPin = 9;\n", "\n", "const int HANDSHAKE = 0;\n", "const int LED_OFF = 1;\n", "const int LED_ON = 2;\n", "\n", "\n", "void setup() {\n", " pinMode(ledPin, OUTPUT);\n", " \n", " // initialize serial communication\n", " Serial.begin(115200);\n", "}\n", "\n", "\n", "void loop() {\n", " // Check if data has been sent to Arduino and respond accordingly\n", " if (Serial.available() > 0) {\n", " // Read in request\n", " int inByte = Serial.read();\n", "\n", " // Take appropriate action\n", " switch(inByte) {\n", " case LED_ON:\n", " digitalWrite(ledPin, HIGH);\n", " break;\n", " case LED_OFF:\n", " digitalWrite(ledPin, LOW);\n", " break;\n", " case HANDSHAKE:\n", " if (Serial.availableForWrite()) {\n", " Serial.println(\"Message received.\");\n", " }\n", " break;\n", " }\n", " }\n", "}\n", "```\n", "\n", "Some comments:\n", "\n", "- 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 `int`s.\n", "- 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`.\n", "- 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.\n", "- The `Serial.read()` function reads in a single byte from the read buffer. When it is read, the byte vanishes from the read buffer.\n", "- 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.\n", "- 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.\n", "\n", "I always like to keep the same global codes for messages in my Python code as well. It helps avoid bugs." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "HANDSHAKE = 0\n", "LED_OFF = 1\n", "LED_ON = 2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Thinking exercise 4: if vs. while\n", "\n", "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?\n", "\n", "*The answer to this thinking exercise is at the bottom of this lesson.*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Opening a connection\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Handshake message: Message received.\n", "\n" ] } ], "source": [ "# Open port\n", "arduino = serial.Serial(port, baudrate=115200, timeout=1)\n", "\n", "\n", "def handshake_arduino(\n", " arduino, sleep_time=1, print_handshake_message=False, handshake_code=0\n", "):\n", " \"\"\"Make sure connection is established by sending\n", " and receiving bytes.\"\"\"\n", " # Close and reopen\n", " arduino.close()\n", " arduino.open()\n", "\n", " # Chill out while everything gets set\n", " time.sleep(sleep_time)\n", "\n", " # Set a long timeout to complete handshake\n", " timeout = arduino.timeout\n", " arduino.timeout = 2\n", " \n", " # Read and discard everything that may be in the input buffer\n", " _ = arduino.read_all()\n", "\n", " # Send request to Arduino\n", " arduino.write(bytes([handshake_code]))\n", "\n", " # Read in what Arduino sent\n", " handshake_message = arduino.read_until()\n", "\n", " # Send and receive request again\n", " arduino.write(bytes([handshake_code]))\n", " handshake_message = arduino.read_until()\n", "\n", " # Print the handshake message, if desired\n", " if print_handshake_message:\n", " print(\"Handshake message: \" + handshake_message.decode())\n", "\n", " # Reset the timeout\n", " arduino.timeout = timeout\n", "\n", "\n", "# Call the handshake function\n", "handshake_arduino(arduino, print_handshake_message=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A few comments about the above code, bearing in mind that the Python variable `arduino` is a `serial.Serial` instance.\n", "\n", "- The `arduino.timeout` attribute sets the maximum time in seconds to wait for serial communication.\n", "- 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 `int`s and converts them to bytes. So, the signal we would send for code `0` is `bytes([0])`.\n", "- The `arduino.read_all()` function reads all bytes that are in the input buffer on the Python side.\n", "- 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.\n", "- 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()`.\n", "\n", "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.*" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "arduino.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "with serial.Serial(port, baudrate=115200, timeout=1) as arduino:\n", " handshake_arduino(arduino)\n", " \n", " # And the rest of what you want to do follows...." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Turning on the LED\n", "\n", "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. " ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "with serial.Serial(port, baudrate=115200, timeout=1) as arduino:\n", " handshake_arduino(arduino)\n", " \n", " # Turn on the LED\n", " arduino.write(bytes([LED_ON]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now that we know how to turn an LED on (and off), we can make a little disco party!" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "with serial.Serial(port, baudrate=115200, timeout=1) as arduino:\n", " handshake_arduino(arduino)\n", " \n", " # Flash the LEDs\n", " for _ in range(40):\n", " arduino.write(bytes([LED_ON]))\n", " time.sleep(0.05)\n", " arduino.write(bytes([LED_OFF]))\n", " time.sleep(0.05)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Answer to if vs. while exercise**\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Computing environment" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "CPython 3.8.5\n", "IPython 7.18.1\n", "\n", "serial 3.4\n", "panel 0.9.7\n", "jupyterlab 2.2.6\n" ] } ], "source": [ "%load_ext watermark\n", "%watermark -v -p serial,panel,jupyterlab" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.8" } }, "nbformat": 4, "nbformat_minor": 4 }