Build a Mute/Unmute Button for your Video Calls with a Raspberry Pi Pico and MicroPython

May 18, 2021
Written by
Reviewed by

Build a Mute/Unmute Button for your Video Calls with a Raspberry Pi Pico and MicroPython

I always have trouble remembering what is the Zoom keyboard shortcut to mute or unmute my audio, so I end up grabbing the mouse and clicking the button instead. While there isn’t really a problem with clicking, it feels inefficient, and that awkward silence while every other call participant is waiting for me to unmute and start speaking appears to last an eternity.

I thought it would be interesting to use my Raspberry Pi Pico microcontroller board and MicroPython to design a single-key keyboard with the only function to toggle the audio on my video calls. That way, there is no key combination to remember!

Project demonstration

Do you want to learn how to build this project? Just follow along!

Tutorial requirements

To build this cool little gadget you will need a few hardware components, described in the following sections.

Raspberry Pi Pico microcontroller

The heart of this device is going to be a Raspberry Pi Pico microntroller. For this project it is recommended that you buy one with headers already soldered, as shown in the picture below.

Raspberry Pi Pico

If you are handy with a soldering iron, you can buy the standard Pico, which comes without header pins, and solder them yourself.

Push button

The next component that you will need to acquire is a momentary push button switch. It does not need to be exactly like the one pictured below, but you’d want it to be momentary, which means that after you release it it returns to its original state.

Momentary push button

Breadboard

The easiest way to build the circuit is to do it on a breadboard. For this project a small 400 point breadboard is the perfect size.

400 point breadboard

Jumper wires

The connections between the Raspberry Pi Pico and the push button will be made with jumper wires. These are short cables that have pins on the ends. The pins insert in the holes of the breadboard.

Jumper wires

Micro-USB cable

To power your Raspberry Pi Pico you will need a USB cable. The connector on the Pico side is a micro-USB (pictured on the right side in the image below). The other end of the cable will plug into your computer, so you may need that to be a regular USB connector (as shown in the image), or maybe a USB-C connector if you have a newer computer.

USB cable

Python 3

The final requirement is a Python 3 interpreter installed on your computer. If you don’t have it already, you can grab an installer for your operating system from python.org.

MicroPython vs. CircuitPython

There are two major sources of MicroPython firmware for the Raspberry Pi Pico:

The MicroPython language is the same in both versions (CircuitPython is a fork of MicroPython), but there are some small variations in the functionality that is included in the releases from these two sources. CircuitPython also has an extensive library of installable drivers for many components, so even if you work with the official MicroPython, it is a good idea to keep CircuitPython in mind.

CircuitPython builds come with USB HID support included, while MicroPython builds do not. The HID (Human Interface Devices) part of the USB specification is the one that covers computer peripherals such as mice and keyboards, so for the project we intend to build, CircuitPython makes the most sense.

Installing CircuitPython on your Pico

In this section you are going to work on installing the CircuitPython firmware inside your Raspberry Pi Pico.

Visit the CircuitPython for Pico downloads page and download the latest version of the firmware to your computer. This is a file that ends with a .uf2 file extension.

Now comes the fun part. Take the USB cable and plug the USB or USB-C end into your computer. Do not plug the micro-USB end to your Pico just yet.

Grab your Pico and locate the BOOTSEL button.

Pico BOOTSEL button

Press the button, and while you keep it pressed, plug the micro-USB end of the USB cable to power your device. In a second or two a new disk drive labeled RPI-RP2 will appear on your computer. You can release the BOOTSEL button when you see it.

Pico RPI-RP2 disk drive

To install CircuitPython, drag and drop the .uf2 file that you downloaded earlier inside this disk drive. As soon as the file transfers, the drive will disappear and the device will reboot itself. On some computers you may get a warning about the drive being disconnected without properly ejecting it. You can ignore this warning.

After your device reboots and loads the CircuitPython firmware, a new disk drive with the label CIRCUITPY should appear on your computer.

Installing a uf2 firmware

To verify that your installation was successful, create a file with the name code.py on your computer and enter the following code in it:

import board
import digitalio
import time

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = False

# blink the LED three times
for _ in range(3):
    # light up the LED
    led.value = True

    # wait 0.5 seconds
    time.sleep(0.5)

    # turn off the LED
    led.value = False

    # wait 0.5 seconds
    time.sleep(0.5)

This code blinks the LED that is installed on the Pico board three times. While the Pico is still connected to your computer, drag and drop the code.py file to the CIRCUITPY volume. If there is a code.py file already on the disk first make sure it does not have anything you care about, and then replace it with this version.

The Pico will detect that the code.py file has been updated, will reboot itself, and run the code. You should see the LED, which is near the micro-USB connector, blink three times.

Congratulations, you have successfully installed CircuitPython on your Pico!

Installing CircuitPython libraries

While the low-level support for HID devices is bundled with the CircuitPython build, the HID protocol used by keyboards is fairly complex. Lucky for us, the Adafruit CircuitPython library includes a high-level HID interface that allows an application to emulate a keyboard, a mouse or a gamepad.

The first step to install a library is to download the CircuitPython library bundle to your computer.

Visit the CircuitPython Library Bundle page and download the latest version of the bundle to your computer. There are three downloads associated with the library bundle, all of them .zip files:

  • The compiled “Bundle”
  • The MicroPython source code for the bundle library
  • Example code

What we need for this tutorial is the first of the three downloads, which includes the compiled MicroPython code for all the Adafruit libraries. When you download the bundle zip file, make sure that the version matches the version of CircuitPython that you installed on your Pico.

Use your favorite zip extraction tool to expand the contents of the downloaded bundle file. Inside the top-level folder you will find a lib subdirectory:

CircuitPython lib directory

Inside lib, you will find a really large number of files and subdirectories. Look for a subdirectory with the name adafruit_hid. This is the library that we need to install on the Pico.

Open the CIRCUITPY disk drive, and create a lib directory (if it doesn’t exist yet). Then drag and drop the adafruit_hid directory inside lib. The structure of your Pico disk should now look like the following:

Project file structure

The .mpy files that you see in the adafruit_hid directory are compiled MicroPython files that are optimized to save space in your device’s file system. As such, these are not files that you can open and view in a text editor. To inspect the source code for this library you have to download the source code bundle.

The CircuitPython REPL

Like the standard Python language, MicroPython and CircuitPython come with a REPL, where you can enter statements and have them evaluated interactively. The CircuitPython REPL can be accessed from your computer while the device is connected with the USB cable.

Identifying the Pico serial device

The Pico device appears as a USB serial port device on your computer, so the first task is to figure out which of possibly several serial ports installed on your computer is the one that is connected to your board.

If you are using a Mac OS computer, the name of the Pico serial device should be /dev/tty.usbmodem<n>, where <n> is a number. To find out what the complete name of your serial device is, connect the Pico to your computer and then look in the /dev directory for files with this pattern:

$ ls /dev/tty.usbmodem*
/dev/tty.usbmodem143101

On a Linux computer the device name may vary, but it usually has the format /dev/ttyACM<n>, where <n> is a number, likely 0. You can use the lsusb command to list all the USB attached devices. If you can’t identify which device is the one that maps to the Pico, you can run this command two times, once with the Pico plugged in and another one without, then see which device disappeared in the second run.

On a Windows computer the device name will have the format COM<n> where <n> is a number. You can open the Device Manager in the Control Panel and look under “Ports (COM & LPT)” for the list of serial devices. If you cannot identify which of the devices listed there is your Pico, unplug it from USB and check the list again to find which one is now missing.

Great, now you know what serial device your computer is using to connect to the Pico microcontroller.

Using mpfshell

The Pico behaves like a standard serial device, so it can be accessed with any serial terminal program available for your operating system. In this article you are going to use a tool called mpfshell, a tool that is designed to connect to and manage MicroPython microcontroller boards.

Open a terminal window in your computer and create a new directory where you will store your Pico project:

mkdir pico-mute
cd pico-mute

Create and activate a Python virtual environment. If you are using a Mac or Linux computer, do it as follows:

python3 -m venv venv
source venv/bin/activate

On a Windows computer, use the following commands:

python -m venv venv
venv\Scripts\activate

Make sure that the Python virtual environment is activated by checking that your command prompt was modified to include a (venv) prefix.

Next install the mpfshell tool in your virtual environment:

pip install mpfshell

Make sure the Pico is still connected to your computer with the USB cable, and then execute the following command to connect to it from your computer:

mpfshell <your-pico-serial-device>

Make sure to replace <your-pico-serial-device> with the serial device name assigned to your Pico in the command above. If you are doing this on a Linux or Mac computer, you do not need to include the /dev/ prefix. For example, on my Mac computer the command that I use is:

mpfshell tty.usbmodem143101

On Windows, assuming the Pico is connected to COM3, the command would be:

mpfshell COM3

The output of mpfshell should look as follows:

(venv) $ mpfshell tty.usbmodem143101
Connected to rp2040

** Micropython File Shell v0.9.2, sw@kaltpost.de **
-- Running on Python 3.8 using PySerial 3.5 --

mpfs [/]>

The command should end with a new prompt. Type help in this prompt to see all the commands mpfshell supports:

mpfs [/]> help

Documented commands (type help <topic>):
========================================
EOF  cd     exec  get   lcd  lpwd  md    mput  mrm   put   pwd   rm
cat  close  exit  help  lls  ls    mget  mpyc  open  putc  repl

For now, let’s concentrate on the repl command, which starts an interactive shell on your Pico device:

mpfs [/]> repl
>

*** Exit REPL with Ctrl+] ***

Adafruit CircuitPython 6.2.0 on 2021-04-05; Raspberry Pi Pico with rp2040
>>>

Does this look familiar? If you’ve ever used a Python REPL you will surely recognize the >>> prompt. You can now enter Python statements, exactly like you would in a Python shell running on your computer. But keep in mind that any statements that you enter in this shell will be executed by the Pico board. Your computer is just acting as a dumb terminal.

To exit the CircuitPython shell, press Ctrl-] and that will bring you back to the mpfshell prompt. Then press Ctrl-D at the mpfshell prompt to exit back to your terminal.

If all you want to do is access the MicroPython REPL, then you can skip the intermediate step of the mpfshell prompt by adding -c repl at the end of the mpfshell command:

mpfshell <your-pico-serial-device> -c repl

Building the circuit

We’ve made some good progress on the software front, so now it is time to concentrate on the hardware.

The following diagram shows how to wire the push button to the Pico using two jumper wires. We’ll go over these connections step-by-step to make sure you do it correctly.

Wiring diagram

In addition to the actual wiring diagram, below you can see the pin layout of the Raspberry Pi Pico. You do not need to fully understand this diagram, but it is good to have it present and use it as a reference.

Pico pinout diagram

Positioning the breadboard

To begin, position your breadboard in landscape orientation, with the numbers printed on it increasing from the right to the left. For this project we are going to use the 10 rows of holes, highlighted below:

Breadboard

This part of the breadboard has 30 columns of holes, numbered 1 to 30 starting from the right. The five rows in the upper half are labeled a to e, while the five rows in the bottom half are labeled f to j.

On any given column, the group of 5 vertical holes (either in the top or in the bottom halves) are all internally connected to each other, and this is what makes it easy to make connections between components, as you will soon see.

Installing the Raspberry Pi Pico on the breadboard

Take the Raspberry Pi Pico with the micro-USB port pointing to the right, and carefully align the header pins with the breadboard holes. You want the right-most top and bottom pins of the Pico to match the 1c and 1h holes. Once you have it in the right position, gently press on the board to allow the pins to go in as far as they can.

It is important that the board is vertically centered on the breadboard surface, which should leave the a and b rows of holes visible right above it, and the i and j rows below it. These two rows of holes will allow us to make connections to the terminals of the Pico board.

Installing the push button

The next step is to insert your push button somewhere in the free part of the breadboard, to the left of the Pico board. For this you need to be very careful and understand how your push button’s connectors work.

Push buttons have two terminals, but many models (like the one I’m using) come with each terminal doubled, so while there are still two actual connections, there are four physical pins.

Before you install the button on the breadboard you need to identify the pairs of pins that go together. If you have the same style of push button, when you orient the button so that there are two pins on the top and two on the bottom, the two left-side pins are connected to one of the terminals, and the two right-side pins are on the other. To connect this button you have to pick one of the pins on the left and one on the right.

For convenience when wiring, the button should be installed on the breadboard in such a way that the two terminals (or the two terminal groups) end up on different columns on the breadboard. In the case of a button with double terminals, you can position the two paired terminals above and below the middle line of the breadboard respectively, as I’ve done in the picture above.

The installation that I used has the button’s connectors on columns 25 and 27 of the breadboard, with the terminals on rows d and g. To connect this button you will need a wire in any of the open holes in column 25 (top or bottom, whatever is most convenient) and another in any of the holes in column 27.

Installing the jumper wires

The last wiring step is to connect the Pico to the button with two jumper wires.

Take one of the wires, and plug one of the ends in the hole 1a. This hole is connected to pin 1 of the Pico board, also known as GP0. This is one of the Pico’s GPIO (General Purpose Input/Output) pins, used for connections to other devices.

Insert the other end of the wire next to one of the button’s connectors. It doesn't really matter which one, it can be any of them. I connected it on the 25a hole, which is connected to the right-side terminals of the push button.

The second wire is connected to the Pico on the bottom side, on the 18j hole, which is connected to pin 23. According to the pin diagram this pin is a “ground” pin, or GND, which means that when anything is connected to it, it attracts electricity towards itself.

Connect the other end of the second wire to the second button terminal. In my case I inserted this end of the wire on the 27j breadboard hole, which is in the same group as the lower left-side button terminal.

Testing the push button

The hardware circuit is now complete, so for the next step you are going to write a short CircuitPython application that prints something when the button is pressed.

Remember the code.py file you wrote at the beginning of this tutorial, which made the LED in the Pico board blink three times? Let’s extend that program to also blink the LED when the button is pressed.

Open code.py on your text editor or IDE and copy the following code to it, replacing the previous contents of the file. The parts of this code that are new are highlighted.


import board
import digitalio
import time

gp0 = digitalio.DigitalInOut(board.GP0)
gp0.direction = digitalio.Direction.INPUT
gp0.pull = digitalio.Pull.UP

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = False

# blink the LED three times
for _ in range(3):
    # light up the LED
    led.value = True

    # wait 0.5 seconds
    time.sleep(0.5)

    # turn off the LED
    led.value = False

    # wait 0.5 seconds
    time.sleep(0.5)

while True:
    # block while button is not pressed
    while gp0.value:
        pass
    print("Button pushed!")

    # light up the LED
    led.value = True

    # wait 0.5 seconds
    time.sleep(0.5)

    # turn off the LED
    led.value = False

    # wait for button to be released
    while not gp0.value:
        pass

With this version we are initializing two variables gp0 and led. These are digital pins of the Pico board that can function as inputs or outputs. We are using the direction attribute to indicate that the gp0 pin is an input pin and the led pin is an output pin. That means that the expression gp0.value will give us the state of the GP0 pin (a True or False value), while setting led.value to True or False will operate the LED.

The gp0 pin is also configured as a “pull-up” pin. Pull-up pins have some special wiring (that is implemented internally by the Pico board). These are often used to read buttons or other types of switches. The idea is that a pull-up pin is configured internally to have a charge, which means that it reads as a high or True value when it is not connected to anything.

In our circuit, the GP0 pin is connected to a GND (ground) pin with the button in between. When the button is pressed, the connection between these two will be made, and the charge in the GP0 pin will drain towards the ground, changing the value of the pin to low or False. In  other words, this pin will have a default value of True, and only when the button is pressed the value will change to False.

You’ve seen the for-loop in the first version of this file. This loop blinks the LED three times. While this isn’t necessary for our purposes, I find it useful as a visual signal that the board is actually running the code.

The while-loop that follows starts with an inner while-loop that blocks while gp0.value is True. This is the high state for the GP0 pin, which corresponds to the button not being pressed.

As soon as the button is pressed gp0.value will change to False and that first inner while-loop will exit. At this point we print a debugging message, we turn the LED on, wait for half a second, and then turn it off again.

The last part of the outer while-loop is another inner while-loop that waits for the GP0 pin to go back to the high or True value. The idea here is that we want to prevent duplicate firings of the button, so we want to wait for the user to release the button before we end the loop iteration and go back to the top to wait for another press.

Save code.py locally on your computer, and then drop the file on the CIRCUITPY disk drive. A moment later the board will reboot and execute the code automatically. You will know when the board is ready because you will see the LED blink three times. If you do not see the LED blink, it could be because CircuitPython is still inside a shell sessio, so it is not rebooting automatically when code.py changes. You can disconnect it from power and connect it again to make sure it runs your code.

After the LED does its blinking dance, try pressing the button to see if the LED reacts to it.

Does the LED blink as a response to a button press? Great! You are ready to move on to the last step of the tutorial. If the LED does not blink, then something isn’t working, so you’ll need to debug the problem.

To debug this application, start a REPL with mpfshell:

mpfshell <your-pico-serial-device> -c repl

Now press Ctrl-D to force the Pico to do a soft-reboot and run code.py from scratch. At this point the code will run while you are connected on the terminal, so if there are any errors they’ll be printed to the screen. This should help you debug any issues.

Emulating a keyboard

We are now very close to having this project completed. The last change we need to make to our code.py file is to emit a key to the computer through the USB connection. The computer will be fooled into thinking the key is coming from an actual USB keyboard.

Here is the final code.py, with the changes from the previous version highlighted:


import board
import digitalio
import time
import usb_hid
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode

gp0 = digitalio.DigitalInOut(board.GP0)
gp0.direction = digitalio.Direction.INPUT
gp0.pull = digitalio.Pull.UP

led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
led.value = False

keyboard = Keyboard(usb_hid.devices)

# blink the LED three times
for _ in range(3):
    # light up the LED
    led.value = True

    # wait 0.5 seconds
    time.sleep(0.5)

    # turn off the LED
    led.value = False

    # wait 0.5 seconds
    time.sleep(0.5)

while True:
    # block while button is not pressed
    while gp0.value:
        pass
    print("Button pushed!")

    # light up the LED
    led.value = True

    # send Cmd+Shift+A
    keyboard.send(Keycode.GUI, Keycode.SHIFT, Keycode.A)

    # wait 0.5 seconds
    time.sleep(0.5)

    # turn off the LED
    led.value = False

    # wait for button to be released
    while not gp0.value:
        pass

The USB HID support in CircuitPython requires us to import three new packages:

  • usb_hid: the low-level HID support
  • adafruit_hid.keyboard.Keyboard: the high-level keyboard support class
  • adafruit_hid.keycode.Keycode: key constants

In addition to the gp0 and led variables, we now add keyboard, which is initialized as an instance of the Keyboard class from the Adafruit HID library. This object is what will make the Pico board appear to the computer as a USB keyboard.

To send a key to the computer, we use the keyboard.send() method. Depending on the use you intend to give this application, the key that you send might need to change. The Zoom application uses the Cmd+Shift+A keyboard shortcut to toggle the audio on Mac, so this is what I implemented:

    keyboard.send(Keycode.GUI, Keycode.SHIFT, Keycode.A)

In CircuitPython the Mac’s Command key is called Keycode.GUI.

For a Windows computer, Zoom uses the Ctrl-A shortcut, so you would use this instead:

    keyboard.send(Keycode.CTRL, Keycode.A)

You can change the key combination to be anything you want. And if you want to test this without using Zoom or any other application, you can just make the program output a letter A:

    keyboard.send(Keycode.A)

To test the application this way, make sure you open a text editor so that you can see the letter appear as you push the button.

Next steps with the Raspberry Pi Pico

I hope you had fun working on this project, and that this motivates you to continue working with MicroPython and microcontrollers.

Are you interested in more articles about the Raspberry Pi Pico? I’ve also written an introductory tutorial that uses the official MicroPython build instead of CircuitPython.

I’d love to see what you build!

Miguel Grinberg is a Principal Software Engineer for Technical Content at Twilio. Reach out to him at mgrinberg [at] twilio [dot] com if you have a cool project you’d like to share on this blog!