RP2040 MicroPython: Making a Web Server

9 minute read

Where I begin to develop a web server for the RP2040 (Pi Pico and Pi Pico W) using MicroPython and microdot.


The Pico W added wireless capability to the Pico, which brought the ability to network the Pico board. Once a board can be networked, it becomes valuable to have it serve web pages. An HTTP-compliant web page is a simple first step to using the Pico W via a network. An IOT-focused protocol such as MQTT would be a very good, second step.

Web Framework

It is far better to use someone’s web server framework, than to develop a server on your own. This accelerates your ability to create a usable product and provides a powerful base on which to build. I went with microdot, written by Miguel Grinberg, an accomplished software engineer and educator.


The microdot webserver is outstanding work by Miguel Grinberg! The documentation is very good, the interfaces fairly easy to understand and everything works!

To make styling easy to use and kind of lightweight, I used a CSS-reset/normalize framework, marx. It worked for me, it is light (10kb) and is simple. I was able to accomplish what I needed with a nice look.

The code is on GitHub.

The Version 3 webpage looks like this:

Microserver Version 3webpage

Microserver Version 3 webpage

Large Version to see detail

Getting Started

I’ll walk through the process, in creating the three versions, so you can understand what is required.

Microdot Example Web Server

To test the concept, I begin with the example code from microdot:

from microdot import Microdot

app = Microdot()

def index(request):
    return 'Hello, world!'


I browse to and success! “Hello, world!

Details for Determining IP address

All of my webserver programs use a program called wlan. The process will be the same for all…

When the Pico W resets, it will attempt to connect to the SSID using the information in secrets.py. The built-in LED will blink slowly, while in progress. If a wireless connection is made, the LED will turn off and the program will print the following information: (If a connection can’t be made, review the debugging instructions in the README.)

IP Address:
MAC Address: 28:cd:c1:08:a9:7d
Channel: 1
SSID: Demo
TX Power: 31
Starting sync server on

It is also critical the device (phone or PC) which are using to connect with the Pico is on the same network as well. Be sure you are connected to the same SSID.

The address you enter in your browser is a combination of the addresses supplied above. Use the IP Address combined with the port number following the as in :5001. Using the above data, you would need to go to this address:

Why Templates and not Javascript?

Finally, a quick comment as to why not be more responsive and use javascript or vue or angular? Using javascript etc, is a client-side approach, which is preferable when you want to offer a fast response to the user. However, it can also cause problems where the responsiveness of the webpage is too fast for the Pico and it may become unstable or in an unknown state. I would rather have the Pico in complete control of what appears (which means server-side) and this ensures there is a greater chance for stability.


The microserver program has seven versions, to show iterations of improvement. The README has the details as to how to load each version. The README has quite a bit of detail as to how to work with the Pico W, I recommend reading it along with this entry.

Version 1

This version will light the built-in LED. It is very simple and the objective is to understand how the HTTP interface works.

Microserver Version 1 webpage

Microserver Version 1 webpage

Large Version to see detail

Lines 7-14

This code was described here and determines if running on a Pico, if so, it attempts to make a wireless connection. When it does, it provides the information described above. It will be in all web-based programs and won’t be mentioned again.

# Required for WLAN on Pico W, 'machine' indicates Pico-based micropython
# Will not differeniate between Pico and Pico W!
if hasattr(sys.implementation, '_machine'):
    from wlan import connect
    if not (connect()):
        print(f"wireless connection failed")

Line 15

Setup built-in LED to be an Output and name it builtin. builtin = Pin("LED", Pin.OUT)

Lines 17-23

Very similar to the microdot example, this code starts the webserver and provides the initial webpage. I have created a folder called templates as we will want to serve pages from this folder in the future.

app = Microdot()

def index(request):
    return send_file('templates/index.html')

Lines 25-32

This is the critical work. It uses a HTTP POST method. This type of operation allows us to retrieve data from a web page. The easiest way to understand (and debug) this, is to open your browser and use the developer tools. [Use search to find out how to use Developer tools for your desired browser.]

With your developer tool, you can see the data which is sent on each button click. For example, FireFox Browser Developer Edition will show the following:

FireFox Post Request

FireFox Post Request

Large Version to see detail

This is what the browser will show immediately following a click on “ON” and click on “Lights!”. It has two parts, the word level: and the value “1”. If we were to click on “OFF”, we would see the value “0”. This means, we’re able to turn the LED on (level = 1) and off, (level = 0). We do that by the code below:

  1. First we get the value of level and convert it to an integer.
  2. We then use it to set the value for the builtin LED.
    def index_post(request):
        level = int(request.form['level'])
        return send_file('templates/index.html')

Lines 34-40

This code will need to be duplicated for every file which needs to be served. For example, when we add a CSS style file, we will have a similar section of code or if you wish to offer an ICO (web icon file). The code simply provides a route to the webserver as to what to send when the route is requested by the browser.

The last line will start the webserver. If you wish to change the port number from 5000, you may add port=5001 as in app.run(port=5001, debug=True)

def computer_svg(request):
    return send_file('./computer.svg',
                     content_type='image/svg+xml', max_age=31536000)


Version 2

Lines 7-21

The next version expands on the number of LEDs, we may light up. I added a yellow, green, red, and blue led to pins GP2, GP15, GP16 and GP22, respectively. The code to enable them and provide the ability to turn them on/off is this:

yellow = Pin(2, Pin.OUT)
green = Pin(15, Pin.OUT)
red = Pin(16, Pin.OUT)
blue = Pin(22, Pin.OUT)

def set_led(color, level):
    if color == 'RED':
    if color == 'GREEN':
    if color == 'BLUE':
    if color == 'YELLOW':

Lines 41-47

With four LEDs instead of one, I added another value to the form called led. It provides a color upon selection based on the color of the led desired. I use similar code to get the values, however, I do the integer conversion in the led function instead of on getting the value.

def index_post(request):
    level = request.form['level']
    led = request.form['led']
    print("Set", led, "led", level)
    set_led(led, level)
    return send_file('templates/index.html')
Microserver Version 2 webpage

Microserver Version 2 webpage

Large Version to see detail

It works, however, from a user experience (UX) perspective, it needs work. Your inclination is to select multiple LEDs and having to click and additional button for on or off seems tedious. It would be nice to check a box, if you want the LED on or uncheck for off for all of the LEDs at once.

Version 3

Along with the UX concern voiced above, the long line of text is also a bit of a problem as well as the serif font. To fix the latter problem, we had a file which cleans up our CSS a bit called marx. It provides a nice CSS palatte for our web page. Notice we had to add a section of code to serve the file as well. (Lines 66-69) And we added the file to the templates folder in the Pico.

We also added a folder of code called utemplate. This code provides the capability of templating. Templating is the ability to use placeholders in your code and change the value of the placeholder, programmatically.

We use this concept to replace the radio buttons on the previous two versions with checkboxes. A checkbox will appear checked if the word “checked” follows it, in the HTML. We use an arra called led_state to track that for the four leds, so now when the led is on, the checkbox will be checked. If off, checked will be a "" or null string.

The code is a bit cumbersome as the POST operation only returns which boxes are checked. And if no boxes are checked, its important to set all of the values in led_state to ‘’.

def set_led(leds):
    global led_state
    if len(leds) == 0:
        led_state = ['' for _ in range(4)]
        for led in leds:
            if 'YELLOW' in leds:
                led_state[0] = 'checked'
                led_state[0] = ''
            if 'GREEN' in leds:
                led_state[1] = 'checked'
                led_state[1] = ''
            if 'RED' in leds:
                led_state[2] = 'checked'
                led_state[2] = ''
            if 'BLUE' in leds:
                led_state[3] = 'checked'
                led_state[3] = ''

This version becomes the image you see near the top of this page.

Version 4

From here I continue to expand on the capability of the site. In this version, I add documentation as to the pins being used and the colors of the led’s attached to the pins. This required an include file as part of the template package. I pass a two-dimensional list (array) to the template than use the variables to provide the color and pin number. I found this version to be more cumbersome than useful, so I went on to version 5.

Version 5

This becomes the basis of my final version. I use a capability of the POST function to switch pages. The first page is the typical index.html, where I provide the capability for the user to set the label text and pin number (Pico W pin, not GPIO number). When the POST operation happens, it calls the second page called control.html, as it controls the LEDs. When this page loads, it uses a set of lists to determine the following:

  • label - the name attached to the pin
  • pin - the Pico W pin number (0-40)
  • gpio - the gpio corresponding to the pin number (0-22)
  • state - whether or not, the check box is checked

A nice feature of this version going forward, is that the LED colors could be labeled as in Warning, Success, Error, and Informational as compared to Yellow, Green, Red, and Blue, respectively.

Version 6

While I was happy with the capability of version 5, I believed the code was a bit cumbersome. I also found a new css template which was more representative what I wanted. To resolve the first issue, I changed the four lists with a common index to a class. This provided a stronger basis of self-documentation and the code would scale more easily. Using a class helped to simplify the code, however the code control_led() still seems a bit clunky, due to the HTML POST operation.

I saw another minimum css file called mvp.css. In the case of mvp.css, it was described specifically to provide the basis of a minimum-viable-product type website. Which is precisely what I have with this site. It is a bit more opinionated, however, seems easier to adjust than marx.css.


Comments powered by Talkyard.