In this guide, you'll setup an HTTP (web) server with a Pico W running CircuitPython. HTTP servers are handy for creating custom web interfaces to monitor and control IoT projects.

This example sets a static IP address for the server, logs temperature readings from a DS18B20 temperature sensor, displays server information on an OLED and serves an HTML webpage with buttons that can send HTTP POST requests to toggle pins on the Pico W.

The HTML webpage is included in the code.py file as an f-string so that it can take in variables directly from code.py. The HTML code includes buttons that send POST requests for turning the Pico W onboard LED on and off and for displaying a party parrot animation on the OLED.

The OLED displays important information about the server. It tells you the connection status, the name of your SSID, the IP address and the current temperature reading from the DS18B20. It can also show the less important, but more fun, party parrot animation.

Since the Pico W is often affectionately referred to as the Pi-Cow, a bovine-themed 3D printed case is included for this project.

Prerequisite Guides

Parts

Angled shot of a green microcontroller with castellated pads.
The Raspberry Pi foundation changed single-board computing when they released the Raspberry Pi computer, now they're ready to...
Fully Reversible Pink/Purple USB A to micro B Cable
This cable is not only super-fashionable, with a woven pink and purple Blinka-like pattern, it's also fully reversible! That's right, you will save seconds a day by...
Monochrome 1.3" OLED module with Adafruit logos falling like snow
These displays are small, only about 1.3" diagonal, but very readable due to the high contrast of an OLED display. This display is made of 128x64 individual white OLED pixels,...
Waterproof 1-Wire DS18B20 Compatible Digital temperature sensor coiled around a US quarter and a resister above it.
This is a pre-wired and waterproofed (with heat shrink) version of a 1 Wire DS18B20 sensor. Handy for when you need to measure something far away, or in wet conditions. While the...
Angled shot of long, skinny prototyping breakout board.
Ding dong! Hear that? It's the PiCowbell ringing, letting you know that the new Adafruit PiCowbell Proto is finally in stock and ready to assist your
Angled shot of STEMMA QT / Qwiic JST SH 4-pin Cable.
This 4-wire cable is a little over 100mm / 4" long and fitted with JST-SH female 4-pin connectors on both ends. Compared with the chunkier JST-PH these are 1mm pitch instead of...
1 x Socket Headers for Raspberry Pi Pico
2 x 20 pin Female Headers
1 x 0.1" male header
Break-away 0.1" 36-pin strip male header
1 x 220 ohm resistors
Through-Hole Resistors - 220 ohm 5% 1/4W - Pack of 25 Through-Hole Resistors - 220 ohm 5% 1/4W - Pack of 25
1 x Machine Screw and Stand-off Set – M2.5 Thread
Black Nylon Machine Screw and Stand-off Set – M2.5 Thread
1 x 5mm RGB Slow Blinking LED
Auto changing RGB LED
1 x M2 Brass Standoffs
M2 standoffs

OLED

  • OLED STEMMA QT port to PiCowbell STEMMA QT port

DS18B20

  • Sensor GND to PiCowbell GND (blue wire)
  • Sensor Data to PiCowbell GP6 (yellow wire)
  • Sensor VIN to PiCowbell 3V (red wire)
  • 4.7K ohm resistor between 3V and GP6

LED for Party Parrot

  • LED cathode to PiCowbell GND (black wire)
  • LED anode to 220 ohm resistor (white wire)
  • 220 ohm resistor to PiCowbell GP10

Plug in the Pico W

After wiring, plug the Pico W into the PiCowbell. The Pico W USB port should be over the PiCowbell STEMMA QT port.

PiCowbell Headers

Solder socket headers to the PiCowbell. You can use the Pico W as a jig to keep the headers secure.

Resistors

Solder the 4.7K ohm resistor for the DS18B20 from the 3V line to GP6 on the PiCowbell. Then, solder a 220 ohm resistor from GP10 to one of the prototyping rails on the PiCowbell.

JST Cable

Cut the JST cable in half. Splice and tin the wires on the socket end of the cable.

Solder the black wire of the JST cable to the GND pin on the PiCowbell. Solder the red wire of the JST cable to the prototyping rail connected to the 220 ohm resistor.

DS18B20

Tin the three wires of the DS18B20. Blue is ground, yellow is data and red is power.

Solder the DS18B20 to the PiCowbell.

  • Blue wire to PiCowbell GND
  • Red wire to PiCowbell 3V
  • Yellow wire to the prototyping rail next to GP6

Bridge the solder joints from the 4.7K ohm resistor on GP6 to the DS18B20 yellow wire.

That completes the PiCowbell wiring.

LED to JST

Take the remaining plug end of the JST cable and slip a piece of heat shrink over each of the wires.

Solder the LED cathode to the black wire on the JST cable. Solder the LED anode to the red wire on the JST cable.

Apply the heat shrink over the LED legs and the solder connections.

Plug the LED JST cable into the JST socket and plug the Pico W into the PiCowbell. That completes all of the wiring!

The HTTP server may be assembled with 3D printed parts, described below. The case has three lid options depending on your taste: a plain lid, a lid with a cow print or a lid with a cow face. The cow print and cow face versions of the lid can utilize the M600 filament change command in your slicer software to accentuate the design. 

The STL files can be downloaded directly here or from Thingiverse.

Main Case

The main case portion needs supports for the DS18B20 temperature sensor slot. In your slicer software, you can use a support blocker to only add supports to the sensor slot.

Cow Print Lid

For the cow print lid, slice the model with a 0.2 mm layer height and insert a filament change command on layer 2.

Cow Face Lid

For the cow face lid, use the support on build plate only option in your slicer. Slice the model with a 0.2 mm layer height and insert filament change commands on layers 25, 28 and 32.

The plain lid does not require any supports or filament changes. All of the lids have cutouts and mounting holes for the OLED screen and Pico W.

CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.

CircuitPython Quickstart

Follow this step-by-step to quickly get CircuitPython working on your board.

Click the link above and download the latest UF2 file.

Download and save it to your desktop (or wherever is handy).

Start with your Pico W unplugged from USB. Hold down the BOOTSEL button, and while continuing to hold it (don't let go!), plug the Pico W into USB. Continue to hold the BOOTSEL button until the RPI-RP2 drive appears!

If the drive does not appear, unplug your Pico W and go through the above process again.

A lot of people end up using charge-only USB cables and it is very frustrating! So make sure you have a USB cable you know is good for data sync.

You will see a new disk drive appear called RPI-RP2.

 

Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.

The RPI-RP2 drive will disappear and a new disk drive called CIRCUITPY will appear.

That's it, you're done! :)

Flash Resetting UF2

If your Pico W ever gets into a really weird state and doesn't even show up as a disk drive when installing CircuitPython, try installing this 'nuke' UF2 which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After nuking, re-install CircuitPython

CircuitPython works with WiFi-capable boards to enable you to make projects that have network connectivity. This means working with various passwords and API keys. As of CircuitPython 8, there is support for a settings.toml file. This is a file that is stored on your CIRCUITPY drive, that contains all of your secret network information, such as your SSID, SSID password and any API keys for IoT services. It is designed to separate your sensitive information from your code.py file so you are able to share your code without sharing your credentials.

CircuitPython previously used a secrets.py file for this purpose. The settings.toml file is quite similar.

Your settings.toml file should be stored in the main directory of your CIRCUITPY drive. It should not be in a folder.

CircuitPython settings.toml File

This section will provide a couple of examples of what your settings.toml file should look like, specifically for CircuitPython WiFi projects in general.

The most minimal settings.toml file must contain your WiFi SSID and password, as that is the minimum required to connect to WiFi. Copy this example, paste it into your settings.toml, and update:

  • your_wifi_ssid
  • your_wifi_password
CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"

Many CircuitPython network-connected projects on the Adafruit Learn System involve using Adafruit IO. For these projects, you must also include your Adafruit IO username and key. Copy the following example, paste it into your settings.toml file, and update:

  • your_wifi_ssid
  • your_wifi_password
  • your_aio_username
  • your_aio_key
CIRCUITPY_WIFI_SSID = "your_wifi_ssid"
CIRCUITPY_WIFI_PASSWORD = "your_wifi_password"
ADAFRUIT_AIO_USERNAME = "your_aio_username"
ADAFRUIT_AIO_KEY = "your_aio_key"

Some projects use different variable names for the entries in the settings.toml file. For example, a project might use ADAFRUIT_AIO_ID in the place of ADAFRUIT_AIO_USERNAME. If you run into connectivity issues, one of the first things to check is that the names in the settings.toml file match the names in the code.

Not every project uses the same variable name for each entry in the settings.toml file! Always verify it matches the code.

settings.toml File Tips

Here is an example settings.toml file.

# Comments are supported
CIRCUITPY_WIFI_SSID = "guest wifi"
CIRCUITPY_WIFI_PASSWORD = "guessable"
CIRCUITPY_WEB_API_PORT = 80
CIRCUITPY_WEB_API_PASSWORD = "passw0rd"
test_variable = "this is a test"
thumbs_up = "\U0001f44d"

In a settings.toml file, it's important to keep these factors in mind:

  • Strings are wrapped in double quotes; ex: "your-string-here"
  • Integers are not quoted and may be written in decimal with optional sign (+1, -1, 1000) or hexadecimal (0xabcd).
    • Floats, octal (0o567) and binary (0b11011) are not supported.
  • Use \u escapes for weird characters, \x and \ooo escapes are not available in .toml files
    • Example: \U0001f44d for 👍 (thumbs up emoji) and \u20ac for € (EUR sign)
  • Unicode emoji, and non-ASCII characters, stand for themselves as long as you're careful to save in "UTF-8 without BOM" format

 

 

When your settings.toml file is ready, you can save it in your text editor with the .toml extension.

Accessing Your settings.toml Information in code.py

In your code.py file, you'll need to import the os library to access the settings.toml file. Your settings are accessed with the os.getenv() function. You'll pass your settings entry to the function to import it into the code.py file.

import os

print(os.getenv("test_variable"))

In the upcoming CircuitPython WiFi examples, you'll see how the settings.toml file is used for connecting to your SSID and accessing your API keys.

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

Attach four M2 standoffs to the Pico W's four mounting holes with M2 nuts.

Attach the Pico W to the case lid with four M2 screws.

Attach the OLED screen with four M2.5 screws and nuts.

Plug the PiCowbell into the Pico W. The STEMMA port on the PiCowbell should be below the USB port on the Pico W.

Connect the OLED to the STEMMA port on the PiCowbell with a STEMMA QT cable.

Route the DS18B20 through the larger opening in the case. Slide the sensor through the clip on the top of the case.

Wrap the excess cable for the DS18B20 into the bottom of the case. Route the LED through the larger side opening.

Close the case with the lid. Mount the LED into the mounting hole on the lid in the cow's bell.

After powering up your Pico W, your server will begin running on your static IP address. Access your HTML webpage by opening a browser and navigating to the IP address. You can confirm your IP address by checking the OLED connected to the Pico W.

You should see your HTML code displayed in your browser window. The temperature reading from the DS18B20 will be shown towards the top of the screen.

Click the LED ON and LED OFF buttons on the webpage to turn the Pico W's onboard LED on and off.

Click the PARTY! button on the webpage to start and stop the party parrot animation on the OLED. The slow change RGB LED will also turn on and off.

Going Further

You can adapt this code to suit your project needs. You can change the look of the HTML page by changing the CSS styling or change the CircuitPython portion to control a relay or log multiple sensors around your home. For more information on the CircuitPython HTTPServer library, be sure to check out the library documentation on Read the Docs.

This guide was first published on Nov 09, 2022. It was last updated on Mar 28, 2024.