Close

Software

A project log for Pico.Lab - Web-GUI Embedded Multitool

Open-Source Web-GUI Embedded Multitool based on Raspberry Pico

maxkMax.K • 2 hours ago•0 Comments

One main inspiration for this project was the web interface of the GP2040-CE game controller firmware. I remember being surprised that I could access the interface with a browser after simply plugging the Raspberry Pi Pico into my computer's USB port. But how does this work without Wi-Fi? The RP2350 has full control over its USB interface and can act as basically any USB device, including a network adapter. An lwIP server handles incoming web requests for its IP address, just like opening the configuration interface of your Wi-Fi router. It took me a while to get this setup running using TinyUSB and lwIP inside the Raspberry Pi Pico SDK, but eventually I was looking at my first “hello world” website.

In order to host a website from the Pico, the entire content has to be stored within the SPI flash. Luckily, the CMake function pico_set_lwip_httpd_content can automatically convert raw files to C code during compilation. This way, all the HTML, JS, and CSS files for the web interface can be stored in a folder within the code project as plain text.

As a next step, I needed to figure out how to send and receive data between the web interface and the actual firmware running on the RP2350. The final firmware would need all kinds of data sent back and forth—for example, scope captures, trigger settings, and current telemetry. My first idea was WebSockets because they allow fast two-way communication over a single TCP connection. But WebSockets turned out to be too complex and CPU-heavy for such a small microcontroller. What I ended up using is a mix of simple HTTP GET/POST requests and Server-Sent Events (SSE). Unlike WebSockets, SSEs are unidirectional and only allow text-based data, but they require far fewer resources. Depending on the application, SSE is used for frequent tasks and HTTP for sporadic events or large transfers:

With the data sending and receiving figured out, I moved on to the UI design. My goal for the user interface was for it to be super easy to use while still providing all the controls you would get from a CLI-based tool. I decided to go with a window-based system where each of the tools or instruments is displayed as an app. Only the necessary apps need to be opened, and they can be viewed side by side if needed. I did not want to reinvent the wheel, so I looked for a window manager that would offer a similar experience to interfaces like Synology DSM. I found WinBox.js, which is an open-source, lightweight HTML5 window manager, and integrated it into my web interface as a starting point. Some modifications and additions were necessary to style the windows and display them in a taskbar, but all animations and logic are handled by WinBox itself. I also added an application launcher, similar to the Windows Start menu, in the top-left corner. The desktop itself is empty, only showing a wallpaper as a backdrop. Windows and menus have a frosted-glass look and a permanent dark mode.

From this point on, I started developing the apps for each tool one by one. Because of the complexity and my practically non-existent HTML skills, I used AI heavily for the user interface and also for large parts of the firmware. This usually consisted of me sketching the interface and then letting the AI implement it. This worked surprisingly well for most of the functionality. Still, tons of testing, bug fixes, and optimization were needed to make the apps robust and have them look consistent. After each new app was mostly done, I integrated it into the main web UI and had the code refactored.

The first finished app was the GPIO controller. It uses HTTP GET to set pin states and modes. Only the necessary pins are displayed as cards, with a small map of the available GPIO pins at the top. Most other apps after this use the same or a similar-looking interface for picking pins and functions. The data logger was next. Since it uses the existing telemetry stream for its data, it’s quite simple. A graph of all selected pins is drawn in real time. What’s great about a web UI is that all the heavy lifting is done by the browser, so there are almost no limits on how the UI can look and what it can do.

The oscilloscope was the first app that was really tricky and hit the limits of the server. Configuring trigger options is simple, but after the capture, a lot of data has to be sent to the browser. Streaming raw floats or heavy JSON objects over SSE immediately choked the lwIP stack and caused the telemetry to stutter. This was fixed by moving to raw binary buffers encoded in Base64. To keep the UI smooth at higher sampling rates without starving the USB stack, I made use of the RP2350's dual-core architecture. Core 0 handles all the network traffic, the lwIP HTTP/SSE daemons, and background telemetry. Meanwhile, Core 1 is dedicated entirely to high-speed data acquisition. It configures a Direct Memory Access (DMA) channel to stream round-robin ADC samples directly into a RAM buffer, freeing up the CPU to hunt for trigger conditions in real time. Once triggered, the buffer gets Base64-encoded and pushed out through SSE, making for a responsive oscilloscope view right inside the browser. I tried to pull all kinds of tricks to improve the bandwidth, but even with overclocked ADCs, it sits at around 400kHz. That is good enough for basic debugging of PWM signals, but not enough for looking at communication data from fast logic buses.

Next up was the Logic Analyzer. While the scope relies on analog ADCs, the logic analyzer needs to capture fast digital state changes across the GPIO pins simultaneously. Capturing thousands of digital samples at megahertz speeds generates massive blocks of data. Rather than trying to stream this live—which would instantly flood the 12 Mbps USB Full Speed bus—I went with a capture-then-download approach. The user sets the trigger conditions in the UI, the RP2350 captures the digital bus state into RAM, and then it compresses the data using Run-Length Encoding (RLE). The compressed blocks are sent over SSE in chunks, and the browser unpacks them locally to render them. The waveforms can then be zoomed in and panned. I am quite happy with the 100MS/s that the analyzer can achieve. However, since it is sampling into its internal RAM, the capture time is relatively small.

The PSU app mostly displays the telemetry data for the voltage rails. It also allows the user to set the voltage and current limit for the adjustable voltage rail. One problem that I had to fix was the MP28167 turning on by default. There is just no way in hardware to enable the converter without also turning on the output voltage. Of course, the voltage can be disabled via I2C once the converter is enabled. The firmware has to quickly turn on the converter, then bombard it with I2C messages telling it to turn off its output. I was not the first one to run into this exact problem: https://github.com/MisterHW/EVAL-MP8862

While I was adding more and more apps, the HTML and JavaScript files increased in size as well. The initial loading time for the website eventually increased to 10 seconds. Wireshark analysis revealed two main culprits: Windows waiting on a DNS timeout for Web Proxy Auto-Discovery (WPAD), and a sort of traffic jam caused by the telemetry filling up the tiny lwIP transmission queue during page reloads. By configuring the Pico's DNS server as a captive portal to instantly intercept and handle those background OS queries and expanding the TX buffer queue size, the connection bottleneck vanished, making the interface load almost instantaneously.

The final apps were the UART Terminal, I2C and SPI apps. The terminal uses HTTP POST requests for outgoing data, while incoming serial data streams back continuously via SSE. 

Overall, I tried to keep the memory and flash footprint of the firmware small enough so it works even on the original RP2040-based Raspberry Pi Pico. The wallpaper is a 3KB .svg file, and the pinout diagrams are stored as efficient .webp images at only 80KB. I hand-drew most of the icons as vector images and tried to keep them simple yet modern.

Discussions