Raspberry Pi Pico W Web Server in MicroPython: Control an LED and Read a Sensor
Build a small local web interface on a Raspberry Pi Pico W using MicroPython. The Pico W will join a Wi-Fi network, serve a page to a browser, let you switch an LED, and return a sensor reading. This project introduces networking, HTTP requests, GPIO, and basic error handling without requiring a cloud service. The example is designed for a private test network; it is not an internet-facing security system.
What you are building
A browser sends an HTTP request to the Pico W's IP address. The board accepts the connection, reads the request path, and returns a small HTML response. A request to /led?state=on turns the output on; /led?state=off turns it off. A request to /sensor returns a reading in a simple text response. The Pico W does not need Node.js, a desktop server, or a cloud account—the MicroPython script runs directly on the board.
Keep the first version intentionally simple. A single-threaded socket server is adequate for a small demonstration, but it may serve only one client at a time and can block if a client disconnects unexpectedly. Later sections cover practical limitations, security, and ways to extend it.
Parts and safe wiring
You need a Raspberry Pi Pico W, a USB data cable, a computer with Thonny, a breadboard, an LED, a current-limiting resistor (typically 220–1,000 ohms for a simple indicator), and optionally a supported digital sensor. This guide uses GP15 for the LED output and GP16 for a simple digital sensor example; verify the actual board pinout and sensor interface before wiring. Connect GP15 through the resistor to the LED anode and connect the LED cathode to GND. Never connect an LED directly to a GPIO without a series resistor.
For the sensor, use a module whose voltage and signal levels are compatible with 3.3 V GPIO. Connect sensor ground to Pico ground and its data output to the chosen GPIO. Some sensors need pull-up resistors or a specific driver; the sample handler below deliberately leaves sensor reading as a function you must adapt to the actual device. Do not assume a DHT, I²C, analog, or 5 V module uses the same wiring or API. Power down before changing connections.
Install MicroPython and connect with Thonny
Install a current stable MicroPython firmware build specifically for Raspberry Pi Pico W, not firmware intended for the original non-wireless Pico. Connect the board over USB and select the MicroPython (Raspberry Pi Pico) interpreter in Thonny. If the board is new or the interpreter cannot connect, use the board's BOOTSEL procedure and follow the official firmware installation steps for the Pico W. Keep the firmware version noted because networking and library behavior can change between releases.
Run a quick REPL check:
import machine
print(machine.Pin(15))
If this fails, verify that Thonny is using the board interpreter rather than the computer's local Python. If the USB device does not appear, try a known data-capable cable, another USB port, and the correct BOOTSEL process. Do not troubleshoot Wi-Fi until USB serial communication works.
Configure Wi-Fi
Use a 2.4 GHz Wi-Fi network; the Pico W does not connect to a 5 GHz-only network. The example below waits a limited time for association and prints the assigned IP address. Replace the placeholders locally and never publish your actual password in a public code repository or screenshot.
import network
import time
SSID = "YOUR_WIFI_NAME"
PASSWORD = "YOUR_WIFI_PASSWORD"
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
for _ in range(30):
if wlan.isconnected():
break
time.sleep(1)
if not wlan.isconnected():
raise RuntimeError("Wi-Fi connection timed out")
print("IP address:", wlan.ifconfig()[0])
Open your router's client list if the address is unclear. A DHCP address can change after a reboot, so consider a DHCP reservation in your router if you want a stable local bookmark. Do not hard-code an address that conflicts with another device. If the board cannot connect, check the SSID and password, signal strength, access-point compatibility, and whether the network uses a captive portal or client isolation.
Build the LED web server
Save the following as main.py on the Pico W. It uses socket to listen on port 80 and returns a minimal HTML page with links to control the LED. The server closes each client connection in a finally block and catches common socket errors so a malformed request does not necessarily terminate the loop.
import socket
import machine
led = machine.Pin(15, machine.Pin.OUT)
led.value(0)
HTML = """<!doctype html>
<html><head><meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Pico W Control</title></head><body>
<h1>Pico W Control</h1>
<p><a href='/led?state=on'>Turn LED ON</a></p>
<p><a href='/led?state=off'>Turn LED OFF</a></p>
<p><a href='/sensor'>Read sensor</a></p>
</body></html>"""
addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(2)
s.settimeout(1)
print("Web server listening on", addr)
while True:
try:
cl, remote = s.accept()
except OSError:
continue
try:
cl.settimeout(3)
request = cl.recv(1024).decode("utf-8", "ignore")
first_line = request.split("\\r\\n", 1)[0]
parts = first_line.split()
path = parts[1] if len(parts) > 1 else "/"
if path.startswith("/led?state=on"):
led.value(1)
body = "LED is ON"
elif path.startswith("/led?state=off"):
led.value(0)
body = "LED is OFF"
elif path.startswith("/sensor"):
body = "Sensor reader is not configured yet. Add your sensor driver here."
else:
body = HTML
response = "HTTP/1.1 200 OK\\r\\nContent-Type: text/html; charset=utf-8\\r\\nConnection: close\\r\\nContent-Length: " + str(len(body.encode())) + "\\r\\n\\r\\n" + body
cl.send(response.encode())
except OSError as exc:
print("Client error:", exc)
finally:
cl.close()
Upload the file, run it, and note the IP address printed in the shell. On a phone or computer connected to the same LAN, open http://PICO_IP/ and replace PICO_IP with the board's address. Tap the links and confirm the LED changes state. This is plain HTTP and has no login; use it only on a trusted test network.
Understand the HTTP request and response
A browser sends a request line such as GET /led?state=on HTTP/1.1, followed by headers and a blank line. The script reads a limited number of bytes and extracts the path from the first line. It then chooses a response body and sends an HTTP status line, content type, connection header, content length, blank line, and body. HTTP header names and URL paths should be treated carefully; paths are case-sensitive in many server implementations.
This small parser is suitable for learning, not for untrusted traffic. It does not fully parse HTTP, authenticate users, handle POST bodies, or validate every possible request. If you add user input, avoid directly executing it or using it to select arbitrary files or commands. For more complex applications, use a maintained MicroPython-compatible web framework or a more complete parser and understand its memory cost.
Add a real sensor reading
The server's /sensor route currently returns a placeholder because sensor models use different electrical interfaces and libraries. Replace that route with a call to a function that reads your exact sensor. For example, if your sensor is an I²C device, initialize machine.I2C with the correct SDA and SCL pins, verify its address with a scan, and use the device's documented register map or compatible driver. For a one-wire digital sensor, use its dedicated protocol library. An analog sensor requires an ADC input and suitable scaling; never exceed the GPIO/ADC input range.
A safe pattern is to separate sensor access from request parsing:
def read_sensor_text():
# Replace this with a verified driver for your exact sensor.
# Return a short text value or raise an exception on failure.
return "No sensor driver configured"
Then call read_sensor_text() in the /sensor route and handle exceptions by returning a clear error message. Do not make up a reading when a sensor read fails. A displayed error is more honest than a stale or fabricated value. Keep slow sensor operations out of the main request path if they could cause the browser to wait too long.
Improve the page and browser experience
The starter page uses simple links because they work without JavaScript. You can replace them with buttons and a small script that fetches /led?state=on in the background, then updates a status label. Add a viewport meta tag for mobile layout, readable contrast, and clear feedback for each action. Avoid repeatedly refreshing the entire page if a short JSON response would do. If you return JSON, set Content-Type: application/json and ensure the body is valid JSON with escaped strings.
A useful next step is a /status route that returns the LED state and most recent sensor reading. Store the last successful reading with a timestamp and display how old it is. If the sensor fails, keep the previous value only if the page clearly labels it as stale. A small web interface is a good way to learn request routing, but the browser is not a substitute for hardware safety: do not use a page like this as the only control for machinery, heaters, mains voltage, or any system where an unauthorized request could cause harm.
Troubleshooting
The page will not load: check that the Pico W and browser are on the same network, use the exact IP printed by the board, and include http://. Confirm the server loop is still running and port 80 is not blocked by network isolation. The board has no IP: verify 2.4 GHz Wi-Fi, credentials, signal strength, and firmware. The LED never changes: check the LED polarity, resistor, chosen GPIO, and whether your board's built-in LED uses a different interface; this example controls an external LED on GP15. The browser shows a blank page: inspect the Thonny shell for exceptions and verify that the HTTP response includes a blank line between headers and body.
The server stops after a browser refresh: make sure each accepted socket is closed even when an exception occurs. The sensor route returns the placeholder: that is expected until you implement the correct sensor driver. A second client hangs: the simple server handles clients sequentially; a client that stalls may delay other requests until its timeout. Reduce blocking operations, use timeouts, and consider an asynchronous or framework-based server if concurrency matters. The address changes: configure a DHCP reservation or inspect the router's lease table.
Security and reliability checklist
This server has no authentication or encryption. Anyone who can reach it on the LAN may be able to toggle the LED, and ordinary HTTP traffic is not encrypted. Do not forward port 80 from your router to the board or expose the device directly to the internet. For a demonstration, use a trusted private network and disconnect it when finished. For remote access, prefer a properly secured gateway or VPN rather than a port-forwarded microcontroller server. If you add authentication, do not assume a password sent over plain HTTP is protected.
Handle Wi-Fi loss explicitly in a longer-running project: periodically check wlan.isconnected(), attempt reconnection with bounded delays, and report connection state. Avoid uncontrolled rapid retries. Keep request bodies and HTML responses small because the Pico W has limited memory. If the board runs out of memory, reduce buffers and page size, close sockets promptly, and avoid keeping unnecessary copies of strings. Do not use the web server as a safety-critical controller.
Extend the project and test systematically
Test in layers: first toggle the LED directly from the REPL, then connect Wi-Fi and print the IP, then serve a static page, then add one route at a time, and finally integrate the sensor. This sequence isolates hardware, network, HTTP, and driver faults. Record the board model, MicroPython version, GPIO mapping, and sensor module; keep credentials out of that record.
Once the baseline works, add JSON status responses, a sensor timestamp, a responsive page, and a status indicator for Wi-Fi connectivity. You can also publish the same sensor data through MQTT, letting the web page serve local manual control while Node-RED or Home Assistant handles telemetry. Do not assume the board can serve unlimited clients or store large histories. For durable logging, send measurements to a suitable server or database and make the Pico W responsible for acquisition and lightweight control.
The core lesson is to keep each layer explicit: GPIO handles electrical signals, MicroPython drivers handle the sensor, Wi-Fi provides network access, and HTTP connects the browser to the board. Verify each layer independently, document pin assignments, and treat an unauthenticated local web server as a lab exercise rather than a production security boundary.