Raspberry Pi Pico W beginners’ components tutorial

raspberry pi pico w components tutorial

This Raspberry Pi Pico W tutorial will teach you how to interact with the microcontroller to interact with simple components such as LEDs, ultrasonic sensors and many other items found in a beginner’s kit.

If you are a complete beginner, this tutorial will help you understand MicroPython so that you can read, write and modify code. With this knowledge, you can combine code and components to create something that has a real-world use case.

If you are coming from a Pico, this tutorial will teach you how to control the Pico W wirelessly. Previously, you could only interact with the Pico through a switch, button or some physical interaction device. Not anymore! You can now control the components with your phone or desktop.

Tutorial flow

Introduction

  1. Soldering header pins
  2. Using Thonny
  3. Updating your firmware

The Hello World tutorial for the Pico W

  1. Serve a web page that says “Hello World” on the Pico
  2. Controlling a LED wirelessly

One step up

  1. Control RGB LED wirelessly
  2. Buzzer operation on the Pico W

Basic ways to transmit data from sensor

  1. Pico W and HC-SR04 ultrasonic sensor
  2. Transmit a web page with sensor data
  3. Reducing payload with AJAX
  4. Pimoroni Phew to streamline endpoint coding

Connecting to web utilities

  1. Log DHT22 sensor climate data to Google Sheets with IFTTT
  2. Build a Spotify remote with play/pause/skip functions

No-code GPIO control with PiCockpit

  1. Super simple PiCockpit installation on Pico W
  2. Simple LED control with PiCockpit and Pico W
  3. Pico W, 5V fan and a transistor, controlled by PiCockpit

MQTT

  1. Display photoresistor using MQTT and Node-RED with Pico W

Table of contents

Contents hide

Tutorial goals in summary

  • Learn how to interact with fundamental components that make up larger projects
  • Control all of these wirelessly — no switches, buttons or other interaction devices.
  • Gain a better understanding of the Pico W’s strengths

Important links

Github repo for tutorial code (except secrets.py)

MicroPython documentation

Errors, suggestions, comments? Leave a comment in the comment box below, email me or Tweet me.

OSError: [Errno 98] EADDRINUSE

If you get this error, just unplug and plug in your Raspberry Pi Pico.

You can also do this by typing these commands into Thonny’s Shell:

import machine
machine.reset()

Result:

Soldering header pins

When you buy a Raspberry Pi Pico W, it might not come with headers that will allow you to connect components to your Pico.

At the time of writing, the Pico WH (H for headers) hasn’t been released. Our Pico W mega-article is keeping track of its release.

If, however, you can find a Pico W with pre-soldered headers, I’d advise you to buy that.

Nonetheless, for the rest of us, soldering headers onto the Pico W is a simple thing to do. You will need:

  • Breadboard
  • Soldering iron and solder
  • Headers

When you buy your headers, make sure to buy one that’s meant for the Pico W. Unlike headers for the Raspberry Pi Zero-series, the headers on the Pico W aren’t side-by-side. They are on opposite ends of the board.

A breadboard, the Pico W and 2×20 pin headers

The pins have a long side and a short side. You’ll want the longer pins to be on the side where you see the GPIO labels (GP0, GP1, GND, VBUS, VSYS, etc.)

You will want the longer pins to be on the side opposite where the USB connector is.

Therefore, insert the longer pins into the breadboard. You will need four holes’ and a gutter worth of spacing between them. If unsure, test with your Pico W.

Soldering is easy. Make sure your soldering iron is hot, and make sure to use the tip of the soldering iron.

What I found most effective was to get the soldering iron’s tip close to the pin, get the solder touching the tip and see it flow down the pin and create a connection.

After soldering, make sure to check and ensure there isn’t any extraneous solder that could be connecting two GPIO pins together or some leftover waste solder that’s on your board.

Use Thonny as your code editor

Thonny programming a Raspberry Pi Pico W

Thonny remains the easiest way to program your Raspberry Pi Pico W.

If you are using the Raspberry Pi OS, you would already have it installed.

However, if you are using Windows or Mac, you will need to download it and configure it.

Here’s a guide that will walk you through the steps.

Also make sure to refer to the guide on how to upload files onto your Pico W.

Update your firmware by uploading the latest UF2

When you buy your Pico W, you might have already have the outdated firmware.

Many changes are coming to the Pico W, so it’s ideal to update your firmware now.

Some changes that I have seen since the day of release are improvements in the WLAN’s access point function, and future updates could unlock the Bluetooth function on the board.

Update now! Here’s a guide in our mega-article.


1. Serve a web page that says “Hello World” on the Pico

The “Hello World” project of the Raspberry Pi Pico W

One of the most fundamental projects in all programming tutorials is the “Hello World” project.

A microcontroller’s “Hello World” usually involves blinking an LED. It’s super easy. Here’s how.

However, since the Pico W can serve a web page, let’s start by learning how to serve a web page that has a “Hello World” message.

The setup used here will form the most fundamental building block for the rest of the tutorials.

There are two ways you can connect to the Pico W. You can make it join a WiFi network or you can broadcast a SoftAP hotspot similar to what your smartphone does. If you want to try the latter, follow this link. However, for consistency throughout this tutorial, we will always connect to a WiFi network rather than broadcast one from the Pico W.

So, fundamentally, the steps for serving a web page involves:

  • Connecting to WiFi
  • Writing code to serve index.html to anyone who connects to the Pico W’s IP address

Let’s set up a few files. Save these and upload them to your Raspberry Pi Pico W. Here’s how to upload files, in case you missed it.

wifi.py

wifi.py is a boilerplate meant to help you to connect to your WiFi network. Creating a separate file and importing it into main.py file later will help reduce cluttered code.

Note that you should change your country’s code in the line rp2.country(‘DE’) if your country is not Germany.

import rp2
import network
import ubinascii
import machine
import urequests as requests
import time
from secrets import secrets


def init_wifi():
    # Set country to avoid possible errors
    rp2.country('DE')

    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)

    # Load login data from different file for safety reasons
    ssid = secrets['ssid']
    pw = secrets['pw']

    wlan.connect(ssid, pw)

    # Wait for connection with 10 second timeout
    timeout = 10
    while timeout > 0:
        if wlan.status() < 0 or wlan.status() >= 3:
            break
        timeout -= 1
        print('Waiting for connection...')
        time.sleep(1)

    # Define blinking function for onboard LED to indicate error codes    
    def blink_onboard_led(num_blinks):
        led = machine.Pin('LED', machine.Pin.OUT)
        for i in range(num_blinks):
            led.on()
            time.sleep(.2)
            led.off()
            time.sleep(.2)

    wlan_status = wlan.status()
    blink_onboard_led(wlan_status)

    if wlan_status != 3:
        raise RuntimeError('Wi-Fi connection failed')
    else:
        print('Connected')
        status = wlan.ifconfig()
        print('ip = ' + status[0])

secrets.py

wifi.py imports secrets.py, where you store your WiFi network’s info.

secrets.py is a simple JSON file which contains your WiFi SSID and password.

secrets = {
    'ssid': 'SM-A520W9371',
    'pw': 'starting',
    }

serve_webpage.py

As the name suggests, this page serves websites to users who connect to the Pico W.

Upon receiving a connection, the Pico W finds a file called index.html and sends it to the connected client, as seen in the line response = get_html(‘index.html’).

import socket

def serve_webpage():
    #Function to load in html page    
    def get_html(html_name):
        # open html_name (index.html), 'r' = read-only as variable 'file'
        with open(html_name, 'r') as file:
            html = file.read()
            
        return html

    # HTTP server with socket
    addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

    s = socket.socket()
    s.bind(addr)
    s.listen(1)

    print('Listening on', addr)

    # Listen for connections
    while True:
        try:
            cl, addr = s.accept()
            print('Client connected from', addr)
            response = get_html('index.html')
            cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
            cl.send(response)
            cl.close()
            
        except OSError as e:
            cl.close()
            print('Connection closed')

Finally, we need to create the index.html file that will be sent to a connected client.

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
    <body>
        <h1>Hello World</h1>
    </body>
</html>

This is a simple HTML boilerplate with two changes: one to the <title> tag which outputs “Pico W” as the title and the <h1> tag which says “Hello World”.

main.py

Since we have placed all the code elsewhere, our main.py file just has to import and call these functions in order to serve the Hello World web page.

As you can see, we initialize WiFi first before serving the web page.

from wifi import init_wifi
from serve_webpage import serve_webpage

init_wifi()
serve_webpage()

You’re almost there!

Referring to the screenshot below, make sure:

  • You have uploaded five files into your Raspberry Pi Pico W (see bottom left red box)
  • Make sure your interpreter is set to MicroPython (Raspberry Pi Pico) (see bottom right box)
  • Then, highlight the main.py in your code editor, and click the green run button (top left red box)
  • Once you run this, you will see your IP address in the Shell. Go to your browser and type in this address and you will see the Hello World web page.
  • If your Shell is not open, go to View -> Shell.

If all is successful, you will see the page below.


2. Controlling a LED wirelessly

Now that you have the basics set up, let’s go one step forward.

One of the most fundamental Raspberry Pi projects involves blinking an LED.

Let’s up that a notch by controlling the LED wirelessly. We want to be able to blink, turn on and off the LED.

In order to do this, you will need to set up a circuit and a web server with three buttons — ON, OFF, BLINK.

For this project, you will need an LED, a 330 ohm resistor, one jumper wire and a breadboard.

We will use a red LED because most kits will have it. Do note that if you use an LED of any other colour, you will need to adjust the resistor.

Here’s how to connect the components

  • GPIO 2 -> LED’s long leg (anode/positive)
  • GND -> 330 ohm resistor -> LED’s short leg (cathode/negative)

Code to control LED on Pico W

Let’s build upon what we did in the previous tutorial. The only two files we will need to modify are index.html to add buttons and main.py to interact with the LED based on the input from index.html.

The parts that are bold indicate the new lines that were added to index.html. They add three buttons and a paragraph that says “Control the LED”.

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
    <body>
        <h1>Pico W</h1>
        <p>Control the LED</p>
        <a href=\"?led=on\"><button>ON</button></a>&nbsp;
        <a href=\"?led=off\"><button>OFF</button></a>
        <a href=\"?led=blink\"><button>BLINK</button></a>
    </body>
</html>

Notice when you press the buttons, you will see a parameter added to your Pico W’s IP address (e.g. http://192.168.43.134/%22?led=blink\). These parameters are captured by the Pico W’s backend and controls the LED.

We are going to move serve_webpage.py‘s code into the main.py file.

Here’s main.py:

from wifi import init_wifi
import socket
import machine
import time

init_wifi()


#LED controls
led = machine.Pin(2, machine.Pin.OUT)
      
def blink_led():
    led.on()
    time.sleep(0.2)
    led.off()
    time.sleep(0.2)

#Function to load in html page    
def get_html(html_name):
    # open html_name (index.html), 'r' = read-only as variable 'file'
    with open(html_name, 'r') as file:
        html = file.read()
        
    return html

# HTTP server with socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.bind(addr)
s.listen(1)

print('Listening on', addr)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('Client connected from', addr)
        request = cl.recv(1024)
        print(request)
        
        request = str(request)
        led_on = request.find('?led=on')
        led_off = request.find('?led=off')
        led_blink = request.find('?led=blink')
        print('led_on = ', led_on)
        print('led_off = ', led_off)
        print('led_blink = ', led_blink)
        if led_on > -1:
            print('LED ON')
            led.on()
            
        if led_off > -1:
            print('LED OFF')
            led.off()
            
        if led_blink > -1:
            print('LED BLINK')
            blink_led()
            
        response = get_html('index.html')
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()
        
    except OSError as e:
        cl.close()
        print('Connection closed')

The first, and key, segment is below:

request = cl.recv(1024)
        print(request)
        
        request = str(request)
        led_on = request.find('?led=on')
        led_off = request.find('?led=off')
        led_blink = request.find('?led=blink')
        print('led_on = ', led_on)
        print('led_off = ', led_off)
        print('led_blink = ', led_blink)
        if led_on > -1:
            print('LED ON')
            led.on()
            
        if led_off > -1:
            print('LED OFF')
            led.off()
            
        if led_blink > -1:
            print('LED BLINK')
            blink_led()

The variable “request”, when printed, outputs the first block of text below. The last three lines are the print statements that check for LED on, off or blink:

b'GET /%22?led=blink\\%22 HTTP/1.1\r\nHost: 192.168.43.134\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nReferer: http://192.168.43.134/%22?led=on\\%22\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: en-US,en;q=0.9,es;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5\r\n\r\n'
led_on =  456
led_off =  -1
led_blink =  10

The code above will try to search out specific strings such as “led=on“. If it exists, the value will be more than -1, therefore triggering the relevant if statement.

For example, if led=on exists in the parameters, then the variable led_on will be more than -1, therefore the if led_on > -1 statement triggers and runs led.on();

The only complicated bit here is the led_blink function which will trigger the function:

    def blink_led():
        led.on()
        time.sleep(0.2)
        led.off()
        time.sleep(0.2)

Finally, here’s how you initialize GPIO 2 to operate the LED:

import machine
import time
    
#LED controls
led = machine.Pin(2, machine.Pin.OUT)

We import machine to interact with the GPIO pin. As you can see in the variable led, we want the Pico W to power GPIO 2.

We import time so that we can have a pause of 0.2 seconds in blink_led().


3. RGB LED on Pico W

To light up your RGB LED, you need:

  • Three 330-ohm resistors
  • One RGB LED
  • One jumper wire

On the RGB LED, you will find four legs. One leg will be the longest. That is either a cathode (negative) or anode (positive). My RGB LED had a shared cathode so here’s the connection:

Image shows R, G, B legs on a common cathode RGB LED. (Image from Raspberry Pi Foundation CC-BY-SA)
  • GPIO 15 -> 330-ohm resistor -> red LED
  • GPIO 17 -> resistor -> green LED
  • GPIO 16 -> resistor -> blue LED

Here’s some code to see if you wired it up right:

import machine

red_led = machine.PWM(machine.Pin(15))
green_led = machine.PWM(machine.Pin(17))
blue_led = machine.PWM(machine.Pin(16))

red_led.duty_u16(65534)
green_led.duty_u16(65534)
blue_led.duty_u16(65534)

In this example, we are using PWM, which allows you to vary the brightness of the R, G, B LEDs.

You can change the values passed to [colour].duty_u16 to a value between 0 and 65534. Theoretically, you should be able to pass 65535, but somehow that doesn’t seem to work for me.

Think of passing “0” as saying you want zero per cent brightness. If you pass it 65534, you want 100 per cent brightness.

If you do 65534 for one colour and zero for the rest, you will be able to tell whether you have connected the right GPIO pins to the right LED colour.

So why are we using PWM? Because it will help you get more colours. If you just use an “on-and-off” method, you can get red, green, blue, white-ish and no light. With PWM, you can vary the intensity of the R, G, B LED and create as many colours as you can imagine.

Now, let’s create something that you can control wirelessly!

index.html

The main change here is to have a <form> which has three sliders. These sliders have a value between zero and 100.

Using values zero to 100 helps you visualize brightness as a percentage. It also reduces the amount of code needed to parse out the value from the parameters (seen later in main.py)

When you have set the values for the R, G, B LEDs, you would press submit, and this data is captured by the Pico W.

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
      <body>
        <h1>Pico W RGB LED</h1>
        <form id="form">
            
            <input type="range" min="0" max="100" value="slider_value" name="red" id="red">
            <label for="red">R</p>
            
            <input type="range" min="0" max="100" value="slider_value" name="green" id="green">
             <label for="green">G</p>
            
            <input type="range" min="0" max="100" value="slider_value" name="blue" id="blue">
            <label for="blue">B</p>
            
            <input type="submit" id="submit">
        </form>
        <script>
        
            submit.addEventListener('click', function ()  {
                form.submit();
            }, false);
        
        </script>
</html>

main.py

from wifi import init_wifi
import socket
import machine
import time

init_wifi()

#LED controls
red_led = machine.PWM(machine.Pin(15))
green_led = machine.PWM(machine.Pin(17))
blue_led = machine.PWM(machine.Pin(16))

#Function to load in html page    
def get_html(html_name):
    # open html_name (index.html), 'r' = read-only as variable 'file'
    with open(html_name, 'r') as file:
        html = file.read()
    return html

def find_intensity(color, request_str):
    index = request_str.find(color) + len(color)
    offset = 0
    if request_str[index].isdigit():
        offset = 1
        if request_str[index+1].isdigit():
            offset = 2
            if request_str[index+2].isdigit():
                offset = 3

    intensity = int(request_str[index:index+offset])
    return intensity
    
    
# HTTP server with socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.bind(addr)
s.listen(1)

print('Listening on', addr)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('Client connected from', addr)
        request = cl.recv(1024)
        
        request_str = str(request)
       
        #find intensity ONLY if the params exist in request
        if request_str.find('red') > -1 :
            #int = get rid of decimal
            #/100*65534 = find_intensity returns something 0 to 100,
            # so x/100 = proportion of 65534 that you want to send to LED
            # 65534 = max number you can use for PWM
            red_intensity = int(find_intensity('red=', request_str) /100 * 65534)
            green_intensity = int(find_intensity('green=', request_str) /100 * 65534)
            blue_intensity = int(find_intensity('blue=', request_str) /100 * 65534)
            
            #print('r=' + str(red_intensity))
            #print('g=' + str(green_intensity))
            #print('b=' + str(blue_intensity))
            
            red_led.duty_u16(red_intensity)
            green_led.duty_u16(green_intensity)
            blue_led.duty_u16(blue_intensity)
                        
        response = get_html('index.html')
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()
        
    except OSError as e:
        cl.close()
        print('Connection closed')

As you press submit on the index.html web page, the Pico W will take the parameters and process it.

Let’s look at some key points in the code, bolded in the code block above.

find_intensity function

This function takes two params: color and request_str. color takes in “red”, “green” or “blue” and finds the value after the equals (=) sign.

For example, your URL after you submit the form is “http://192.168.1.142/?red=89&green=50&blue=50”.

If you pass “red” to find_intensity, it will return 89.

intensity parser

The second bolded block of code represents the code that tells the Pico W’s GPIO how much brightness you want from each LED.

First up, we must ensure that the params exist in the URL. This is done by the if statement request_str.find(‘red’) > -1. I just used ‘red’ because the params will 100% contain the string ‘red’ if you use the form.

If you are visiting your Pico W’s IP address (e.g. http://192.168.1.142/) for the first time, you won’t have the params, so the program will crash if you ran find_intensity.

If params exist, we find the intensity for each LED according to the values submitted. Let’s take a look at red_intensity.

red_intensity = int(find_intensity('red=', request_str) /100 * 65534)
...
red_led.duty_u16(red_intensity)

find_intensity returns an integer zero to 100. We divide this by 100 so you can get a percentage. This percentage divides the maximum value that the duty_u16 method can take.

However, we need an int function to wrap this because you can get a float sometimes and duty_u16 needs an int. For example, say you wanted 41% brightness — 41% of 65534 is 26,868.94. You can’t pass this float as the program will crash.


4. Buzzer operation on the Pico W

A buzzer is a pretty important component for basic notifications, just like how an LED can tell you about the state of something, a buzzer give you an auditory notification.

Connect the buzzer’s positive terminal to GPIO 16 and ground to a GND pin on the Pico W.

Rather than implementing my own code, I’ve chosen to use Giuseppe Cassibba’s implementation. If you ran his code on the Pico W, you will hear beeps on your buzzer.

But since this code is meant for the Pico, it won’t have any wireless interactivity features.

So, let’s get the fork out!

First, let’s modify the index.html to implement the buttons. Let’s have a button for “ON”, “OFF”, “SCALE”, “MUSIC”.

The first two buttons are self-explanatory. The third button plays a C-scale and the last one plays a piece of music.

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
    <body>
        <h1>Pico W Buzzer</h1>
        <p>Control the buzzer</p>
        <a href=\"?buzzer=on\"><button>ON</button></a>&nbsp;
        <a href=\"?buzzer=off\"><button>OFF</button></a>
        <a href=\"?buzzer=scale\"><button>SCALE</button></a>
        <a href=\"?buzzer=music\"><button>MUSIC</button></a>
    </body>
</html>

Just like before, we create a few <button> tags with <a> tags surrounding them. Upon clicking the buttons, the URL will have a parameter such as /buzzer=on. The Pico W reads this and turns the buzzer on.

Now, let’s look at Giuseppe’s code for the Pico:

from machine import Pin, PWM
from time import sleep

buzzerPIN=16
BuzzerObj=PWM(Pin(buzzerPIN))

def buzzer(buzzerPinObject,frequency,sound_duration,silence_duration):
    # Set duty cycle to a positive value to emit sound from buzzer
    buzzerPinObject.duty_u16(int(65536*0.2))
    # Set frequency
    buzzerPinObject.freq(frequency)
    # wait for sound duration
    sleep(sound_duration)
    # Set duty cycle to zero to stop sound
    buzzerPinObject.duty_u16(int(65536*0))
    # Wait for sound interrumption, if needed 
    sleep(silence_duration)


# Play following notes by changing frequency:
#C (DO)
buzzer(BuzzerObj,523,0.5,0.1)

#D (RE)
buzzer(BuzzerObj,587,0.5,0.1)

#E (MI)
buzzer(BuzzerObj,659,0.5,0.1)

#F (FA)
buzzer(BuzzerObj,698,0.5,0.1)

#G (SOL)
buzzer(BuzzerObj,784,0.5,0.1)

#A (LA)
buzzer(BuzzerObj,880,0.5,0.1)

#B (SI)
buzzer(BuzzerObj,987,0.5,0.1)

#Deactivates the buzzer
BuzzerObj.deinit()

His code plays a C-scale when run using the function buzzer(), which takes in four parameters: Buzzer object, frequency in Hertz, sound duration and pause duration before playing the next sound.

Let’s modify the code so that we can activate the ON, OFF, SCALE and MUSIC.

Here’s how we integrate Giuseppe’s code into our main.py:

from wifi import init_wifi
import socket
import machine
import time

init_wifi()

# Buzzer
buzzerPIN = 16
BuzzerObj = machine.PWM(machine.Pin(buzzerPIN))


def buzzer(buzzerPinObject, frequency, sound_duration, silence_duration):
    # Set duty cycle to a positive value to emit sound from buzzer
    buzzerPinObject.duty_u16(int(65536*0.2))
    # Set frequency
    buzzerPinObject.freq(frequency)
    # wait for sound duration
    time.sleep(sound_duration)
    # Set duty cycle to zero to stop sound
    buzzerPinObject.duty_u16(int(65536*0))
    # Wait for sound interrumption, if needed
    time.sleep(silence_duration)

# Function to load in html page


def get_html(html_name):
    # open html_name (index.html), 'r' = read-only as variable 'file'
    with open(html_name, 'r') as file:
        html = file.read()

    return html


# HTTP server with socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.bind(addr)
s.listen(1)

print('Listening on', addr)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('Client connected from', addr)
        request = cl.recv(1024)

        request = str(request)
        buzzer_on = request.find('?buzzer=on')
        buzzer_off = request.find('?buzzer=off')
        buzzer_scale = request.find('?buzzer=scale')
        buzzer_music = request.find('?buzzer=music')

        if buzzer_on > -1:
            BuzzerObj.duty_u16(int(65536*0.2))
            BuzzerObj.freq(440)

        if buzzer_off > -1:
            BuzzerObj.duty_u16(0)

        if buzzer_scale > -1:
            #C (DO)
            buzzer(BuzzerObj, 523, 0.5, 0.1)

            #D (RE)
            buzzer(BuzzerObj, 587, 0.5, 0.1)

            #E (MI)
            buzzer(BuzzerObj, 659, 0.5, 0.1)

            #F (FA)
            buzzer(BuzzerObj, 698, 0.5, 0.1)

            #G (SOL)
            buzzer(BuzzerObj, 784, 0.5, 0.1)

            #A (LA)
            buzzer(BuzzerObj, 880, 0.5, 0.1)

            #B (SI)
            buzzer(BuzzerObj, 987, 0.5, 0.1)

            BuzzerObj.deinit()

        if buzzer_music > -1:
            pass

        response = get_html('index.html')
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        cl.close()
        print('Connection closed')

In the above implementation, we haven’t written the code for MUSIC.

The code is very similar to the red LED tutorial above, where the Pico W captures the parameters after the Pico W’s IP address. If the param buzzer=ON, it will play a 440Hz sound. If buzzer=scale, it will play the scale that’s taken from Giuseppe’s code.

What about implementing music? Implementing music is a bit more complicated so we should create a new file called constants.py and add a few lines into our main.py.

This code is a fork from Rowan Packard’s Arduino code.

constants.py

A3F = 208  # 208 Hz
B3F = 233  # 233 Hz
B3 = 247  # 247 Hz
C4 = 261  # 261 Hz MIDDLE C
C4S = 277  # 277 Hz
E4F = 311  # 311 Hz
F4 = 349  # 349 Hz
A4F = 415  # 415 Hz
B4F = 466  # 466 Hz
B4 = 493  # 493 Hz
C5 = 523  # 523 Hz
C5S = 554  # 554 Hz
E5F = 622  # 622 Hz
F5 = 698  # 698 Hz
F5S = 740  # 740 Hz
A5F = 831  # 831 Hz

main.py (additions bolded)

from wifi import init_wifi
import socket
import machine
import time
from constants import *

init_wifi()

# Buzzer
buzzerPIN = 16
BuzzerObj = machine.PWM(machine.Pin(buzzerPIN))


def buzzer(buzzerPinObject, frequency, sound_duration, silence_duration):
    # Set duty cycle to a positive value to emit sound from buzzer
    buzzerPinObject.duty_u16(int(65536*0.2))
    # Set frequency
    buzzerPinObject.freq(frequency)
    # wait for sound duration
    time.sleep(sound_duration)
    # Set duty cycle to zero to stop sound
    buzzerPinObject.duty_u16(int(65536*0))
    # Wait for sound interrumption, if needed
    time.sleep(silence_duration)

# Function to load in html page


def get_html(html_name):
    # open html_name (index.html), 'r' = read-only as variable 'file'
    with open(html_name, 'r') as file:
        html = file.read()

    return html


# HTTP server with socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.bind(addr)
s.listen(1)

print('Listening on', addr)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('Client connected from', addr)
        request = cl.recv(1024)

        request = str(request)
        buzzer_on = request.find('?buzzer=on')
        buzzer_off = request.find('?buzzer=off')
        buzzer_scale = request.find('?buzzer=scale')
        buzzer_music = request.find('?buzzer=music')

        if buzzer_on > -1:
            BuzzerObj.duty_u16(int(65536*0.2))
            BuzzerObj.freq(440)

        if buzzer_off > -1:
            BuzzerObj.duty_u16(0)

        if buzzer_scale > -1:
            #C (DO)
            buzzer(BuzzerObj, 523, 0.5, 0.1)

            #D (RE)
            buzzer(BuzzerObj, 587, 0.5, 0.1)

            #E (MI)
            buzzer(BuzzerObj, 659, 0.5, 0.1)

            #F (FA)
            buzzer(BuzzerObj, 698, 0.5, 0.1)

            #G (SOL)
            buzzer(BuzzerObj, 784, 0.5, 0.1)

            #A (LA)
            buzzer(BuzzerObj, 880, 0.5, 0.1)

            #B (SI)
            buzzer(BuzzerObj, 987, 0.5, 0.1)

            BuzzerObj.deinit()

        if buzzer_music > -1:
            pause = 0.05
            # pauses between notes
            t = 0.125
            # time that music note plays

            music_notes = [B4F, B4F, A4F, A4F,
                           F5, F5, E5F, B4F, B4F, A4F, A4F, E5F, E5F, C5S, C5, B4F,
                           C5S, C5S, C5S, C5S,
                           C5S, E5F, C5, B4F, A4F, A4F, A4F, E5F, C5S,
                           B4F, B4F, A4F, A4F,
                           F5, F5, E5F, B4F, B4F, A4F, A4F, A5F, C5, C5S, C5, B4F,
                           C5S, C5S, C5S, C5S,
                           C5S, E5F, C5, B4F, A4F, A4F, A4F, E5F, C5S, C5S]

            rhythm = [1, 1, 1, 1,
                      3, 3, 6, 1, 1, 1, 1, 3, 3, 3, 1, 2,
                      1, 1, 1, 1,
                      3, 3, 3, 1, 2, 2, 2, 4, 8,
                      1, 1, 1, 1,
                      3, 3, 6, 1, 1, 1, 1, 3, 3, 3, 1, 2,
                      1, 1, 1, 1,
                      3, 3, 3, 1, 2, 2, 2, 4, 8, 4]

            for i in range(len(music_notes)):
                buzzer(BuzzerObj, music_notes[i], rhythm[i]*t, pause)

        response = get_html('index.html')
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        cl.close()
        print('Connection closed')

As you can see, there are two arrays, music_notes and rhythm. You would then run a for loop to put these values into the buzzer() function. Also, at the top of the code, we import all variables from constants.py.

The buzzer() function also uses two new variables — t and pause. These two variables help you tune tempo of the music. t defines how long each note should be played and pause defines how long of a silence there should be between notes.


5. Pico W and HC-SR04 ultrasonic sensor

pico-w-ultrasonic-sensor
`

For the previous tutorials, we’ve been using the Pico W to send commands to the LEDs and buzzers.

But what if we’re using the Pico W to receive information?

In this case, we’re talking about the HC-SR04 ultrasonic distance sensor.

When you plug in your ultrasonic sensor, be sure to know if it’s a 5V or 3.3V component. I plugged my 5V version into the 3.3V pin and got no response from the program, which I will share below. Once I moved the power source to the 5V pin, I immediately got a response.

Apparently, some newer versions of the HC-SR04 can take both 3.3V to 5V. Perhaps it’s best to just try the 3.3V pin first and see if you get a result. If not, try the 5V.

Here’s the schematics and the program you can run in Thonny. If you get a response, that means you have plugged everything in the right way, including the voltage.

Wiring

The wiring from the labelled pins of the HC-SR04 sensors are as follows:

  • VCC to 3.3V or 5V pin (if in doubt, try 3.3V first)
  • TRIG to GPIO 16
  • ECHO to GPIO 15
  • GND to GND

Test program for HC-SR04

In order to test if your wiring is correct, try this code that will print out the distance on the Thonny Shell.

from machine import Pin
import time
trigger = Pin(16, Pin.OUT)
echo = Pin(15, Pin.IN)
def ultrasonic():
   trigger.low()
   time.sleep_us(1)
   trigger.high()
   time.sleep_us(10)
   trigger.low()
   while echo.value() == 0:
       signaloff = time.ticks_us()
   while echo.value() == 1:
       signalon = time.ticks_us()
   timepassed = signalon - signaloff
   distance = (timepassed * 0.0340) / 2
   print("Distance = ",distance,"cm")
while True:
   ultrasonic()
   time.sleep(1)

Your output should be:

Distance =  9.707001 cm
Distance =  9.707001 cm
Distance =  8.619 cm
Distance =  8.415001 cm
Distance =  8.551001 cm
Distance =  8.551001 cm
Distance =  8.619 cm

If the program runs and quits without any output, you probably wired something wrongly. When I plugged the sensor to a 3.3V pin instead of a 5V, the program quit with no response.

Getting data sent to your browser

Let’s whip up the files that we used to serve a web page and modify it so that we can see the distance values.

Here’s the final code:

main.py

from wifi import init_wifi
import socket
from machine import Pin
import time

init_wifi()

# ultrasonic sensor pins and functions
trigger = Pin(16, Pin.OUT)
echo = Pin(15, Pin.IN)


def ultrasonic():
    trigger.low()
    time.sleep_us(1)
    trigger.high()
    time.sleep_us(10)
    trigger.low()
    while echo.value() == 0:
        signaloff = time.ticks_us()
    while echo.value() == 1:
        signalon = time.ticks_us()
    timepassed = signalon - signaloff
    distance = (timepassed * 0.0340) / 2
    return distance

# Function to load in html page

def get_html(html_name, distance):
    # open html_name (index.html), 'r' = read-only as variable 'file'
    with open(html_name, 'r') as file:
        html = file.read()
    content = html.replace(
        "<h2 id=\"ultrasonic\"></h2>", f"<h2 id=\"ultrasonic\">{distance}cm</h2>")

    return content


# HTTP server with socket
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]

s = socket.socket()
s.bind(addr)
s.listen(1)

print('Listening on', addr)

# Listen for connections
while True:
    try:
        cl, addr = s.accept()
        print('Client connected from', addr)
        request = cl.recv(1024)
        print(ultrasonic())
        response = get_html('index.html', ultrasonic())
        cl.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
        cl.send(response)
        cl.close()

    except OSError as e:
        cl.close()
        print('Connection closed')

index.html

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
      <body>
        <h1>Pico W Ultrasonic Distance</h1>
        <h2 id="ultrasonic"></h2>
        <script>
            setInterval(() => location.reload(), 500)
        </script>
</html>

Let’s look at some snippets of the code to understand what’s happening.

Code to get ultrasonic distance

def ultrasonic():
    trigger.low()
    time.sleep_us(1)
    trigger.high()
    time.sleep_us(10)
    trigger.low()
    while echo.value() == 0:
        signaloff = time.ticks_us()
    while echo.value() == 1:
        signalon = time.ticks_us()
    timepassed = signalon - signaloff
    distance = (timepassed * 0.0340) / 2
    return distance

The code block above triggers the emission of an ultrasonic wave. The time.sleep between the lows and highs are necessary for the functioning of the ultrasonic sensor.

To measure how much time it takes between the emission of the ultrasonic wave and the time for the sound wave to return, we use time.ticks_us to measure the time of emission and the time for the echo to be detected.

time.ticks_us is an arbitrary number, so we will have to subtract signalon from signaloff to get the time passed.

To get the distance, we use the formula distance = (timepassed * speedofsound) / 2. Speed of sound is 340m/s, which therefore is 0.0340.

The reason why we have to divide it by two is because the sound wave travels to the object and returns back.

Code to serve web page with distance value

def get_html(html_name, distance):
    # open html_name (index.html), 'r' = read-only as variable 'file'
    with open(html_name, 'r') as file:
        html = file.read()
    content = html.replace(
        "<h2 id=\"ultrasonic\"></h2>", f"<h2 id=\"ultrasonic\">{distance}cm</h2>")

    return content

...

response = get_html('index.html', ultrasonic())

This function has been modified a little from the previous tutorials, taking in an additional parameter distance, which runs ultrasonic().

The function opens the index.html file normally but uses the replace method to find the <h2> and inserts the distance variable.

index.html pings server every 500ms to get new value

index.html

The main web page receives a function that reloads the web page every 500ms. Every reload pings the Pico W to get the new ultrasonic distance value.

    <script>
            setInterval(() => location.reload(), 500)
        </script>

6. Ultrasonic data, but let’s use AJAX to reduce payload

The previous tutorial works by having the Raspberry Pi Pico W send a whole new index.html file every time there’s a connection.

This works but it’s incredibly inefficient because the whole index.html is being resent when you only need to update the ultrasonic distance data.

We want the same results as the previous tutorial, but without the heavy payload of the previous method.

So, we need to make the client (your phone or PC) ping an endpoint that will respond only with the ultrasonic data.

The new methodology is this: if people visit the root URL, say 192.168.1.119, they will be served the index.html.

index.html will have JavaScript that pings the /data endpoint, which triggers the Pico W to get ultrasonic distance data, and respond with it.

Then, index.html will receive this data and update the web page.

Splicing the request from client

Remember that in the main.py, there’s always a line saying requests = cl.recv(1024). The request object looks a bit like this:

b'GET /data HTTP/1.1
Host: 192.168.1.119
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://192.168.1.119/data
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,es;q=0.8,zh-TW;q=0.7,zh-CN;q=0.6,zh;q=0.5

First up, we need to filter this wall of text out.

We use the find() method to find whether the request variable has “/data”. If it does, respond with the ultrasonic distance data. If there’s no “/data”, then respond with index.html.

Before

request = cl.recv(1024)
        print(ultrasonic())
        response = get_html('index.html', ultrasonic())

After

 request = cl.recv(1024)
        request = str(request)
        print (request.find('/data'))
        if (request.find('/data') > -1):
            #if string is found, respond with data.
            response = str(ultrasonic())
        else:
            response = get_html('index.html', ultrasonic())

If you visited /data in your browser, you will see the distance value printed, as below.

Previously, the backend handled almost everything. The ultrasonic distance was modified before it was served as index.html.

Now, index.html is fetching the sensor data from Pico W, so we’d have to make some changes to the JavaScript that’s inside the HTML file. Changes are highlighted in bold.

<!DOCTYPE html>
<html>
    <head>
        <title>Pico W</title>
    </head>
      <body>
        <h1>Pico W Ultrasonic Distance</h1>
        <h2 id="ultrasonic"></h2>
        <script>
            //instead of hardcoding, use address which browser accessed Pico W.
            const PicoAddress = window.location.href

            const getData = async () => {
                //there is a slash at the end of PicoAddress, so don't use "/data". use "data"
                const data = await fetch(`${PicoAddress}data`);
                //response is just a string, so use data.text() to decode the Response's body
                const distance = await data.text();
                //target ultrasonic id and change its inner HTML
                ultrasonic.innerHTML = `${distance} cm`;
                //run getData() again.
                setInterval(getData,100)
                //you can run getData() but it seems more optimized if you give it some time to breathe
            };

            getData();
           
        </script>
</html>

Central to the code here is the Fetch API and the latest async/await syntax here.

The function getData() pings the Pico W’s /data endpoint, and the Pico W responds. We ensure the program doesn’t skip ahead before we get a full response asking it to await.

Fetch becomes a Response object which you’ll need to use the Response.text() method to get to the body of the response. Hence the lines const distance = await data.text();

Once we have the ultrasonic data, we change the <h2 id=”ultrasonic”> element’s inner HTML, which starts empty, with the distance data. You can target an element with an id directly without use the getElementById() or querySelector() methods.

Once all the code is done, we want to run getData() again. Why did I use a setInterval to set a 100ms interval?

What I found is that if you called getData() without an interval, you will get more pauses between outputs. You’ll get a sequence of fast updates, then a pause. With the 100ms breathing room, the Pico W runs a bit better and updates seem more or less real-time still.

If you want to understand what’s happening under the hood, refer to the Fetch API documentation and the async/await documentation.


7. Pimoroni Phew for cleaner code

Pimoroni Phew being used to deliver ultrasonic data

In the last section, we covered the AJAX method of updating the ultrasonic sensor data.

But is there an even better way to do this?

Pimoroni’s Phew is a library that helps you do a lot of networking easily because you can avoid a lot of drugery.

The goals are the same:

  • Get ultrasonic data and display it on index.html
  • Keep payload as low as possible.

Here’s how main.py looks like.

from wifi import init_wifi
from phew import server
from phew.template import render_template
from machine import Pin
import time

init_wifi()

# ultrasonic sensor pins and functions
trigger = Pin(16, Pin.OUT)
echo = Pin(15, Pin.IN)


# Function to load in html page

@server.route("/data", methods=["GET"])
def ultrasonic(request):
    trigger.low()
    time.sleep_us(1)
    trigger.high()
    time.sleep_us(10)
    trigger.low()
    while echo.value() == 0:
        signaloff = time.ticks_us()
    while echo.value() == 1:
        signalon = time.ticks_us()
    timepassed = signalon - signaloff
    distance = (timepassed * 0.0340) / 2
    return str(distance), 200


@server.catchall()
def catchall(request):
    return render_template("index.html")


server.run()

As you can see, the code is a lot more readable as you can set endpoints and link them up with functions that return specific data.

Instead of fumbling with code to run the sockets, you’d just call a function from Phew’s library.

Some things that are like about Phew are:

  • No more using of request.find() to create an endpoint
  • No more get_html() function to open the index.html file, just use Phew’s render_template()
  • main.py is a lot easier to read, with minimal additional effort!
  • Phew also adds in logging functionality that will allow you to debug your code more easily.

The only downside is that Phew is still “a very new project and should be considered, at best, alpha stage.” So, you might find unexpected roadblocks sometimes.

Check it out.

The previous tutorials talked about how to interact with your Pico W with your PC/mobile phone.

Let’s take a step out of this and get the Pico W to interact with a cloud service.

For this, let’s make a temperature-and-humidity logger with a DHT22 sensor that sends data to Google Sheets via IFTTT.


8. Setting up the DHT22 temperature and humidity sensor to the Pico W

Raspberry Pi Pico W paired with a DHT22 temperature and humidity logger.

All these steps work for the DHT11 and DHT22. The main difference between the two is the accuracy of the readings. DHT11 is blue in colour while DHT22 is white.

If you have a DHT11, note that references to the DHT22 in the code should be changed to DHT11. For example,

import dht
#dht22 = dht.DHT22(Pin(16))
#should be
dht11 = dht.DHT11(Pin(16))
DHT22 pins

Looking at the diagram below, from top to bottom of the DHT22, you’d connect 3.3V to the first pin.

The second pin is the data pin. You’d need to power it. So, connect a 10K ohm resistor from the power rail to it, and then connect another jumper wire to GPIO 16 on the Raspberry Pi Pico.

GND goes to GND.

To test the sensor, run this:

from machine import Pin
import dht

# ultrasonic sensor pins and functions
dht22 = dht.DHT22(Pin(16))

print(dht22.measure())
print(dht22.temperature())
print(dht22.humidity())

If you get OSError: [Errno 110] ETIMEDOUT means you are running the script too soon. The DHT22 requires a two-second break before it can return another value. DHT11 requires one second.

Connecting IFTTT and Google Sheets

Pico W logging time, temperature and humidity data into Google Sheets

The easiest way to get data from the Pico W to Google Sheets is via IFTTT.

First, sign up for IFTTT account.

Then, click the “Create” button to create the applet.

You will see this screen:

Click on “If This” and search for Webhooks. Click on “Receive a web request”. Do not pick the JSON one. Name the event name “dht22”. The event name is critical because this is how IFTTT knows which applet to trigger.

Pick this, not the JSON payload option.

What you’re doing here is creating an endpoint which you can ping with the DHT22’s sensor data.

Then, click “Then That”. Pick “Google Sheets”. Then pick “Add row to spreadsheet”. You’d have to link your Google Sheets account.

Change the “Spreadsheet name” and “Drive folder path” to whatever you want. The only thing critical here is the “Formatted row”, where you’ll want it to be

{{OccurredAt}} ||| {{Value1}} ||| {{Value2}}

Now, you will need to go get the endpoint where the Pico W can send sensor data to.

Head to https://ifttt.com/maker_webhooks and click on “Documentation”.

The documentation will tell you your key, the endpoint you need to send data to and how to structure the JSON body.

Since you used “dht22” as your event name, your endpoint is:

ttps://maker.ifttt.com/trigger/dht22/with/key/[your_key_here]

You’ll also want to structure your JSON body like this:

{'value1': *** temperature data *** , value2': *** humidity data *** }

Coding the Pico W to get DHT22 data and sending it to IFTTT

Here’s how.

from machine import Pin
from wifi import init_wifi
import urequests
import ujson
import dht
import time

init_wifi()

dht22 = dht.DHT22(Pin(16))


def send_data():
    #tell dht22 to take a reading
    dht22.measure()
    #find temp & humidity data and then jsonify data.
    dht_data = {'value1': str(dht22.temperature()),
                'value2': str(dht22.humidity())}
    post_data = ujson.dumps(dht_data)
    request_url = 'https://maker.ifttt.com/trigger/dht22/with/key/[your_key_here]'
    res = urequests.post(request_url, headers={
                         'content-type': 'application/json'}, data=post_data)
    #log response from IFTTT.
    print(res.text)
    #sleep for a minute before running send_data again
    time.sleep(1*60)
    send_data()


send_data()

If you are going to copy this code, make sure to replace [your_key_here] inn the request_url variable.

You should get a new reading every minute that gets logged to Google Sheet, as below.

Pico W logging time, temperature and humidity data into Google Sheets

Note that the temperature logged is in Celcius. If you want it in Fahrenheit, here’s the formula:

fahrenheit = dht22.temperature() * 1.8000 + 32.00

If you want data to be logged more (or less) frequently, change the value in time.sleep().

Use IFTTT to notify you if error occurs

Of course, this code is still quite fragile.

Let’s say something doesn’t go right. For example, your cat pulled the 3.3V jumper wire while you placed on the floor to log the temperature.

The program will die, but you won’t get any notification until you notice that your data isn’t being logged.

How do you resolve this?

Well, get IFTTT to send you a notification when that happens!

You would need the IFTTT app on your phone. So download it.

Then, you’d create a new applet.

For the “If This” part, Choose Webhooks -> Receive a web request. Name your event “error“.

For the “Then This” part, choose “Notifications” and “Send a notification from the IFTTT app”.

I made the message simple

Error: {{Value1}}

Now, let’s build the try-error blocks.

from machine import Pin
from wifi import init_wifi
import urequests
import ujson
import dht
import time

init_wifi()

dht22 = dht.DHT22(Pin(16))


def send_data():
    # tell dht22 to take a reading
    dht22.measure()
    # find temp & humidity data and then jsonify data.
    dht_data = {'value1': str(dht22.temperature()),
                'value2': str(dht22.humidity())}
    post_data = ujson.dumps(dht_data)
    request_url = 'https://maker.ifttt.com/trigger/dht22/with/key/[your-key-here]'
    res = urequests.post(request_url, headers={
                         'content-type': 'application/json'}, data=post_data)
    # log response from IFTTT.
    print(res.text)
    # sleep for a minute before running send_data again
    time.sleep(1*60)
    send_data()


try:
    send_data()
except Exception as e:
    print(e)
    request_url = 'https://maker.ifttt.com/trigger/error/with/key/[your-key-here]'
    post_data = ujson.dumps({"value1": str(e)})
    urequests.post(request_url, headers={
        'content-type': 'application/json'}, data=post_data)

Try it. Unplug the 3.3V wire from your DHT22 and you will get a notification as such:


9. Build a physical Spotify remote control with Raspberry Pi Pico W

Building on the previous project, we’ll use IFTTT to control Spotify.

We’ll create a device that has a play, pause and skip track button.

The chief benefit of using IFTTT to control your Spotify is how easy it is.

The downside is that you need a paid Spotify account and responses are slow. Meaning that if you pressed the skip button, you’d have to wait a while before you see the result.

The beauty of having a dedicated Spotify remote is that you can change your music without carrying your phone around or having to open up the Spotify app.

If you have ever driven a modern car, you’ll know how nice having steering wheel controls are.

Wiring up a remote Spotify controller

Spotify Raspberry Pi Pico W controller
Spotify Raspberry Pi Pico W controller. From top to bottom, the buttons are pause, play, skip track.

You’ll need…

  • 7x jumper wires
  • 3x buttons
  • Raspberry Pi Pico W
  • Spotify paid account

There’s a lot of wires crisscrossing up there in the image, so here’s a textual explanation.

The push buttons are like switches. You need to connect the 3V3 pin to each of them, which means using the positive column on the breadboard. Therefore:

3V3 -> Breadboard positive column -> Push button (on one side of the breadboard gutter) -> GPIO (on the other side)

My code will use GPIO 16 for the play button, GPIO 2 for the pause button and GPIO 15 for the skip track button.

Setting up IFTTT to control Spotify

We’ll create three IFTTT applets to control Spotify.

We have to create three applets, one each for the play, pause and skip function.

If you have the premium version of IFTTT, you probably could use the JavaScript filters to contain it all into one app. But since we don’t, we need an applet each.

Once logged in, click on “Create” on the top right of the main menu.

On the “If This” bar, click on it and search for Webhooks.

Click on the “If This” or “Then That” and choose Webhooks and Spotify respectively.

Then, click on “Receive a web request” (not the other option that has “with a JSON payload”).

For event name, type spotify_skip.

You will have to repeat this step two times more for spotify_pause and spotify_play once you’re done creating this applet.

Once that’s done, go to the “Then That” bar and click on it. Search for “Spotify“.

You will have to authorize IFTTT to connect to Spotify once.

If you are doing the spotify_skip action, you will want to click on “Skip track”. But if you are doing an applet to do some other action, the image above will show you which one to use.

Once you have created all three applets, it’s time to code!

Coding the Spotify remote for the Pico W

First up, you will need to know the endpoint you need to hit.

Go to this page and click on the Documentation.

You will see your key there. If you have followed all the steps above, the difference between your endpoint and mine is your key. Hence,

Play endpoint: https://maker.ifttt.com/trigger/spotify_play/with/key/[your_key_here]

Pause: https://maker.ifttt.com/trigger/spotify_pause/with/key/[your_key_here]

Skip: https://maker.ifttt.com/trigger/spotify_skip/with/key/[your_key_here]

Code

from machine import Pin
from wifi import init_wifi
import urequests
import time

init_wifi()

play_btn = Pin(16, Pin.IN, Pin.PULL_DOWN)
pause_btn = Pin(2, Pin.IN, Pin.PULL_DOWN)
skip_btn = Pin(15, Pin.IN, Pin.PULL_DOWN)


def play():
    request_url = 'https://maker.ifttt.com/trigger/spotify_play/with/key/[your_key_here]'
    res = urequests.post(request_url)
    # print response from IFTTT.
    print(res.text)


def pause():
    request_url = 'https://maker.ifttt.com/trigger/spotify_pause/with/key/[your_key_here]'
    res = urequests.post(request_url)
    # print response from IFTTT.
    print(res.text)


def skip():
    request_url = 'https://maker.ifttt.com/trigger/spotify_skip/with/key/[your_key_here]'
    res = urequests.post(request_url)
    # print response from IFTTT.
    print(res.text)


try:
    while True:
        if play_btn():
            print('play btn')
            play()
            time.sleep(0.25)
        if pause_btn():
            print('pause btn')
            pause()
            time.sleep(0.25)
        if skip_btn():
            skip()
            print('skip')
            time.sleep(0.25)

except Exception as e:
    print(e)
    request_url = 'https://maker.ifttt.com/trigger/error/with/key/[your_key_here]'
    post_data = ujson.dumps({"value1": str(e)})
    urequests.post(request_url, headers={
        'content-type': 'application/json'}, data=post_data)

Let’s go through the code.

Note that you will need to replace [your_key_here] with your real key, as obtained through the Documentation link.

First, we declare variables for the push buttons.

play_btn = Pin(16, Pin.IN, Pin.PULL_DOWN)
pause_btn = Pin(2, Pin.IN, Pin.PULL_DOWN)
skip_btn = Pin(15, Pin.IN, Pin.PULL_DOWN)

Currently, if you do not press the button, your variable will have a value of 0. If you press it, it becomes 1. This is what we’ll use to trigger the play() , pause() and skip() functions.

Then, we create functions for the play, pause and skip endpoints. The general template is as such:

def play():
    request_url = 'https://maker.ifttt.com/trigger/spotify_play/with/key/[your_key_here]'
    res = urequests.post(request_url)
    # print response from IFTTT.
    print(res.text)

It’s pretty simple. If this function is run, it will send a POST request to IFTTT. Sending a GET request will not work.

Then, we have the try/except block.

try:
    while True:
        if play_btn():
            print('play btn')
            play()
            time.sleep(0.25)
        if pause_btn():
            print('pause btn')
            pause()
            time.sleep(0.25)
        if skip_btn():
            skip()
            print('skip')
            time.sleep(0.25)

except Exception as e:
    print(e)
    request_url = 'https://maker.ifttt.com/trigger/error/with/key/[your_key_here]'
    post_data = ujson.dumps({"value1": str(e)})
    urequests.post(request_url, headers={
        'content-type': 'application/json'}, data=post_data)

If a button is pressed, the code will run the relevant function. For example, pressing the skip button will run the function skip().

A time.sleep(0.25) will pause the function for 250ms. Without this, even a short press can overload and crash the Pico W.

The except block is optional, but I did it because I already had an “error” applet on IFTTT. If you have followed the previous tutorial, you might have used it.

Basically, it sends the error message, e, to IFTTT, so that you get the error message as a phone app notification.

Why is it not working?

Using IFTTT as a medium to control your Spotify is easy, but it does come with some downsides.

You must start the music the regular way first

If you tried pressing the play button on your Pico W and expected music to start playing… well, nothing happens.

The solution is to start the music on your computer or phone normally. You need to go to your app and press play.

I think this declares which device is the active device. Once you have done this, you will be able to use your Pico W Spotify remote.

Slow responses

It takes a few seconds between button press and response. Unfortunately, this is the way it is.

You could pay for an IFTTT upgrade to get faster response speeds. At least, that’s what they promise for your money.

Is there a direct way to connect Spotify?

Yes! Spotify has an API which you can connect to.

It gives you significantly more control. You could add a rotary encoder to control volume. You could add add a LCD screen to show what’s playing. Check out Spotify’s console here.

Amazing, but also much more difficult to program, especially on a Pico W.

IFTTT makes it all easy because they do all the heavy lifting. If you want to be the heavy lifter, check out the authentication flow.

Authorization Code Flow diagram by Spotify

Of course, we’re Raspberry Pi enthusiasts. Someone out there will do it. Should it be you? Or me? Comment below.

Control your Pico W wirelessly with PiCockpit!

You can control and get data from your Pico W wirelessly by using PiCockpit.

PiCockpit allows you to get values, control and use PWM through a GUI via its GPIO applet.

You can also see your Pico W’s stats through the PiStats applet.

Integrating PiCockpit into your Pico W is super easy.

Follow this tutorial.

Write even less code with PiCockpit and the Pico W

PiCockpit makes it easy for you to control your GPIO pins without having to write any code.

If you look at tutorial number 2, notice how much code is necessary just to toggle an LED.

With our new Pico W integration, PiCockpit makes it so much easier as you don’t have to program anything at all. Not even the WiFi configuration — that’s done with our setup wizard.

10. Simple LED control with PiCockpit and Pico W

If you have your LED configured exactly the way I’ve done it in tutorial no. 2, then all that’s left is to set it up on PiCockpit.

If you’re coding it out, you’ll declare which pin your LED is on using led = machine.Pin(2, machine.Pin.OUT)

On PiCockpit, you’ll go to your GPIO applet, and the scroll to “GPIO Output (On/Off)”.

Choose BCM02 from the dropdown menu because your LED is on GPIO 2.

Then, on the “Control” column, toggle the switch to turn the LED on.

You can also easily use the Software PWM section below to control the brightness of your LED.

Note that you will need to remove the previous setting because you cannot have two outputs on the same GPIO.

As you toggle the “Control” slider, you’ll notice the brightness of the LED changing.

picockpit gpio pwm remote control

11. Pico W, 5V fan and a transistor, controlled by PiCockpit

Let’s try something a little bit more comprehensive but using the same GPIO Output toggle.

To illustrate some real-world use cases, I will power a 5V fan using PiCockpit.

This is a low-power 5V fan taken from my Raspberry Pi 4, so it’s well within the output capabilities of the Raspberry Pi Pico W.

That said, because it’s a 5V fan, I can’t use a GPIO pin. In less power-intensive components, like an LED, you can have the GPIO do double duty of supplying power to the component and being the “switch” that turns it on and off.

But the 5V fan would require too high a voltage. So, the next best way is to put a transistor in the middle.

This allows me to supply 5V to the fan, while ensuring that I can turn it on and off.

Once again, because of PiCockpit, I did zero programming. I only did the hardware, which is wired as follows:

The fan is a 5V/0.12A fan, connected to 5V on the positive end (red wire), and the negative wire goes to the emitter leg of the transistor.

The transistor is a PN2222 (NPN) transistor, which means that it switches ON when it receives a high signal.

From left to right, with the semi-circled part facing away from you, the legs are the Emitter, Base and Collector.

The Base leg is connected to a 1K resistor, then connected to GPIO 15.

The Collector leg is connected to ground.

Configuring PiCockpit to work with transistor

Once again, super easy.

Go to the dropdown menu in the GPIO Output section and add BCM15.

Once it’s in, you can click the downward arrow and change the State Names to “fan off” and “fan on”.

Toggle the control switch and you should see the fan turning on.

You can also use PiStats to see the drop in temperatures on your board.

Display photoresistor using MQTT and Node-RED with Pico W.

The main goal of this tutorial is to introduce MQTT.

In the previous tutorials, I’ve shown you how you can use your Pico W to deliver data, but what if you want a central repository of data in the cloud?

HiveMQ Cloud is a free service that we can use to achieve this goal. By using someone else’s computers, we can also lighten the load on the Pico W.

To add to that, MQTT has major advantages compared to the previous methods used. For one, it’s a lot more efficient in sending small data. MQTT protocol headers are 2 bytes in size. HTTP is about 4000 times larger.

Reducing local processing load and network load means longer battery life for your Pico W, which is perfect for battery or solar-powered projects.

Photoresistor connection with the Pico W

A photoresistor (photocell) is super easy to connect.

Place the photoresistor across the central gutter of the breadboard.

Then, connect the 3V3 pin to one side of the photoresistor.

You’ll want to connect an ADC pin to the other side of the photoresistor, so connect GPIO 26.

Finally, connect a 10K ohm resistor from ground to the photocell.

HiveMQ Cloud and coding the Pico W

First up, sign up for HiveMQ Cloud here.

Go through the setup and create a cluster. It will ask you to choose AWS or Azure. For our purposes, there’s no difference.

Then, click “Manage Cluster”.

Cluster main menu. Take note of the two orange rectangles.

Take note of your Cluster URL, and click on Access Management to create a new user. Go through the steps and create a new user.

With these details, you can now program your Pico W to send data there.

Coding the Pico W to receive photocell data and MQTTClient

from machine import Pin, ADC
from wifi import init_wifi
import time
from umqtt.simple import MQTTClient

init_wifi()

photoresistor = ADC(Pin(26))


def readLight():
    light = photoresistor.read_u16()
    return light


# Connect MQTT

def connectMQTT():
    client = MQTTClient(client_id=b"[your_client_id]",
                        server=b"[your-host-name]",
                        port=0,
                        user=b"[your-user]",
                        password=b"[your-pw]",
                        keepalive=7200,
                        ssl=True,
                        ssl_params={
                            'server_hostname': '[your-host-name]'}
                        )

    client.connect()
    return client


client = connectMQTT()


def publish(topic, value):
    print(topic)
    print(value)
    client.publish(topic, value)
    print("data published")


while True:
    brightness = str(readLight()) #to publish, must send string

    print(brightness)

    publish('picow/brightness', brightness)

    time.sleep(0.1)

First, let’s get our imports in order.

from machine import Pin, ADC
from wifi import init_wifi
import time
from umqtt.simple import MQTTClient

The wifi import comes from the previous tutorials.

You will need the umqtt.simple library, which can be downloaded here.

Once that’s downloaded, you can upload it to your board (guide here).

You should have these files on your Pico W.

Then, create a function to get a reading from the photoresistor:

photoresistor = ADC(Pin(26))

def readLight():
    light = photoresistor.read_u16()
    return light

This returns a value up to 65535. The brighter it is, the higher the value.

Connecting to HiveMQ

In order to connect to HiveMQ, you will need to send some parameters to the MQTTClient class.

# Connect MQTT

def connectMQTT():
    client = MQTTClient(client_id=b"[your_client_id]",
                        server=b"[your-host-name]",
                        port=0,
                        user=b"[your-user]",
                        password=b"[your-pw]",
                        keepalive=7200,
                        ssl=True,
                        ssl_params={
                            'server_hostname': '[your-host-name]'}
                        )

    client.connect()
    return client


client = connectMQTT()

Replace [your-host-name] with the address found on your dashboard. You will have to do this twice, once for server and another for server_hostname. Also replace [your_client_id] with a name for your device, such as “your_picow”.

Then, replace [your-user] and [your-pw] with the user you created in the Access Management page (screenshot of Access Management page below).

For reference, this function sends data to the HiveMQ:

def publish(topic, value):
    print(topic)
    print(value)
    client.publish(topic, value)
    print("data published")

Let’s call it in our while loop:

while True:
    brightness = str(readLight()) #to publish, must send string

    print(brightness)

    publish('picow/brightness', brightness)

    time.sleep(0.1)

When publishing, you must send strings, which is why brightness = str(readLight()) is there.

If you send integers or floats, the program will die.

In the publish function, give your topic a name. Say, picow/brightness, then add the value you want to send. In this case, we want to send the stringified light reading, brightness.

You should be able to see the data being published when you log into the Web Client tab.

HiveMQ’s web client, on the left, showing you the data that has been published.

Node-RED

Those are just numbers online that might seem like gibberish. What if you wanted to access the data on HiveMQ Cloud and present it graphically?

Rather than rolling your own, you could just use Node-RED.

Node-RED makes it really easy for you to take data from HiveMQ and then present it using graphical representations.

We’re going to make a gauge using Node-RED.

To start, you will need to have nodejs. Check HiveMQ’s documentation to see which version is recommended.

Once you have Node installed, you will need to open up a command prompt/Terminal and run this commands (exclude sudo if you’re on Windows):

sudo npm install -g --unsafe-perm node-red

This will use node package manager (npm) to install Node-RED globally.

Then, run Node-RED by typing node-red into the Terminal/command prompt.

Open your browser and head to http://127.0.0.1:1880 or whatever address is listed in your Terminal.

Let’s build the flow. Drag an “mqtt in” onto the canvas. You can find it under the “network” tab on the left sidebar.

We’ll need to configure the tab, so double click on the rectangle and do the following:

In the “topic” field, make sure to add picow/brightness, since that’s what you published from the Pico W.

In “server”, add a new one by clicking the pencil icon and you will be brought to the next menu.

Put a new server address in and change the port to 8883. Tick “Use TLS” but don’t bother adding new tls-config.

Then, go to the security tab and add in your login credentials.

All of these details can be found in your code when you initialize MQTTClient.

Adding a gauge

To add a gauge, you must have the node-red-dashboard.

On the left sidebar, if you do not see these:

Then go to the menu (top right button) -> Manage Palette. Then go to the Install tab and search for node-red-dashboard. Click on “Install”.

Drag and drop a “gauge” to the right of the mqtt in rectangle, and connect them by dragging a line from mqtt in to the gauge.

Double click the gauge rectangle and change the label to “brightness” and the “range” max to 65535.

Great. Now let’s press “Deploy”.

If your settings were correct, you will see a green circle and “connected” under the rectangle. If not, your terminal will give you more details as to why there’s an error.

With your Pico W delivering data to HiveMQ Cloud, it’s now time to check out the dashboard. Visit http://127.0.0.1:1880/ui and you should see the gauge being updated frequently.


Your suggestions are welcome. Leave a comment in the comment box below, email me or Tweet me.

2 Comments

  1. Quang on January 10, 2023 at 2:21 pm

    Hello,
    Thank you for sharing your knowledge with us.
    Could you advise if the WiFi exercises are for in-network only? I was not able to access the Pico W if I’m on a different WiFi network. If so, do you have any examples for out of network access?
    Thank you.

    • raspi berry on February 4, 2023 at 11:52 am

      Yes, due to the nature of networks, routers and firewalls these exercises work only within the same WiFi network.
      PiCockpit itself is a solution which spans networks – you can access your Pico W from everywhere in the Internet.
      We are working to bring more features to the platform.

      If you want to recreate something like that yourself, you would need to have some kind of tunneling solution to your network, or a relay server, or something like that.
      We offer consulting services if you want to go deep into this topic.

Leave a Comment