You dont have javascript enabled! Please enable it!

Practical case: Drive WS2812B LEDs with Raspberry Pi Pico W

Practical case: Drive WS2812B LEDs with Raspberry Pi Pico W — hero

Objective and use case

What you’ll build: Control WS2812B NeoPixel strips in real-time using WebSocket on Raspberry Pi Pico W. This project enables advanced IoT enthusiasts to achieve low-latency LED control.

Why it matters / Use cases

  • Real-time color and brightness adjustments for LED displays in art installations.
  • Dynamic lighting effects for events or performances, allowing for synchronized visual experiences.
  • Integration into smart home systems for ambient lighting that responds to user interactions.
  • Remote control of LED strips for DIY projects, enhancing user engagement through mobile applications.

Expected outcome

  • Achieve less than 50ms latency in LED color changes over Wi-Fi.
  • Control up to 300 WS2812B LEDs with stable performance at 30 FPS.
  • Maintain consistent brightness levels across all LEDs with less than 5% variance.
  • Successfully handle multiple simultaneous WebSocket connections without performance degradation.

Audience: Advanced IoT enthusiasts; Level: Intermediate to Advanced

Architecture/flow: WebSocket server on Raspberry Pi Pico W communicates with WS2812B strip, controlled via JSON commands sent over Wi-Fi.

Advanced Practical: WebSocket NeoPixel IoT Control on Raspberry Pi Pico W + WS2812B Strip

This hands-on project builds a robust, low-latency WebSocket server on a Raspberry Pi Pico W that controls a WS2812B (NeoPixel) LED strip in real time. You’ll send JSON commands over Wi‑Fi to change colors, brightness, and animations. The code runs fully on the Pico W (MicroPython), while we use a Raspberry Pi computer running Raspberry Pi OS Bookworm 64‑bit as the development/flash host and as a validation client.

Focus: websocket-neopixel-iot-control
Device model: Raspberry Pi Pico W + WS2812B strip

The tutorial is designed for advanced users who want precise control, clean asynchronous design (uasyncio), and a protocol you can integrate into larger IoT systems.


Prerequisites

  • A Raspberry Pi computer running Raspberry Pi OS Bookworm 64‑bit (as your development host). A Pi 4/5 recommended.
  • Internet access on the Raspberry Pi (to download firmware and libraries).
  • Comfort with Python 3.11, virtual environments, MicroPython, and basic networking.
  • Familiarity with LED power considerations and safe wiring practices.

Family defaults adapted to this model:
– We use Raspberry Pi OS Bookworm 64‑bit on the host and Python 3.11 to build tooling, manage the Pico W over USB (mpremote), and run validation clients.
– MicroPython runs on the Pico W; all LED control and the WebSocket server are embedded on the microcontroller.
– We will demonstrate enabling system interfaces on the Raspberry Pi host (via raspi-config or config files) for completeness, even though the project itself does not require GPIO/I2C/SPI on the host.


Materials (exact model)

  • Raspberry Pi Pico W (RPI-PICO-W, RP2040 + CYW43439 Wi‑Fi).
  • WS2812B LED strip (5 V), any length from 30 to 300 LEDs; ensure power budget is adequate.
  • 5 V DC power supply sized for the strip:
  • Rule of thumb: each WS2812B can draw up to ~60 mA at full white (255,255,255).
  • Example: 60 LEDs → up to 3.6 A; use a 5 V 4 A (or higher) supply.
  • Micro‑USB cable (data capable) for Pico W.
  • Level shifting (recommended, but sometimes optional):
  • 74AHCT125, SN74HCT245, or a single‑channel logic level shifter to shift Pico’s 3.3 V data to ~5 V.
  • Alternatively, run the strip at 5 V and verify it accepts 3.3 V logic (many do, but not guaranteed).
  • 300–500 Ω series resistor in the data line (to protect the first LED).
  • 1000 µF electrolytic capacitor across strip 5 V and GND (surge protection).
  • Breadboard and jumpers (if prototyping).

Setup/Connection

Host system setup (Raspberry Pi OS Bookworm 64‑bit, Python 3.11)

  1. Update and install essential tools:
    sudo apt update
    sudo apt full-upgrade -y
    sudo apt install -y python3.11-venv wget git unzip

  2. Enable common interfaces (not strictly required for this project, but included per family defaults):

  3. Launch raspi-config:
    sudo raspi-config

    • System Options → Wireless LAN → set your country/SSID/password (optional; this is for the Raspberry Pi host, not the Pico).
    • Interface Options → enable SSH if you want remote access.
    • Interface Options → I2C/SPI/UART: you may leave disabled for this project; not required.
    • Finish and reboot if prompted.
  4. Alternatively, to edit the firmware config directly:
    sudo nano /boot/firmware/config.txt

    • You can ensure nothing conflicts with USB. No changes required specifically for the Pico W.
  5. Create and activate a Python virtual environment (for host-side tools and validation clients):
    python3 -m venv ~/venvs/pico-ws
    source ~/venvs/pico-ws/bin/activate
    pip install --upgrade pip
    pip install mpremote websockets

Note: The host will not control GPIO. We don’t need gpiozero/smbus2/spidev for this build.

Wiring the Raspberry Pi Pico W + WS2812B

  • Do NOT power the Pico W from the strip’s 5 V supply directly. Power the Pico via USB; power the strip with its own 5 V supply. Always share grounds.
  • Put a 1000 µF capacitor across strip 5 V and GND (observe polarity).
  • Insert a 300–500 Ω resistor in series with the data line to the strip.
  • If using a level shifter, place it between the Pico’s GPIO and the strip’s DIN.

We’ll use GPIO 2 (GP2) as the NeoPixel data pin. Connect as follows:

Pico W Pin WS2812B Connection Notes
GND GND Common ground is required.
GP2 DIN (Data In) Through 300–500 Ω series resistor; via level shifter recommended.
VBUS (5 V output from USB) DO NOT USE Power the strip from a dedicated 5 V supply.
5 V PSU + VCC Provide correct current capacity.
5 V PSU − GND Tie to Pico GND to share reference.

Electrical cautions:
– If the strip is powered but the Pico is not, avoid backfeeding through the data line. Keep a common ground but avoid connecting Pico’s 5 V pin.
– Ensure the first LED’s DIN sees a clean edge and a valid logic-high. A 74AHCT125 buffer is preferred for long runs or high brightness.


Full Code (MicroPython on Pico W)

We’ll deploy three files to the Pico W: secrets.py, wsproto.py (WebSocket handshake and frames), and main.py (Wi‑Fi setup, server, patterns).

File: secrets.py

Replace with your Wi‑Fi credentials and country code.

SSID = "YOUR_SSID"
PASSWORD = "YOUR_PASSWORD"
COUNTRY = "US"  # 2-letter country code (e.g., "US", "DE", "GB")
HOSTNAME = "pico-w-neopixel"

File: wsproto.py

Minimal WebSocket server primitives for MicroPython, handling handshake, text frames, ping/pong, and close.

# wsproto.py
import uasyncio as asyncio
import uhashlib as hashlib
import ubinascii as binascii
import ujson as json

GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

async def _read_exact(reader, n):
    buf = b""
    while len(buf) < n:
        chunk = await reader.read(n - len(buf))
        if not chunk:
            raise OSError("socket closed during read")
        buf += chunk
    return buf

async def handshake(reader, writer):
    # Read HTTP request headers until CRLFCRLF
    data = b""
    while b"\r\n\r\n" not in data:
        chunk = await reader.read(512)
        if not chunk:
            raise OSError("early disconnect")
        data += chunk

    header_text = data.split(b"\r\n\r\n", 1)[0]
    lines = header_text.split(b"\r\n")
    if not lines or not lines[0].startswith(b"GET "):
        raise OSError("invalid HTTP method")

    headers = {}
    for ln in lines[1:]:
        if b":" in ln:
            k, v = ln.split(b":", 1)
            headers[k.strip().lower()] = v.strip()

    if headers.get(b"upgrade", b"").lower() != b"websocket":
        raise OSError("no Upgrade: websocket")
    if b"sec-websocket-key" not in headers:
        raise OSError("missing Sec-WebSocket-Key")

    key = headers[b"sec-websocket-key"]
    sha = hashlib.sha1(key + GUID).digest()
    accept = binascii.b2a_base64(sha).strip()

    resp = b"HTTP/1.1 101 Switching Protocols\r\n" \
           b"Upgrade: websocket\r\n" \
           b"Connection: Upgrade\r\n" \
           b"Sec-WebSocket-Accept: " + accept + b"\r\n\r\n"
    writer.write(resp)
    await writer.drain()

def _encode_frame(opcode, payload=b""):
    # Server->client frames are not masked
    fin_opcode = 0x80 | (opcode & 0x0F)
    length = len(payload)
    if length < 126:
        header = bytes([fin_opcode, length])
    elif length < (1 << 16):
        header = bytes([fin_opcode, 126, (length >> 8) & 0xFF, length & 0xFF])
    else:
        # 64-bit length
        header = bytes([fin_opcode, 127, 0, 0, 0, 0,
                        (length >> 24) & 0xFF, (length >> 16) & 0xFF,
                        (length >> 8) & 0xFF, length & 0xFF])
    return header + payload

async def send_text(writer, text):
    if isinstance(text, str):
        payload = text.encode()
    else:
        payload = text
    frame = _encode_frame(0x1, payload)  # text
    writer.write(frame)
    await writer.drain()

async def send_pong(writer, data=b""):
    frame = _encode_frame(0xA, data)  # pong
    writer.write(frame)
    await writer.drain()

async def send_close(writer, code=1000, reason=b""):
    payload = bytes([code >> 8, code & 0xFF]) + reason
    frame = _encode_frame(0x8, payload)
    writer.write(frame)
    await writer.drain()

async def recv_frame(reader):
    # Returns (opcode, payload_bytes) for a complete frame
    h = await _read_exact(reader, 2)
    b1, b2 = h[0], h[1]
    fin = (b1 & 0x80) != 0
    opcode = b1 & 0x0F
    masked = (b2 & 0x80) != 0
    length = b2 & 0x7F

    if length == 126:
        ext = await _read_exact(reader, 2)
        length = (ext[0] << 8) | ext[1]
    elif length == 127:
        ext = await _read_exact(reader, 8)
        length = 0
        for b in ext:
            length = (length << 8) | b

    mask = b""
    if masked:
        mask = await _read_exact(reader, 4)

    payload = await _read_exact(reader, length) if length else b""

    if masked:
        payload = bytes([payload[i] ^ mask[i % 4] for i in range(length)])

    return opcode, payload

def parse_json_payload(payload_bytes):
    try:
        s = payload_bytes.decode()
        return json.loads(s)
    except Exception:
        return None

File: main.py

Wi‑Fi, uasyncio server, LED driver with effects, and JSON command handling. Adjust LED_COUNT to your strip length.

# main.py
import uasyncio as asyncio
import network
import time
import machine
import neopixel
import ujson as json
import usocket
import rp2

from wsproto import handshake, recv_frame, send_text, send_pong, send_close, parse_json_payload
import secrets

# ---------- Configuration ----------
LED_PIN = 2            # GP2
LED_COUNT = 60         # Adjust to your strip length
FPS_DEFAULT = 40
BRIGHTNESS_DEFAULT = 0.5

# Wi-Fi power save off for lower latency
PM_OFF = 0xA11140

# ---------- LED/Color helpers ----------
class StripController:
    def __init__(self, pin, count):
        self.np = neopixel.NeoPixel(machine.Pin(pin, machine.Pin.OUT), count)
        self.count = count
        self.brightness = BRIGHTNESS_DEFAULT
        self.effect = "solid"
        self.color = (255, 0, 0)
        self.range = (0, count - 1)
        self.fps = FPS_DEFAULT
        self._tick = 0
        self._hue = 0
        self._chase_size = 5
        self._chase_gap = 3
        self._rainbow_speed = 2
        self._running = True

    def set_brightness(self, b):
        self.brightness = max(0.0, min(1.0, float(b)))

    def set_color_hex(self, hexstr):
        # Accept "#RRGGBB" or "RRGGBB"
        h = hexstr.strip().lstrip("#")
        if len(h) != 6:
            return False
        r = int(h[0:2], 16)
        g = int(h[2:4], 16)
        b = int(h[4:6], 16)
        self.color = (r, g, b)
        return True

    def set_range(self, start, end):
        start = max(0, min(self.count - 1, int(start)))
        end = max(0, min(self.count - 1, int(end)))
        if end < start:
            start, end = end, start
        self.range = (start, end)

    def clear(self):
        for i in range(self.count):
            self.np[i] = (0, 0, 0)
        self.np.write()

    def apply_solid(self):
        start, end = self.range
        r, g, b = self._apply_brightness(self.color)
        for i in range(self.count):
            if start <= i <= end:
                self.np[i] = (r, g, b)
            else:
                self.np[i] = (0, 0, 0)
        self.np.write()

    def _apply_brightness(self, rgb):
        return tuple(int(c * self.brightness) for c in rgb)

    def _hsv_to_rgb(self, h, s, v):
        # h: 0..360, s:0..1, v:0..1
        h = h % 360
        c = v * s
        x = c * (1 - abs((h / 60) % 2 - 1))
        m = v - c
        if 0 <= h < 60:
            r1, g1, b1 = c, x, 0
        elif 60 <= h < 120:
            r1, g1, b1 = x, c, 0
        elif 120 <= h < 180:
            r1, g1, b1 = 0, c, x
        elif 180 <= h < 240:
            r1, g1, b1 = 0, x, c
        elif 240 <= h < 300:
            r1, g1, b1 = x, 0, c
        else:
            r1, g1, b1 = c, 0, x
        return (int((r1 + m) * 255), int((g1 + m) * 255), int((b1 + m) * 255))

    def render_rainbow(self):
        start, end = self.range
        span = max(1, end - start + 1)
        hue_step = 360 / span
        # self._hue advances frame to frame
        for idx in range(self.count):
            if start <= idx <= end:
                hue = (self._hue + hue_step * (idx - start)) % 360
                r, g, b = self._hsv_to_rgb(hue, 1.0, 1.0)
                self.np[idx] = self._apply_brightness((r, g, b))
            else:
                self.np[idx] = (0, 0, 0)
        self.np.write()
        self._hue = (self._hue + self._rainbow_speed) % 360

    def render_chase(self):
        start, end = self.range
        r, g, b = self._apply_brightness(self.color)
        size = max(1, self._chase_size)
        gap = max(0, self._chase_gap)
        period = size + gap
        for i in range(self.count):
            if start <= i <= end:
                phase = (i + self._tick) % period
                self.np[i] = (r, g, b) if phase < size else (0, 0, 0)
            else:
                self.np[i] = (0, 0, 0)
        self.np.write()
        self._tick = (self._tick + 1) % period

    def render(self):
        if self.effect == "off":
            self.clear()
        elif self.effect == "solid":
            self.apply_solid()
        elif self.effect == "rainbow":
            self.render_rainbow()
        elif self.effect == "chase":
            self.render_chase()
        else:
            self.apply_solid()

    def apply_command(self, cmd):
        # cmd is a dict parsed from JSON
        op = cmd.get("op") or cmd.get("cmd")
        if op in ("off", "solid", "rainbow", "chase"):
            self.effect = op
        if "color" in cmd and isinstance(cmd["color"], str):
            self.set_color_hex(cmd["color"])
        if "brightness" in cmd:
            self.set_brightness(cmd["brightness"])
        if "range" in cmd and isinstance(cmd["range"], list) and len(cmd["range"]) == 2:
            self.set_range(cmd["range"][0], cmd["range"][1])
        if "fps" in cmd:
            self.fps = max(1, min(120, int(cmd["fps"])))
        if "size" in cmd:
            self._chase_size = int(cmd["size"])
        if "gap" in cmd:
            self._chase_gap = int(cmd["gap"])
        if "speed" in cmd:
            self._rainbow_speed = int(cmd["speed"])

# ---------- Wi-Fi ----------
async def wifi_connect():
    rp2.country(secrets.COUNTRY)
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    wlan.config(pm=PM_OFF)
    try:
        wlan.config(hostname=secrets.HOSTNAME)
    except Exception:
        pass
    if not wlan.isconnected():
        wlan.connect(secrets.SSID, secrets.PASSWORD)
        t0 = time.ticks_ms()
        while not wlan.isconnected():
            await asyncio.sleep_ms(200)
            if time.ticks_diff(time.ticks_ms(), t0) > 20000:
                raise RuntimeError("Wi-Fi connect timeout")
    print("Wi-Fi connected:", wlan.ifconfig())
    return wlan

# ---------- WebSocket server ----------
class WSServer:
    def __init__(self, strip: StripController):
        self.strip = strip
        self._client_task = None

    async def _client(self, reader, writer):
        try:
            await handshake(reader, writer)
            await send_text(writer, json.dumps({"status": "ok", "msg": "connected"}))
            while True:
                opcode, payload = await recv_frame(reader)
                # Opcodes: 0x1 text, 0x8 close, 0x9 ping, 0xA pong
                if opcode == 0x9:  # ping
                    await send_pong(writer, payload)
                    continue
                if opcode == 0x8:  # close
                    await send_close(writer, 1000, b"bye")
                    break
                if opcode == 0x1:  # text
                    cmd = parse_json_payload(payload)
                    if not cmd:
                        await send_text(writer, json.dumps({"status": "error", "msg": "bad json"}))
                        continue
                    self.strip.apply_command(cmd)
                    ack = {"status": "ok", "effect": self.strip.effect,
                           "brightness": self.strip.brightness, "fps": self.strip.fps}
                    await send_text(writer, json.dumps(ack))
        except Exception as e:
            # Log or ignore; typical on disconnects
            pass
        finally:
            try:
                await send_close(writer, 1000, b"closed")
            except Exception:
                pass
            await writer.wait_closed()

    async def run(self, host="0.0.0.0", port=8765):
        async def _handler(reader, writer):
            await self._client(reader, writer)

        srv = await asyncio.start_server(_handler, host, port, backlog=2)
        print("WebSocket server listening on ws://{}:{}".format(host, port))
        async with srv:
            await srv.serve_forever()

# ---------- Animation loop ----------
async def animator(strip: StripController):
    while True:
        strip.render()
        await asyncio.sleep_ms(int(1000 / max(1, strip.fps)))

# ---------- Entry ----------
async def main():
    wlan = await wifi_connect()
    strip = StripController(LED_PIN, LED_COUNT)
    strip.clear()
    ws = WSServer(strip)
    anim_task = asyncio.create_task(animator(strip))
    srv_task = asyncio.create_task(ws.run())
    await asyncio.gather(anim_task, srv_task)

try:
    asyncio.run(main())
finally:
    asyncio.new_event_loop()

Notes:
– The server expects JSON messages with keys like op, color, brightness, range, fps, etc. Example messages:
– {«op»:»solid»,»color»:»#00FF99″,»brightness»:0.6}
– {«op»:»off»}
– {«op»:»rainbow»,»speed»:3,»fps»:50,»brightness»:0.4}
– {«op»:»chase»,»color»:»#FF0000″,»size»:4,»gap»:2,»fps»:40,»range»:[0,59]}


Build/Flash/Run Commands (Host on Raspberry Pi OS Bookworm)

  1. Activate the virtual environment:
    source ~/venvs/pico-ws/bin/activate

  2. Download MicroPython firmware for Raspberry Pi Pico W (stable). As of this writing:

  3. MicroPython v1.22.2 (rp2-pico-w):
    mkdir -p ~/pico-w-firmware && cd ~/pico-w-firmware
    wget https://micropython.org/resources/firmware/RPI_PICO_W-20240222-v1.22.2.uf2 -O micropython-picow.uf2

  4. Put the Pico W in BOOTSEL mode:

  5. Hold BOOTSEL button while plugging in the USB cable to the Raspberry Pi.
  6. A mass storage device RPI-RP2 appears.

  7. Flash MicroPython UF2:
    cp micropython-picow.uf2 /media/$USER/RPI-RP2/
    The Pico W will reboot into MicroPython and expose a serial device (e.g., /dev/ttyACM0).

  8. Verify the serial device:
    dmesg | tail -n 20
    ls /dev/ttyACM*

  9. Create a project folder on the host and put your MicroPython files:
    mkdir -p ~/pico-ws/project && cd ~/pico-ws/project
    nano secrets.py
    nano wsproto.py
    nano main.py

    Paste the contents from the Full Code section (ensure your SSID/PASSWORD/COUNTRY are set in secrets.py).

  10. Use mpremote to copy files to the Pico W:
    mpremote connect list
    # Example output: /dev/ttyACM0 ...
    mpremote connect /dev/ttyACM0 fs cp secrets.py :secrets.py
    mpremote connect /dev/ttyACM0 fs cp wsproto.py :wsproto.py
    mpremote connect /dev/ttyACM0 fs cp main.py :main.py

Optionally, soft reboot to start main.py:
mpremote connect /dev/ttyACM0 soft-reset

  1. View console logs (optional):
    mpremote connect /dev/ttyACM0 repl
    You should see “Wi‑Fi connected: (‘IP’, …)” and “WebSocket server listening …”.

Validation (Step‑by‑Step)

We’ll validate with a Python 3.11 WebSocket client running on the Raspberry Pi host.

  1. Determine the Pico W IP:
  2. From REPL logs (mpremote repl) you’ll see something like:
    • Wi‑Fi connected: (‘192.168.1.88’, ‘255.255.255.0’, ‘192.168.1.1’, ‘8.8.8.8’)
  3. Or check your DHCP server/router.

  4. Create a test client on the host:
    cd ~/pico-ws/project
    nano ws_client.py

    Paste:

«`python
# ws_client.py
import asyncio, websockets, json, sys

async def main():
if len(sys.argv) < 2:
print(«Usage: python ws_client.py ws://:8765″)
return
uri = sys.argv[1]
async with websockets.connect(uri) as ws:
hello = await ws.recv()
print(«Server:», hello)

       # Solid teal
       cmd = {"op": "solid", "color": "#00CCAA", "brightness": 0.6}
       await ws.send(json.dumps(cmd))
       print("ACK:", await ws.recv())

       # Rainbow
       cmd = {"op": "rainbow", "speed": 3, "fps": 50, "brightness": 0.4}
       await ws.send(json.dumps(cmd))
       print("ACK:", await ws.recv())

       # Chase red
       cmd = {"op": "chase", "color": "#FF0000", "size": 4, "gap": 2, "fps": 40}
       await ws.send(json.dumps(cmd))
       print("ACK:", await ws.recv())

       # Range-limited solid
       cmd = {"op": "solid", "color": "#3344FF", "range": [10, 25], "brightness": 0.8}
       await ws.send(json.dumps(cmd))
       print("ACK:", await ws.recv())

       # Off
       cmd = {"op": "off"}
       await ws.send(json.dumps(cmd))
       print("ACK:", await ws.recv())

if name == «main«:
asyncio.run(main())
«`

  1. Run the client:
    source ~/venvs/pico-ws/bin/activate
    python ws_client.py ws://192.168.1.88:8765
  2. Replace 192.168.1.88 with your Pico W IP.
  3. You should see:
    • Server: {«status»:»ok»,»msg»:»connected»}
    • ACKs for each command.
  4. Visually confirm the LED strip changes to teal, rainbow animation, red chase, blue range slice, then off.

  5. Try custom commands interactively:
    «`
    python -q

    import asyncio, websockets, json
    async def t():
    … ws = await websockets.connect(«ws://192.168.1.88:8765»)
    … print(await ws.recv())
    … await ws.send(json.dumps({«op»:»solid»,»color»:»#FF00FF»,»brightness»:0.3}))
    … print(await ws.recv())
    … await ws.close()

    asyncio.run(t())
    «`

  6. Latency/throughput sanity check:

  7. Increase fps to 60–100 in rainbow/chase and observe animation smoothness:
    python ws_client.py ws://192.168.1.88:8765
    Modify the “fps” field in the script and rerun to see responsiveness.

  8. Optional web browser validation (local file):

  9. Create an HTML test page (static file on your Raspberry Pi host):
    nano ~/pico-ws/project/index.html
    Paste:
    html
    <!doctype html>
    <meta charset="utf-8">
    <title>Pico W NeoPixel WS Test</title>
    <input id="ip" value="ws://192.168.1.88:8765" size="30">
    <button id="connect">Connect</button>
    <div id="status"></div>
    <button onclick='send({"op":"solid","color":"#00FFAA","brightness":0.6})'>Solid</button>
    <button onclick='send({"op":"rainbow","speed":3,"fps":50,"brightness":0.4})'>Rainbow</button>
    <button onclick='send({"op":"chase","color":"#FF0000","size":5,"gap":2,"fps":45})'>Chase</button>
    <button onclick='send({"op":"off"})'>Off</button>
    <script>
    let ws;
    document.getElementById("connect").onclick = () => {
    const u = document.getElementById("ip").value;
    ws = new WebSocket(u);
    ws.onopen = () => document.getElementById("status").innerText = "Connected";
    ws.onmessage = (ev) => console.log("RX:", ev.data);
    ws.onclose = () => document.getElementById("status").innerText = "Closed";
    ws.onerror = (e) => console.error("WS error", e);
    };
    function send(obj) {
    if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj));
    }
    </script>
  10. Open it in a browser on your Raspberry Pi and click Connect and buttons to test.

Troubleshooting

  • Wi‑Fi won’t connect:
  • Ensure secrets.py SSID/PASSWORD are correct and COUNTRY is a valid 2‑letter code.
  • Some APs require 2.4 GHz; Pico W does not support 5 GHz.
  • Move closer to the AP or disable power save: we set wlan.config(pm=0xA11140) already.

  • No LED response:

  • Verify GND shared between Pico W and the strip’s 5 V PSU.
  • Confirm you used the strip’s DIN, not DOUT.
  • Try a lower LED_COUNT in code to match your strip length.
  • Use a level shifter if the first LED fails to latch data; long data wires and 3.3 V logic can be marginal at 5 V power levels.
  • Always have the 300–500 Ω series resistor and the 1000 µF capacitor; they improve reliability and protect the first LED.

  • Flicker or random colors:

  • Inadequate power supply or voltage droop; use thicker wires, power injection for long strips (feed 5 V and GND at multiple points).
  • Increase brightness gradually; at 100% white many supplies sag.
  • Ensure data and ground run together to reduce noise coupling.

  • mpremote cannot connect:

  • Check the device path:
    ls /dev/ttyACM*
    mpremote connect /dev/ttyACM0 repl
  • If Thonny or another tool is connected, close it first.

  • WebSocket client fails to connect:

  • Verify the Pico W IP.
  • Ensure port 8765 is not blocked on your LAN.
  • Confirm that the Pico reports “WebSocket server listening …” in REPL.

  • JSON errors:

  • The server sends {«status»:»error»,»msg»:»bad json»} if parsing fails. Validate JSON formatting.

  • Performance tuning:

  • Reduce LED_COUNT or FPS if you need more CPU headroom for networking.
  • Keep message rate modest; the Pico can handle frequent updates but don’t flood with large frames.

Improvements

  • TLS (wss):
  • MicroPython supports ssl.wrap_socket, but a fully secure WebSocket server with TLS on a microcontroller requires certificate handling and more RAM. For production-grade security, consider placing a reverse proxy (nginx/Caddy) on your LAN and proxying wss to the Pico’s ws endpoint, or implement TLS on the Pico with a carefully tuned cert/key.

  • Persistent configuration:

  • Store default effect/brightness in a JSON file on the Pico filesystem and load at boot. Add a command to save current settings.

  • Input validation and schema:

  • Extend command parsing to validate ranges strictly and send detailed error messages with error codes.

  • mDNS:

  • Provide a friendly hostname discovery. MicroPython mDNS on Pico W is non-trivial; you can also run a tiny mDNS reflector or keep IP in a DHCP reservation.

  • OTA-like updates:

  • Use mpremote fs cp to push updates without re‑flashing firmware. Consider packaging code into a single file and versioning it in git.

  • More effects:

  • Theater chase, comet, twinkle, gradients over time, palettes, and segment groups.

  • Home Assistant integration:

  • Create an automation that uses a Python script or Node-RED to send WebSocket JSON commands.

  • Rate limiting:

  • Add a queue or time gate to avoid command storms overwhelming animations.

Reference JSON Commands

The WebSocket server on the Pico W accepts text frames containing JSON objects. Supported fields:

  • op or cmd: «off» | «solid» | «rainbow» | «chase»
  • color: «#RRGGBB» (for solid/chase)
  • brightness: 0.0–1.0
  • range: [start, end] indices inclusive
  • fps: 1–120
  • size: integer (for chase)
  • gap: integer (for chase)
  • speed: integer (for rainbow hue advance per frame)

Examples:
– {«op»:»off»}
– {«op»:»solid»,»color»:»#00FF99″,»brightness»:0.5}
– {«op»:»rainbow»,»speed»:4,»fps»:60,»brightness»:0.4}
– {«op»:»chase»,»color»:»#FF3300″,»size»:3,»gap»:2,»range»:[0,50]}


Safety and Power Notes

  • Current draw: Full-brightness white is worst case. Use adequate 5 V power and consider multiple injection points for long strips to avoid voltage drop.
  • Thermal: High brightness may heat the strip; ensure ventilation.
  • ESD: Handle the Pico W and strip with care, avoid hot-plugging the data line with power applied.

Final Checklist

  • Raspberry Pi host:
  • Raspberry Pi OS Bookworm 64‑bit installed and updated.
  • Python 3.11 venv created; mpremote and websockets installed.
  • Interfaces enabled via raspi-config as desired; not required for this project.

  • Hardware wiring:

  • Pico W GP2 → 300–500 Ω resistor → WS2812B DIN (via level shifter recommended).
  • Common ground: Pico GND ↔ strip GND ↔ PSU GND.
  • 1000 µF capacitor across strip 5 V and GND.
  • Dedicated 5 V supply sized for total LED count.

  • MicroPython on Pico W:

  • UF2 flashed: RPI_PICO_W-20240222-v1.22.2.uf2 (or a later stable release).
  • Files uploaded: secrets.py, wsproto.py, main.py.

  • Network:

  • secrets.py has correct SSID/PASSWORD/COUNTRY.
  • Pico W obtains an IP; server prints “WebSocket server listening …”.

  • Validation:

  • Python client connects to ws://:8765 and receives {«status»:»ok»,»msg»:»connected»}.
  • Solid, rainbow, chase, range, brightness, fps commands work as expected.
  • Strip responds smoothly without flicker under normal brightness.

By following the steps above, you’ve implemented a clean, low-latency WebSocket interface to a WS2812B LED strip driven by a Raspberry Pi Pico W, ready to integrate into a larger IoT control plane or UI.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What type of server is built on the Raspberry Pi Pico W in this project?




Question 2: Which programming language is used to run the code on the Pico W?




Question 3: What is the purpose of sending JSON commands over Wi-Fi?




Question 4: Which Raspberry Pi model is recommended as the development host?




Question 5: What is the voltage requirement for the WS2812B LED strip?




Question 6: What is the focus of the project described in the article?




Question 7: What is a prerequisite for this project regarding Python?




Question 8: What does the tutorial aim to provide for advanced users?




Question 9: What type of interfaces may be enabled on the Raspberry Pi host?




Question 10: What is the maximum number of LEDs recommended for the WS2812B LED strip in this project?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top