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.
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"
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])
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
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
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)
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"
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
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')
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)
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)
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
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
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
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()
Text editor powered by tinymce.