Raspberry Pi Pico W Wi-Fi Web Server to Control an LED and Read Data
The Pico W adds an Infineon CYW43439 wireless chip to the Pico, giving it 2.4 GHz Wi-Fi. With a few lines of MicroPython it can join your home network or phone hotspot and serve web pages, so any browser on the same network becomes a remote control. In this tutorial you will connect to Wi-Fi robustly, then write a small HTTP server using plain sockets that turns the LED on and off, displays the chip temperature, and offers a JSON endpoint other programs can read.
Key Concepts
| Term | Meaning in this project |
|---|---|
| Station mode (STA_IF) | The Pico W joins an existing access point, like a phone does |
| Access point mode (AP_IF) | The Pico W creates its own network; not used here |
| IP address | Address the router gives the Pico W, for example 192.168.1.42; you type it in the browser |
| Socket | An endpoint for network communication; the server listens on TCP port 80 |
| HTTP request | Text the browser sends, starting with a line such as GET /on HTTP/1.1 |
The wireless chip supports only 2.4 GHz networks. If your router broadcasts a combined 2.4 and 5 GHz name, it usually works; a 5 GHz-only network will never be found. Setting the country code with rp2.country('IN') makes the radio use the channels permitted in India.
Step 1: Keep Credentials in a Separate File
Create a file named secrets.py on the Pico with your network name and password. Keeping them out of the main program means you can share or publish main.py without leaking your password.
# secrets.py - keep this file private
SSID = 'YourNetworkName'
PASSWORD = 'YourWiFiPassword'
Step 2: Connect and Serve Pages
# main.py - Pico W web server: LED control, temperature and JSON API
import network
import socket
import time
import rp2
from machine import Pin, ADC
import secrets
rp2.country('IN') # regulatory domain for the radio
led = Pin('LED', Pin.OUT)
temp_adc = ADC(4)
def chip_temp():
v = temp_adc.read_u16() * 3.3 / 65535
return 27 - (v - 0.706) / 0.001721
def connect_wifi(timeout_s=20):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(secrets.SSID, secrets.PASSWORD)
start = time.time()
while not wlan.isconnected():
if wlan.status() < 0: # negative status = failure
raise RuntimeError('Wi-Fi failed, status {}'.format(wlan.status()))
if time.time() - start > timeout_s:
raise RuntimeError('Wi-Fi timeout')
led.toggle() # blink while connecting
time.sleep(0.5)
led.off()
ip = wlan.ifconfig()[0]
print('Connected, open http://{}/'.format(ip))
return wlan
PAGE = """<!DOCTYPE html>
<html><head><meta name="viewport" content="width=device-width">
<title>Pico W</title></head>
<body style="font-family:sans-serif;text-align:center">
<h2>Pico W Control</h2>
<p>LED is <b>{state}</b></p>
<p><a href="/on"><button>ON</button></a>
<a href="/off"><button>OFF</button></a></p>
<p>Chip temperature: {temp:.1f} C</p>
</body></html>"""
def handle(path):
# Returns (status line, content type, body) for a request path
if path == '/on':
led.on()
elif path == '/off':
led.off()
elif path == '/api':
body = '{{"led": {}, "temp_c": {:.1f}}}'.format(led.value(), chip_temp())
return '200 OK', 'application/json', body
elif path not in ('/', '/favicon.ico'):
return '404 Not Found', 'text/plain', 'Not found'
state = 'ON' if led.value() else 'OFF'
return '200 OK', 'text/html', PAGE.format(state=state, temp=chip_temp())
wlan = connect_wifi()
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(addr)
server.listen(2)
while True:
client, remote = server.accept()
try:
client.settimeout(5)
request = client.recv(1024).decode()
# First line looks like: GET /on HTTP/1.1
parts = request.split(' ')
path = parts[1] if len(parts) > 1 else '/'
status, ctype, body = handle(path)
client.send('HTTP/1.0 {}\r\nContent-Type: {}\r\nConnection: close\r\n\r\n'.format(status, ctype))
client.send(body)
print(remote[0], path, status)
except OSError as e:
print('Client error:', e)
finally:
client.close()
How the Server Works
connect_wifiactivates the station interface, starts the connection and waits, blinking the LED. It raises an error on a wrong password (negative status) or a timeout, rather than hanging forever.wlan.ifconfig()[0]is the IP address assigned by your router. It is printed in Thonny; you can also find it in the router's connected-devices list.socket.getaddrinfo('0.0.0.0', 80)binds to all interfaces on port 80, the default HTTP port, so the browser URL needs no port number.server.accept()blocks until a browser connects. Each request is read, the path is taken from the first line, andhandlereturns a status, content type and body.- In
PAGE,{state}and{temp:.1f}are filled byformat. In the JSON string, doubled braces{{and}}produce literal braces. Connection: closeand closing the socket after each response keep the server simple: one request at a time, no keep-alive.
It helps to see what the browser actually sends. Add print(request) temporarily and you will see a block of text like the one below. Only the first line matters to this server; the headers that follow describe the browser and are ignored.
GET /on HTTP/1.1 Host: 192.168.1.42 User-Agent: Mozilla/5.0 ... Accept: text/html,... (blank line ends the headers)
Splitting that text on spaces gives ['GET', '/on', 'HTTP/1.1...'], so parts[1] is the path. The status codes Wi-Fi returns are also worth knowing: 3 means connected with an IP address, -1 a general failure, -2 no matching network found, and -3 an authentication failure (wrong password).
Save both files on the Pico, run main.py, and open the printed address in a phone or laptop browser on the same network. Visiting /api returns something like {"led": 1, "temp_c": 31.4}, which a Python script or a home-automation tool can poll.
Making It Reliable
- Reserve the Pico W's IP address in your router's DHCP settings (address reservation) so the URL never changes.
- If the Wi-Fi drops,
accept()keeps waiting. A production version checkswlan.isconnected()periodically and callsconnect_wifi()again, or usesmachine.reset()after repeated failures. - The default power-save mode can add latency to replies. Calling
wlan.config(pm=0xa11140)afterwlan.active(True)disables it, as shown in the official Pico W documentation, at the cost of higher current. - This server has no authentication. Use it only on a trusted local network; never forward port 80 from your router to it.
Troubleshooting
ImportError: no module named 'network': you flashed the plain Pico firmware. Install the Pico W build.- Status -3 or a timeout: wrong password or a 5 GHz-only network.
OSError: [Errno 98] EADDRINUSEafter restarting: the old socket is still open. Press Stop, then soft-reset with Ctrl+D before running again.- Page loads on the laptop but not the phone: the phone is on mobile data or a guest network isolated from the Pico W.
Summary
You connected the Pico W to Wi-Fi in station mode with a timeout, served an HTML control page and a JSON API from a socket server on port 80, and toggled the LED from any browser on the network. The next tutorial goes beyond the local network using MQTT, the lightweight publish-subscribe protocol widely used in IoT.