Once you've finished setting up your Pico W with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.

To do this, click on the Download Project Bundle button in the window below. It will download as a zipped folder.

# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT

import os
import time
import ipaddress
import wifi
import socketpool
import busio
import board
import microcontroller
import displayio
import terminalio
from adafruit_display_text import label
import adafruit_displayio_ssd1306
import adafruit_imageload
from digitalio import DigitalInOut, Direction
from adafruit_httpserver import Server, Request, Response, POST
from adafruit_onewire.bus import OneWireBus
from adafruit_ds18x20 import DS18X20

#  onboard LED setup
led = DigitalInOut(board.LED)
led.direction = Direction.OUTPUT
led.value = False

#  pin used for party parrot animation
parrot_pin = DigitalInOut(board.GP10)
parrot_pin.direction = Direction.OUTPUT
parrot_pin.value = False

# one-wire bus for DS18B20
ow_bus = OneWireBus(board.GP6)

# scan for temp sensor
ds18 = DS18X20(ow_bus, ow_bus.scan()[0])

#  function to convert celcius to fahrenheit
def c_to_f(temp):
    temp_f = (temp * 9/5) + 32
    return temp_f

#  i2c display setup
displayio.release_displays()
oled_reset = board.GP9

# STEMMA I2C on picowbell
i2c = busio.I2C(board.GP5, board.GP4)
display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset)

WIDTH = 128
HEIGHT = 64
offset_y = 5

display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT)

# default display group
splash = displayio.Group()
display.root_group = splash

#  connect to network
print()
print("Connecting to WiFi")
connect_text = "Connecting..."
connect_text_area = label.Label(
    terminalio.FONT, text=connect_text, color=0xFFFFFF, x=0, y=offset_y
)
splash.append(connect_text_area)

#  set static IP address
ipv4 =  ipaddress.IPv4Address("192.168.1.42")
netmask =  ipaddress.IPv4Address("255.255.255.0")
gateway =  ipaddress.IPv4Address("192.168.1.1")
wifi.radio.set_ipv4_address(ipv4=ipv4,netmask=netmask,gateway=gateway)
#  connect to your SSID
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))

print("Connected to WiFi")
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

#  variables for HTML
#  comment/uncomment desired temp unit

#  temp_test = str(ds18.temperature)
#  unit = "C"
temp_test = c_to_f(ds18.temperature)
unit = "F"
#  font for HTML
font_family = "monospace"

#  the HTML script
#  setup as an f string
#  this way, can insert string variables from code.py directly
#  of note, use {{ and }} if something from html *actually* needs to be in brackets
#  i.e. CSS style formatting
def webpage():
    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-type" content="text/html;charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
    html{{font-family: {font_family}; background-color: lightgrey;
    display:inline-block; margin: 0px auto; text-align: center;}}
      h1{{color: deeppink; width: 200; word-wrap: break-word; padding: 2vh; font-size: 35px;}}
      p{{font-size: 1.5rem; width: 200; word-wrap: break-word;}}
      .button{{font-family: {font_family};display: inline-block;
      background-color: black; border: none;
      border-radius: 4px; color: white; padding: 16px 40px;
      text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}}
      p.dotted {{margin: auto;
      width: 75%; font-size: 25px; text-align: center;}}
    </style>
    </head>
    <body>
    <title>Pico W HTTP Server</title>
    <h1>Pico W HTTP Server</h1>
    <br>
    <p class="dotted">This is a Pico W running an HTTP server with CircuitPython.</p>
    <br>
    <p class="dotted">The current ambient temperature near the Pico W is
    <span style="color: deeppink;">{temp_test:.2f}°{unit}</span></p><br>
    <h1>Control the LED on the Pico W with these buttons:</h1><br>
    <form accept-charset="utf-8" method="POST">
    <button class="button" name="LED ON" value="ON" type="submit">LED ON</button></a></p></form>
    <p><form accept-charset="utf-8" method="POST">
    <button class="button" name="LED OFF" value="OFF" type="submit">LED OFF</button></a></p></form>
    <h1>Party?</h>
    <p><form accept-charset="utf-8" method="POST">
    <button class="button" name="party" value="party" type="submit">PARTY!</button></a></p></form>
    </body></html>
    """
    return html

#  route default static IP
@server.route("/")
def base(request: Request):  # pylint: disable=unused-argument
    #  serve the HTML f string
    #  with content type text/html
    return Response(request, f"{webpage()}", content_type='text/html')

#  if a button is pressed on the site
@server.route("/", POST)
def buttonpress(request: Request):
    #  get the raw text
    raw_text = request.raw_request.decode("utf8")
    print(raw_text)
    #  if the led on button was pressed
    if "ON" in raw_text:
        #  turn on the onboard LED
        led.value = True
    #  if the led off button was pressed
    if "OFF" in raw_text:
        #  turn the onboard LED off
        led.value = False
    #  if the party button was pressed
    if "party" in raw_text:
        #  toggle the parrot_pin value
        parrot_pin.value = not parrot_pin.value
    #  reload site
    return Response(request, f"{webpage()}", content_type='text/html')

print("starting server..")
# startup the server
try:
    server.start(str(wifi.radio.ipv4_address))
    print("Listening on http://%s:80" % wifi.radio.ipv4_address)
#  if the server fails to begin, restart the pico w
except OSError:
    time.sleep(5)
    print("restarting..")
    microcontroller.reset()
ping_address = ipaddress.ip_address("8.8.4.4")

#  text objects for screen
#  connected to SSID text
connect_text_area.text = "Connected to:"
ssid_text = f"{os.getenv('CIRCUITPY_WIFI_SSID')}"
ssid_text_area = label.Label(
    terminalio.FONT, text=ssid_text, color=0xFFFFFF, x=0, y=offset_y+15
)
splash.append(ssid_text_area)
#  display ip address
ip_text = f"IP: {wifi.radio.ipv4_address}"
ip_text_area = label.Label(
    terminalio.FONT, text=ip_text, color=0xFFFFFF, x=0, y=offset_y+30
)
splash.append(ip_text_area)
#  display temp reading
temp_text = f"Temperature: {temp_test:.2f} F"
temp_text_area = label.Label(
    terminalio.FONT, text=temp_text, color=0xFFFFFF, x=0, y=offset_y+45
)
splash.append(temp_text_area)

#  party parrot display group
parrot_group = displayio.Group()
#  load in party parrot bitmap
parrot_bit, parrot_pal = adafruit_imageload.load("/partyParrots64.bmp",
                                                 bitmap=displayio.Bitmap,
                                                 palette=displayio.Palette)
parrot_grid = displayio.TileGrid(parrot_bit, pixel_shader=parrot_pal,
                                 width=1, height=1,
                                 tile_height=64, tile_width=64,
                                 default_tile=1,
                                 x=32, y=0)
parrot_group.append(parrot_grid)

clock = time.monotonic() #  time.monotonic() holder for server ping
parrot = False #  parrot state
party = 0 #  time.monotonic() holder for party parrot
p = 0 #  index for tilegrid

while True:
    try:
        #  every 30 seconds, ping server & update temp reading
        if (clock + 30) < time.monotonic():
            if wifi.radio.ping(ping_address) is None:
                connect_text_area.text = "Disconnected!"
                ssid_text_area.text = None
                print("lost connection")
            else:
                connect_text_area.text = "Connected to:"
                ssid_text_area.text = f"{os.getenv('CIRCUITPY_WIFI_SSID')}"
                print("connected")
            clock = time.monotonic()
            #  comment/uncomment for desired units
            #  temp_test = ds18.temperature
            temp_test = c_to_f(ds18.temperature)
            temp_text_area.text = f"Temperature: {temp_test:.2f} F"

        #if parrot is True:
        if parrot_pin.value is True:
            #  switch to party parrot display group
            display.root_group = parrot_group
            if (party + 0.1) < time.monotonic():
                #  the party parrot animation cycles
                parrot_grid[0] = p
                #  p is the tilegrid index location
                p = (p + 1) % 10
                party = time.monotonic()
        #  if it isn't a party
        else:
            #  show default display with info
            display.root_group = splash
        #  poll the server for incoming/outgoing requests
        server.poll()
    # pylint: disable=broad-except
    except Exception as e:
        print(e)
        continue

Upload the Code and Libraries to the Pico W

After downloading the Project Bundle, plug your Pico W into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Pico W's CIRCUITPY drive. 

  • lib folder
  • code.py
  • partyParrots64.bmp

Your Pico W CIRCUITPY drive should look like this after copying the lib folder, partyParrots64.bmp file and the code.py file.

CIRCUITPY

Add Your settings.toml File

As of CircuitPython 8.0.0, there is support for Environment Variables. Environment variables are stored in a settings.toml file. Similar to secrets.py, the settings.toml file separates your sensitive information from your main code.py file. Add your settings.toml file as described in the Create Your settings.toml File page earlier in this guide. You'll need to include your CIRCUITPY_WIFI_SSID and CIRCUITPY_WIFI_PASSWORD.

CIRCUITPY_WIFI_SSID = "your-ssid-here"
CIRCUITPY_WIFI_PASSWORD = "your-ssid-password-here"

How the CircuitPython Code Works

The code begins by setting up the onboard LED, LED connected to pin GP10 and the OneWireBus for the DS18B20 sensor.

#  onboard LED setup
led = DigitalInOut(board.LED)
led.direction = Direction.OUTPUT
led.value = False

#  pin used for party parrot animation
parrot_pin = DigitalInOut(board.GP10)
parrot_pin.direction = Direction.OUTPUT
parrot_pin.value = False

# one-wire bus for DS18B20
ow_bus = OneWireBus(board.GP6)

# scan for temp sensor
ds18 = DS18X20(ow_bus, ow_bus.scan()[0])

Convert Celsius to Fahrenheit

The function c_to_f() converts Celsius temperature readings to Fahrenheit. 

#  function to convert celcius to fahrenheit
def c_to_f(temp):
    temp_f = (temp * 9/5) + 32
    return temp_f

OLED Setup

The I2C OLED is instantiated over I2C with the PiCowbell's STEMMA QT port pins (GP4 and GP5). 

# STEMMA I2C on picowbell
i2c = busio.I2C(board.GP5, board.GP4)
display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset)

WIDTH = 128
HEIGHT = 64
offset_y = 5

display = adafruit_displayio_ssd1306.SSD1306(display_bus, width=WIDTH, height=HEIGHT)

# default display group
splash = displayio.Group()
display.root_group = splash

Static IP and Connect to WiFi

As the network connection is established, the text "Connecting..." is shown on the I2C display. First, a static IP address is set with set_ipv4_address(). You can edit the IP address ipv4 to change to the IP address of your choice. By default, it is 192.168.1.42.

Then, the Pico W connects to your SSID by accessing your SSID and SSID password in your .env file. 

#  connect to network
print()
print("Connecting to WiFi")
connect_text = "Connecting..."
connect_text_area = label.Label(
    terminalio.FONT, text=connect_text, color=0xFFFFFF, x=0, y=offset_y
)
splash.append(connect_text_area)

#  set static IP address
ipv4 =  ipaddress.IPv4Address("192.168.1.42")
netmask =  ipaddress.IPv4Address("255.255.255.0")
gateway =  ipaddress.IPv4Address("192.168.1.1")
wifi.radio.set_ipv4_address(ipv4=ipv4,netmask=netmask,gateway=gateway)
#  connect to your SSID
wifi.radio.connect(os.getenv('CIRCUITPY_WIFI_SSID'), os.getenv('CIRCUITPY_WIFI_PASSWORD'))

print("Connected to WiFi")
pool = socketpool.SocketPool(wifi.radio)
server = Server(pool, "/static", debug=True)

HTML Variables

A few variables are created for the HTML script. The HTML is passed as an f-string, so variables from code.py can be included. temp_test holds the temperature reading from the DS18B20. You can adjust this to utilize either Celsius or Fahrenheit.

font_family is the font used for the CSS styling. You can change this to your preferred CSS font.

#  variables for HTML
#  comment/uncomment desired temp unit

#  temp_test = str(ds18.temperature)
#  unit = "C"
temp_test = str(c_to_f(ds18.temperature))
unit = "F"
#  font for HTML
font_family = "monospace"

HTML Code as a String

The HTML code is passed as an f-string in the webpage() function. The function returns a string that can be passed with HTTPResponse on a @server.route() function.

It's important to note that by using this method, you need to use double curly brackets ({{ and }}) if something in the HTML code actually needs to be inside curly brackets, such as certain parts of CSS style formatting.

#  the HTML script
#  setup as an f string
#  this way, can insert string variables from code.py directly
#  of note, use {{ and }} if something from html *actually* needs to be in brackets
#  i.e. CSS style formatting
def webpage():
    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-type" content="text/html;charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
    html{{font-family: {font_family}; background-color: lightgrey;
    display:inline-block; margin: 0px auto; text-align: center;}}
      h1{{color: deeppink; padding: 2vh; font-size: 35px;}}
      p{{font-size: 1.5rem;}}
      .button{{font-family: {font_family};display: inline-block;
      background-color: black; border: none;
      border-radius: 4px; color: white; padding: 16px 40px;
      text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}}
      p.dotted {{margin: auto; height: 50px;
      width: 75%; font-size: 25px; text-align: center;}}
    </style>
    </head>
    <body>
    <title>Pico W HTTP Server</title>
    <h1>Pico W HTTP Server</h1>
    <p class="dotted">This is a Pico W running an HTTP server with CircuitPython.</p>
    <p class="dotted">The current ambient temperature near the Pico W is 
    <span style="color: deeppink;">{temp_test}°{unit}</span></p>
    <h1>Control the LED on the Pico W with these buttons:</h1>
    <form accept-charset="utf-8" method="POST">
    <button class="button" name="LED ON" value="ON" type="submit">LED ON</button></a></p></form>
    <p><form accept-charset="utf-8" method="POST">
    <button class="button" name="LED OFF" value="OFF" type="submit">LED OFF</button></a></p></form>
    <h1>Party?</h>
    <p><form accept-charset="utf-8" method="POST">
    <button class="button" name="party" type="submit">PARTY!</button></a></p></form>
    </body></html>
    """
    return html

Server Routing and POST Requests

When the static IP address is accessed, the server serves the HTML code in webpage() with Response(). The content_type needs to be defined as "text/html".

The other routing option occurs when an HTTP POST request is sent to the server. When a POST request comes in, the text is decoded into raw_text. POST requests are sent when the buttons are pressed on the HTML site.

The buttons have names and values defined in the HTML code that are sent as a part of the POST request. When the POST request contains "ON", that means that the LED ON button was pressed on the site and the onboard LED on the Pico W is turned on. When a POST request contains "OFF", that means that the LED OFF button was pressed on the site and the onboard LED on the Pico W is turned off. Finally, when a POST request contains "party", it means that the PARTY! button was pressed on the site and the party_pin is toggled.

#  route default static IP
@server.route("/")
def base(request: Request):  # pylint: disable=unused-argument
    #  serve the HTML f string
    #  with content type text/html
    return Response(request, f"{webpage()}", content_type='text/html')

#  if a button is pressed on the site
@server.route("/", POST)
def buttonpress(request: Request):
    #  get the raw text
    raw_text = request.raw_request.decode("utf8")
    print(raw_text)
    #  if the led on button was pressed
    if "ON" in raw_text:
        #  turn on the onboard LED
        led.value = True
    #  if the led off button was pressed
    if "OFF" in raw_text:
        #  turn the onboard LED off
        led.value = False
    #  if the party button was pressed
    if "party" in raw_text:
        #  toggle the parrot_pin value
        parrot_pin.value = not parrot_pin.value
    #  reload site
    return Response(request, f"{webpage()}", content_type='text/html')

Start the Server

The server starts up on the static IP address with server.start(). This is wrapped in a try/except statement in case of an OSError, causing the server to be unable to start. If this occurs, then the Pico W is reset with microcontroller.reset().

After the server has started up successfully, the text on the OLED is updated to say that it is connected along with your SSID name, IP address and the temperature reading from the DS18B20.

print("starting server..")
# startup the server
try:
    server.start(str(wifi.radio.ipv4_address))
    print("Listening on http://%s:80" % wifi.radio.ipv4_address)
#  if the server fails to begin, restart the pico w
except OSError:
    time.sleep(5)
    print("restarting..")
    microcontroller.reset()
ping_address = ipaddress.ip_address("8.8.4.4")

#  text objects for screen
#  connected to SSID text
connect_text_area.text = "Connected to:"
ssid_text = "%s" % os.getenv('WIFI_SSID')
ssid_text_area = label.Label(
    terminalio.FONT, text=ssid_text, color=0xFFFFFF, x=0, y=offset_y+15
)
splash.append(ssid_text_area)

#  display ip address
ip_text = "IP: %s" % wifi.radio.ipv4_address
ip_text_area = label.Label(
    terminalio.FONT, text=ip_text, color=0xFFFFFF, x=0, y=offset_y+30
)
splash.append(ip_text_area)
#  display temp reading
temp_text = "Temperature: %.02f F" % float(temp_test)
temp_text_area = label.Label(
    terminalio.FONT, text=temp_text, color=0xFFFFFF, x=0, y=offset_y+45
)
splash.append(temp_text_area)

Party Parrot Tilegrid

There is a party parrot sprite sheet that is shown as an animated sprite sequence when the PARTY! button is pressed on the HTML site. The sprite sheet is added to a second displayio.Group() called parrot_group. This lets you display either the default splash group with the server information text or the party parrot sprite sheet with parrot_group.

#  party parrot display group
parrot_group = displayio.Group()
#  load in party parrot bitmap
parrot_bit, parrot_pal = adafruit_imageload.load("/partyParrots64.bmp",
                                                 bitmap=displayio.Bitmap,
                                                 palette=displayio.Palette)
parrot_grid = displayio.TileGrid(parrot_bit, pixel_shader=parrot_pal,
                                 width=1, height=1,
                                 tile_height=64, tile_width=64,
                                 default_tile=1,
                                 x=32, y=0)
parrot_group.append(parrot_grid)

Variables

The last portion before the loop are some variables. clock is a time.monotonic() device that will be used for timekeeping. parrot is a state machine to track if the party parrot animation should be playing. party is a time.monotonic() device for showing the party parrot animation and p is the variable for the party parrot tilegrid index.

clock = time.monotonic() #  time.monotonic() holder for server ping
parrot = False #  parrot state
party = 0 #  time.monotonic() holder for party parrot
p = 0 #  index for tilegrid

The Loop

In the loop, every 30 seconds, the Pico W sends out a ping to check the connection status. If the ping is not returned, then the OLED text is updated to Disconnected!. If a ping is returned, then the text remains as Connected to: with your SSID name. Additionally, the temperature reading from the DS18B20 is taken, updating the OLED text entry and the entry for the HTML f string.

while True:
    try:
        #  every 30 seconds, ping server & update temp reading
        if (clock + 30) < time.monotonic():
            if wifi.radio.ping(ping_address) is None:
                connect_text_area.text = "Disconnected!"
                ssid_text_area.text = None
                print("lost connection")
            else:
                connect_text_area.text = "Connected to:"
                ssid_text_area.text = "%s" % os.getenv('WIFI_SSID')
                print("connected")
            clock = time.monotonic()
            #  comment/uncomment for desired units
            #  temp_test = str(ds18.temperature)
            temp_test = str(c_to_f(ds18.temperature))
            temp_text_area.text = "Temperature: %d F" % temp_test

Party Parrot Animation

If the parrot_pin.value is toggled to True by the PARTY! button on the HTML site, then the OLED shows the parrot_group tilegrid for the party parrot sprite sheet. Every 0.1 seconds, the tilegrid is advanced by 1 to play the animation. When the pin is toggled to False, then the display shows the splash group, displaying the server information.

#if parrot is True:
        if parrot_pin.value is True:
            #  switch to party parrot display group
            display.root_group = parrot_group
            if (party + 0.1) < time.monotonic():
                #  the party parrot animation cycles
                parrot_grid[0] = p
                #  p is the tilegrid index location
                p = (p + 1) % 10
                party = time.monotonic()
        #  if it isn't a party
        else:
            #  show default display with info
            display.root_group = splash

Poll the Server

The final, and most important, part of the loop is server.poll(). This function polls the server for incoming and outgoing requests from both code.py and the HTML site.

#  poll the server for incoming/outgoing requests
server.poll()

This guide was first published on Nov 09, 2022. It was last updated on Jun 16, 2024.

This page (Code the Pico W HTTP Server) was last updated on Jun 16, 2024.

Text editor powered by tinymce.