This project turns your Raspberry Pi Pico W into a Wi-Fi access-point. Connecting to it from your computer, tablet or phone will automatically redirect you to a page allowing to control individual pins of the board:
Simply click on the IN label to use a pin as an input (0/1 will be automatically updated every 500ms), or click on OUT to turn it into an output and then click on 0 or 1 to control it.
This project demonstrates how to use Raspberry Pi Pico W to host a web app with responsive CSS, a simple API, modal dialogs and a zero-configuration setup that allows users to open the app by connecting to the Wi-Fi network without having to enter any host names or IP addresses. The project is licensed under the MIT license and can be used as a template to create web apps for managing the configuration of your IoT devices.
The project demonstrates the following technologies:
- The use of DHCP, DNS and HTTP redirects to automatically show the app for all Wi-Fi clients
- A memory-efficient HTTP server capable of handling large requests with very little RAM
- A simple file system for storing multiple files (HTML, images) in the FLASH memory and serving them via HTTP
- A simple CSS layout sufficient to scale the page depending on the browser window size
- A simple API showing how to fetch meaningful data directly from the C code and present it in a meaningful way
This feature relies on the mechanism used by public networks (e.g. airport/hotel) to authenticate users, or make them accept the terms and conditions. When a client connects to such a network, it typically:
- Assigns the client a private DHCP address (e.g. 192.168.0.100) and points it to a local DNS server.
- When the client tries to resolve a hostname (e.g.
sysprogs.com
, it is resolved to the correct IP address). - When the client tries to connect to that address, the router intercepts the request and issues an HTTP 302 redirect to the login page.
Most modern browsers and operating systems support this behavior out-of-the-box by trying to connect to a test URL (e.g. http://www.msftconnecttest.com/redirect). If the connection results in a HTTP redirect, the operating system suggests opening the browser to login:
This mechanism is known as Captive Portals and is implemented by PicoHTTPServer as follows:
-
The DHCP server (taken from the access point example) issues the clients IP addresses from the pre-configured subnet, and reports itself as the gateway and DNS server (see
DHCP_OPT_ROUTER
andDHCP_OPT_DNS
). -
The DNS server compares the requested domain names to the configured name of the Pico W (e.g.
picohttp.piconet.local
). If the name matches, it returns the IP address of the Pico W itself. Note that if we return any private address when the client OS is testing the connection (e.g. resolving http://www.msftconnecttest.com/redirect), most OSes will conclude that it's a private network and won't show the login prompt. To work around it, we resolve all hostnames that don't match our own name to the secondary IP (the default configuration sets it to TEST-NET-2 that is 198.51.100.0). -
In order to answer requests to the secondary IP, we use a patched version of lwIP that it routes packets with this IP address to our netconn instance. From the client's perspective, this is similar to a network router.
-
Finally, the HTTP server checks the
Host
field in the HTTP request. If the request came for our hostname (e.g.picohttp.piconet.local
), it is handled normally. If not, it issues anHTTP/1.0 302 Found
redirect pointing to the primary hostname.
As the Raspberry Pi Pico W only has 256KB of RAM, this project uses its own memory-efficient implementation of the HTTP server designed with the following constraints in mind:
-
The entire HTTP request never needs to fit into the RAM. The server (see
parse_and_handle_http_request()
) reads and parses the request using a small memory window (4KB by default). The window needs to be big enough to fit the first line (method + path) and the longest line from the request header that we want to parse. All request headers that don't fit into the window (e.g. a very longUser-Agent
field) will be safely skipped without interfering with the rest of the fields. -
Likewise, the application logic can generate API responses using the printf()-style
http_server_write_reply()
function without having to fit the entire response into the buffer. As long as a single fragment formatted at once fits, the server will manage the buffering automatically. -
All data sent by the web app via POST requests can be read line-by-line without having to fit the entire request in memory.
This architecture allows handling HTTP requests at decent speeds with only 4KB/thread (+2KB default stack) that can be reduced further at some performance cost.
In order to support images, styles or multiple pages, the HTTP server includes a tool packing the served content into a single file (along with the content type for each file). The file is then embedded into the image, and is programmed together with the rest of the firmware. You can easily add more files to the web server by simply putting them into the www directory and rebuilding the project with CMake.
You can dramatically reduce the FLASH utilization by the web server content by pre-compressing the files with gzip and returning the Content-Encoding: gzip
header for the affected files. The decompression will happen on the browser side, without the need to include decompression code in the firmware.
Raspberry Pi Pico W has 2MB of FLASH memory (~256KB of which are used by the Pico SDK), so it cannot fit a full-scale web framework, or a PHP interpreter. However, it can easily serve JavaScript that will execute in the browser, making calls to various APIs and updating the page accordingly. This is demonstrated by the settings editing popup:
Whenever you click the 'settings' button in the browser, the following events take place:
- The JavaScript uses the
XMLHttpRequest
interface to send a GET request to the/api/settings
endpoint. The code indo_handle_api_call
inmain.c
handles this request, formatting the settings as a JSON object usinghttp_server_write_reply()
. - The JavaScript parses the reply and sets the fields in the settings popup. Note that the JSON parsing is done in the browser, so the code running on Raspberry Pi Pico doesn't need to handle it.
- When the user clicks the 'OK' button in the browser, the JavaScript formats the settings fields into a set of
key=value
lines and sends it as a POST request. - The code in
parse_server_settings()
reads and validates the values from the browser. Because the values are sent in plain text, the entire request doesn't need to fit into the memory at the same time. Instead, the code can read it line-by-line usinghttp_server_read_post_line()
.
The settings are stored in the FLASH memory together with the firmware and the web pages, so they are preserved when you reboot the device.
You can download the pre-built binary of the HTTP server from the releases page. Simply boot your Raspberry Pi Pico W into the bootloader, and copy the PicoHTTPServer.uf2 file on it. The Pico W will restart and create the PicoHTTP network.
The easiest way to build the sources on Windows is to install VisualGDB and open the PicoHTTPServer.sln
file in Visual Studio. VisualGDB will automatically install the necessary toolchain/SDK and will manage build and debugging for you.
You can also build the project manually by running the build-all.sh file. Make sure you have CMake and GNU Make installed, and that you have the ARM GCC (arm-none-eabi) in the PATH.
See this tutorial for detailed step-by-step instructions on adding a new dialog and the corresponding API to the app, as well as testing it out on the hardware.