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

TermMeaning 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 addressAddress the router gives the Pico W, for example 192.168.1.42; you type it in the browser
SocketAn endpoint for network communication; the server listens on TCP port 80
HTTP requestText 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 - Wi-Fi credentials stored on the Pico W
# secrets.py - keep this file private
SSID = 'YourNetworkName'
PASSWORD = 'YourWiFiPassword'

Step 2: Connect and Serve Pages

main.py - Wi-Fi connection with timeout and a socket HTTP server with HTML and JSON routes
# 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_wifi activates 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, and handle returns a status, content type and body.
  • In PAGE, {state} and {temp:.1f} are filled by format. In the JSON string, doubled braces {{ and }} produce literal braces.
  • Connection: close and 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 checks wlan.isconnected() periodically and calls connect_wifi() again, or uses machine.reset() after repeated failures.
  • The default power-save mode can add latency to replies. Calling wlan.config(pm=0xa11140) after wlan.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] EADDRINUSE after 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.