Objetivo y caso de uso
Qué construirás: Controlar tiras WS2812B NeoPixel en tiempo real utilizando WebSocket en Raspberry Pi Pico W.
Para qué sirve
- Iluminación dinámica en proyectos de arte interactivo.
- Control de efectos visuales en instalaciones de eventos.
- Desarrollo de prototipos de sistemas de iluminación para hogares inteligentes.
- Integración con sensores para respuestas visuales en tiempo real.
Resultado esperado
- Latencia de respuesta menor a 50 ms en el control de los LEDs.
- Capacidad de controlar hasta 300 LEDs WS2812B simultáneamente.
- Consumo de ancho de banda de aproximadamente 1.5 kbps por tira de LEDs.
- Estabilidad en la conexión WebSocket con menos del 1% de pérdida de paquetes.
Público objetivo: Entusiastas de IoT y desarrolladores; Nivel: Avanzado
Arquitectura/flujo: Raspberry Pi Pico W ejecutando MicroPython con servidor WebSocket, controlado desde un cliente en Python en un host.
Nivel: Avanzado
Prerrequisitos
Sistema operativo y entorno de desarrollo (host)
- Raspberry Pi OS Bookworm 64‑bit (kernel 6.1.x o superior)
- Python 3.11 (verificable con
python3 --version, p.ej. 3.11.2) - Entorno de red 2.4 GHz con acceso a Internet para instalación de paquetes y para conectar la Pico W
- Usuario con permisos sudo, con acceso a puertos serie/USB
Toolchain exacta y versiones
- Firmware de la placa:
- MicroPython para Raspberry Pi Pico W: micropython-1.21.0-pico-w.uf2
- Herramientas en el host (instaladas dentro de un entorno virtual):
- pip 23.2+ (gestión de paquetes)
- mpremote 1.22.0 (carga y control de ficheros en MicroPython)
- websockets 11.0.3 (cliente de prueba en el host para WebSocket)
- Opcionales (solo si prefieres IDE gráfico):
- Thonny 4.1.x (depuración/flash; Bookworm suele incluir Thonny 4.x)
Notas:
– No necesitamos gpiozero, smbus2 ni spidev en este caso concreto.
– El servidor WebSocket corre en la Pico W (MicroPython), y la validación incluye un cliente desde el host.
Habilitar interfaces en Raspberry Pi OS (host)
Aunque la Pico W se programa por USB (no necesita I2C/SPI/Serial del host), asegúrate de:
– Configurar país de Wi‑Fi para cumplir normativa local (útil si el host se conecta por Wi‑Fi).
– Habilitar acceso serial del usuario al dispositivo USB CDC (pertenecer al grupo dialout).
Pasos (en la Raspberry Pi con Raspberry Pi OS):
1. Ajustar país/región Wi‑Fi (si procede):
– sudo raspi-config
– Localisation Options → WLAN Country → elige tu país
2. Añadir el usuario al grupo dialout para acceso a /dev/ttyACM0:
– sudo usermod -aG dialout $USER
– Cierra sesión y vuelve a entrar (o reinicia) para que surta efecto.
3. (Opcional) Habilitar SSH si quieres administrar el host de forma remota:
– sudo raspi-config
– Interface Options → SSH → Enable
Materiales
- 1× Raspberry Pi Pico W (RP2040 con Wi‑Fi)
- 1× Tira LED WS2812B (Neopixel). Para el caso didáctico se recomienda una sección de 8–30 LEDs.
- Resistencias y capacitores recomendados:
- 1× resistencia 330 Ω en serie con la línea de datos (DIN)
- 1× condensador electrolítico 1000 µF/6.3 V o superior entre +5 V y GND de la tira (para evitar picos)
- 1× Fuente de alimentación 5 V regulada, capaz de suministrar al menos 60 mA por LED a brillo máximo (pico)
- Para 16 LEDs, presupuesto ≈ 16 × 60 mA = 960 mA (pico). Para uso seguro en pruebas, limita brillo.
- 1× Convertidor de nivel recomendado (lógico 3.3 V → 5 V), por ejemplo SN74AHCT125 o 74HCT245
- Nota: muchas tiras WS2812B funcionan con señal de 3.3 V si VCC está cerca de 5 V y el entorno es limpio, pero para máxima fiabilidad y entornos industriales se recomienda nivelado lógico.
- Protoboard y cables Dupont
- 1× Cable micro‑USB para conectar la Pico W al host
- 1× Raspberry Pi (4/400/5) o equipo x86_64 con Raspberry Pi OS Bookworm 64‑bit
Observación de coherencia de modelo: Este caso práctico usa exactamente “Raspberry Pi Pico W + WS2812B strip” para el control de LEDs mediante WebSocket en la Pico W (IoT), cumpliendo el objetivo “websocket‑neopixel‑iot‑control”.
Preparación y conexión
Conexiones eléctricas recomendadas
- Elige GP2 (pin físico 4 de la Pico W) como línea de datos para la WS2812B.
- Alimenta la tira con 5 V y GND. Si tu tira tiene conector, respeta la dirección “DIN”.
- Coloca el condensador de 1000 µF entre +5 V y GND cerca de la tira.
- Inserta la resistencia de 330 Ω en serie en la línea de datos entre GP2 y DIN.
- Si usas convertidor de nivel, colócalo entre la Pico (3.3 V) y la tira (5 V) para la línea DIN.
Tabla de mapeo de pines y alimentación:
| Elemento | Pico W (señal) | Pin Pico W (físico) | Tira WS2812B | Notas |
|---|---|---|---|---|
| Datos (DIN) | GP2 | 4 | DIN | En serie 330 Ω; ideal con nivelador 3.3→5 V |
| Alimentación +5 V de tira | VBUS (5 V) | 40 | +5 V | VBUS alimenta desde USB; suficiente para pocas decenas de LEDs a bajo brillo. Para más, usa fuente 5 V externa compartiendo GND |
| Tierra común | GND | 38 (u otro GND) | GND | GND común Pico–tira–fuente |
| Señal de referencia (opcional) | — | — | — | Mantén cables cortos; evita interferencias |
Advertencia de potencia:
– Si alimentas desde el puerto USB de la Raspberry Pi host, el límite puede rondar 1 A compartido. En producción, usa fuente 5 V externa para la tira y alimenta la Pico por su puerto USB (GND común).
– Controla el brillo por software para no exceder la corriente del suministro.
Preparación del entorno Python en el host (Raspberry Pi OS Bookworm 64‑bit)
1) Actualiza e instala paquetes base:
sudo apt update
sudo apt full-upgrade -y
sudo apt install -y python3-venv python3-pip wget unzip screen
2) Crea y activa un entorno virtual (venv) específico:
python3 -m venv ~/venvs/pico-w-ws
source ~/venvs/pico-w-ws/bin/activate
python -m pip install --upgrade pip
3) Instala herramientas exactas:
pip install mpremote==1.22.0 websockets==11.0.3
4) Verifica versiones:
python --version
pip --version
python -c "import mpremote, websockets; print('mpremote', mpremote.__version__)"
python -c "import websockets; print('websockets', websockets.__version__)"
Flasheo del firmware MicroPython (1.21.0) en la Pico W
1) Descarga el UF2 de MicroPython para Pico W:
mkdir -p ~/pico-firmware
cd ~/pico-firmware
wget https://micropython.org/resources/firmware/micropython-1.21.0-pico-w.uf2 -O micropython-1.21.0-pico-w.uf2
2) Pon la Pico W en modo BOOTSEL:
– Mantén pulsado el botón BOOTSEL de la Pico W.
– Conéctala al host por USB.
– Suelta BOOTSEL; debe montarse como unidad USB (RPI-RP2).
3) Copia el UF2 a la unidad RPI-RP2:
– Opción A (gestor de archivos): arrastra y suelta el UF2.
– Opción B (línea de comandos; ajusta la ruta de montaje si difiere):
cp micropython-1.21.0-pico-w.uf2 /media/$USER/RPI-RP2/
sync
La Pico W se reiniciará y expondrá un puerto serie USB (p. ej., /dev/ttyACM0). Ya está lista para recibir scripts MicroPython.
Código completo
En este caso, la Raspberry Pi Pico W actúa como servidor HTTP+WebSocket minimalista para controlar una tira WS2812B (Neopixel) en tiempo real desde un navegador o un cliente de prueba. El servidor implementa:
– Conexión Wi‑Fi
– Servido de una página HTML muy simple
– Handshake RFC6455 y manejo de frames WebSocket (texto) sin librerías externas
– Cola/estado de efectos: sólido, apagado, arcoíris, “chase”
– Control de brillo global
Arquitectura de archivos en la Pico W:
– main.py (servidor y control de LEDs)
– index.html (cliente web con controles)
Ajusta SSID y contraseña Wi‑Fi en main.py antes de desplegar.
Archivo: main.py (MicroPython, servidor WebSocket y control de neopixel)
# main.py - MicroPython 1.21.0 para Raspberry Pi Pico W
# Objetivo: websocket-neopixel-iot-control (servidor HTTP+WS minimalista)
import network, socket, time, uasyncio as asyncio
import machine
import json
import ubinascii
import hashlib
from neopixel import NeoPixel
# ======== CONFIGURACIÓN ========
WIFI_SSID = "TU_SSID"
WIFI_PASS = "TU_PASSWORD"
HOST = "0.0.0.0"
PORT = 80
LED_PIN = 2 # GP2 (pin físico 4)
LED_COUNT = 16 # Ajusta al número de LEDs de tu tira
BRIGHTNESS = 0.2 # 0.0..1.0
# ======== LEDS ========
np = NeoPixel(machine.Pin(LED_PIN, machine.Pin.OUT), LED_COUNT)
def apply_brightness(color, brightness):
r, g, b = color
return (int(r * brightness), int(g * brightness), int(b * brightness))
def fill_color(color, brightness=None):
if brightness is None:
brightness = state["brightness"]
c = apply_brightness(color, brightness)
for i in range(LED_COUNT):
np[i] = c
np.write()
def wheel(pos):
# Arcoíris de 0..255
if pos < 0 or pos > 255:
return (0, 0, 0)
if pos < 85:
return (255 - pos * 3, pos * 3, 0)
if pos < 170:
pos -= 85
return (0, 255 - pos * 3, pos * 3)
pos -= 170
return (pos * 3, 0, 255 - pos * 3)
# ======== ESTADO GLOBAL ========
state = {
"effect": "off", # "off", "solid", "rainbow", "chase"
"color": (255, 0, 0), # usado en "solid"
"brightness": BRIGHTNESS,
"clients": 0
}
# ======== WIFI ========
def wifi_connect(ssid, password, timeout_s=20):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.config(pm=0xa11140) # modo power-save desactivado
wlan.connect(ssid, password)
t0 = time.ticks_ms()
while not wlan.isconnected():
if time.ticks_diff(time.ticks_ms(), t0) > timeout_s * 1000:
raise OSError("Wi-Fi timeout")
time.sleep_ms(200)
return wlan
# ======== HTTP RESPUESTAS ========
HTTP_200 = "HTTP/1.1 200 OK\r\n"
HTTP_400 = "HTTP/1.1 400 Bad Request\r\n\r\nBad Request"
HTTP_404 = "HTTP/1.1 404 Not Found\r\n\r\nNot Found"
HTTP_HEADERS_HTML = "Content-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n"
HTTP_HEADERS_CSS = "Content-Type: text/css; charset=utf-8\r\nConnection: close\r\n\r\n"
# Cargamos index.html desde el sistema de archivos
def load_index_html():
try:
with open("index.html", "r") as f:
return f.read()
except:
# Página mínima de contingencia
return """<!doctype html><html><body>
<h1>Falta index.html</h1>
<p>Sube el archivo index.html a la Pico W.</p>
</body></html>"""
# ======== WEBSOCKET (mínimo viable RFC6455) ========
WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
def ws_handshake(client, headers):
# Extrae Sec-WebSocket-Key y responde con Sec-WebSocket-Accept
key = None
for h in headers:
if h.lower().startswith("sec-websocket-key:"):
key = h.split(":", 1)[1].strip()
break
if not key:
return False
sha1 = hashlib.sha1((key + WS_GUID).encode()).digest()
accept = ubinascii.b2a_base64(sha1).strip().decode()
resp = (
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
f"Sec-WebSocket-Accept: {accept}\r\n\r\n"
)
client.send(resp.encode())
return True
def ws_recv_frame(client):
# Devuelve (opcode, data_bytes) de un frame WS
# Implementación básica: soporta payloads < 126, sin extensiones
hdr = client.recv(2)
if not hdr or len(hdr) < 2:
return None, None
b1, b2 = hdr[0], hdr[1]
fin = b1 & 0x80
opcode = b1 & 0x0F
masked = b2 & 0x80
length = b2 & 0x7F
if length == 126:
ext = client.recv(2)
length = (ext[0] << 8) | ext[1]
elif length == 127:
ext = client.recv(8)
# Tomamos los últimos 4 para longitudes pequeñas; en este caso evitamos longitudes enormes
length = 0
for b in ext[-4:]:
length = (length << 8) | b
mask = b""
if masked:
mask = client.recv(4)
payload = b""
to_read = length
while to_read > 0:
chunk = client.recv(to_read)
if not chunk:
break
payload += chunk
to_read -= len(chunk)
if masked and payload:
payload = bytes([payload[i] ^ mask[i % 4] for i in range(len(payload))])
return opcode, payload
def ws_send_text(client, text):
data = text.encode()
header = bytearray()
header.append(0x81) # FIN + opcode=1 (text)
l = len(data)
if l < 126:
header.append(l)
elif l < (1 << 16):
header.append(126)
header.append((l >> 8) & 0xFF)
header.append(l & 0xFF)
else:
header.append(127)
for shift in (56, 48, 40, 32, 24, 16, 8, 0):
header.append((l >> shift) & 0xFF)
client.send(header + data)
def handle_command(cmd):
# cmd es dict decodificado de JSON
ctype = cmd.get("action")
if ctype == "solid":
r = int(cmd.get("r", 255))
g = int(cmd.get("g", 0))
b = int(cmd.get("b", 0))
state["color"] = (r, g, b)
state["effect"] = "solid"
elif ctype == "off":
state["effect"] = "off"
elif ctype == "rainbow":
state["effect"] = "rainbow"
elif ctype == "chase":
state["effect"] = "chase"
elif ctype == "brightness":
br = float(cmd.get("value", state["brightness"]))
br = max(0.0, min(1.0, br))
state["brightness"] = br
# Respuesta del estado actual
return {
"ok": True,
"state": {
"effect": state["effect"],
"color": state["color"],
"brightness": state["brightness"]
}
}
async def effect_loop():
pos = 0
chase_idx = 0
while True:
eff = state["effect"]
if eff == "off":
fill_color((0, 0, 0))
await asyncio.sleep_ms(60)
elif eff == "solid":
fill_color(state["color"])
await asyncio.sleep_ms(60)
elif eff == "rainbow":
for i in range(LED_COUNT):
np[i] = apply_brightness(wheel((i + pos) & 255), state["brightness"])
np.write()
pos = (pos + 2) % 256
await asyncio.sleep_ms(30)
elif eff == "chase":
# fondo oscuro
dim = int(10 * state["brightness"])
for i in range(LED_COUNT):
np[i] = (dim, dim, dim)
# píxel en carrera en color base
c = apply_brightness(state["color"], state["brightness"])
np[chase_idx % LED_COUNT] = c
np.write()
chase_idx = (chase_idx + 1) % LED_COUNT
await asyncio.sleep_ms(80)
else:
await asyncio.sleep_ms(100)
async def http_ws_server():
addr = socket.getaddrinfo(HOST, PORT)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(2)
print("Servidor en http://{}:{}".format(wlan.ifconfig()[0], PORT))
while True:
client, remote = s.accept()
client.settimeout(5)
try:
req = b""
# Lee hasta cabeceras completas
while b"\r\n\r\n" not in req:
chunk = client.recv(1024)
if not chunk:
break
req += chunk
if not req:
client.close()
continue
header_text = req.decode(errors="ignore")
lines = header_text.split("\r\n")
request_line = lines[0]
headers = lines[1:]
method, path, _ = request_line.split(" ", 2)
if path == "/":
body = load_index_html()
resp = HTTP_200 + "Content-Length: {}\r\n".format(len(body)) + HTTP_HEADERS_HTML + body
client.send(resp.encode())
client.close()
continue
if path == "/ws" and method == "GET":
# WebSocket upgrade
ok = ws_handshake(client, headers)
if not ok:
client.send(HTTP_400.encode())
client.close()
continue
state["clients"] += 1
try:
ws_send_text(client, json.dumps({
"hello": "pico-w",
"led_count": LED_COUNT,
"state": {
"effect": state["effect"],
"brightness": state["brightness"]
}
}))
while True:
opcode, payload = ws_recv_frame(client)
if opcode is None:
break
if opcode == 8: # close
break
if opcode == 1: # text
try:
cmd = json.loads(payload.decode())
resp = handle_command(cmd)
ws_send_text(client, json.dumps(resp))
except Exception as e:
ws_send_text(client, json.dumps({"ok": False, "error": str(e)}))
# Ignora binarios y pings en este mínimo
finally:
state["clients"] = max(0, state["clients"] - 1)
try:
client.close()
except:
pass
continue
# Rutas no encontradas
client.send(HTTP_404.encode())
client.close()
except Exception as e:
try:
client.send(HTTP_400.encode())
client.close()
except:
pass
# ======== MAIN ========
print("Conectando Wi-Fi...")
wlan = wifi_connect(WIFI_SSID, WIFI_PASS)
print("Wi-Fi OK:", wlan.ifconfig())
# Arranque seguro: LEDs en off
state["effect"] = "off"
fill_color((0, 0, 0), brightness=0.0)
loop = asyncio.get_event_loop()
loop.create_task(effect_loop())
loop.create_task(http_ws_server())
loop.run_forever()
Explicación breve de partes clave:
– Conexión Wi‑Fi: wifi_connect() prepara la interfaz STA, desactiva ahorro de energía y espera conexión.
– Servidor HTTP: Responde en “/” con index.html y hace upgrade a WebSocket en “/ws”.
– Handshake WebSocket: ws_handshake() implementa el cálculo de Sec-WebSocket-Accept conforme a RFC6455.
– Frames WebSocket: ws_recv_frame() y ws_send_text() manejan mensajes de texto con longitudes pequeñas y enmascarado del cliente.
– Motor de efectos: effect_loop() corre con uasyncio e interpreta el estado global para pintar la tira.
– Comandos JSON soportados: {"action":"solid","r":255,"g":0,"b":0}, {"action":"off"}, {"action":"rainbow"}, {"action":"chase"}, {"action":"brightness","value":0.4}.
Archivo: index.html (cliente web básico con WebSocket)
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Pico W • WebSocket Neopixel IoT Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: system-ui, sans-serif; margin: 1rem; }
fieldset { margin-bottom: 1rem; }
label { display: inline-block; width: 8rem; }
#status { padding: 0.5rem; border: 1px solid #ccc; border-radius: 6px; margin-bottom: 1rem; }
.btn { padding: 0.4rem 0.8rem; margin-right: 0.5rem; }
</style>
</head>
<body>
<h1>Pico W → WS2812B por WebSocket</h1>
<div id="status">Desconectado</div>
<fieldset>
<legend>Conexión</legend>
<button id="btnConnect" class="btn">Conectar</button>
<button id="btnDisconnect" class="btn">Cerrar</button>
</fieldset>
<fieldset>
<legend>Color sólido</legend>
<label for="color">Color:</label>
<input id="color" type="color" value="#ff0000">
<button id="btnSolid" class="btn">Aplicar</button>
<button id="btnOff" class="btn">Apagar</button>
</fieldset>
<fieldset>
<legend>Efectos</legend>
<button id="btnRainbow" class="btn">Arcoíris</button>
<button id="btnChase" class="btn">Chase</button>
</fieldset>
<fieldset>
<legend>Brillo</legend>
<input id="brightness" type="range" min="0" max="100" value="20">
<span id="bval">20%</span>
<button id="btnBr" class="btn">Fijar brillo</button>
</fieldset>
<pre id="log"></pre>
<script>
let ws = null;
function log(msg) {
const el = document.getElementById('log');
el.textContent += msg + "\n";
el.scrollTop = el.scrollHeight;
}
function setStatus(text, ok) {
const s = document.getElementById('status');
s.textContent = text;
s.style.background = ok ? "#e6ffed" : "#ffecec";
s.style.borderColor = ok ? "#34c759" : "#ff3b30";
}
function rgbHexToObj(hex) {
// "#rrggbb" → {r,g,b}
const m = /^#?([0-9a-f]{6})$/i.exec(hex);
const n = parseInt(m[1], 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
function connect() {
if (ws && ws.readyState === WebSocket.OPEN) return;
const proto = location.protocol === "https:" ? "wss://" : "ws://";
const url = proto + location.host + "/ws";
log("Conectando a " + url + " ...");
ws = new WebSocket(url);
ws.onopen = () => { setStatus("Conectado", true); log("WS abierto"); };
ws.onclose = () => { setStatus("Desconectado", false); log("WS cerrado"); };
ws.onerror = (e) => { setStatus("Error WS", false); log("WS error: " + e); };
ws.onmessage = (ev) => { log("← " + ev.data); };
}
function disconnect() {
if (ws) {
ws.close();
}
}
function send(obj) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
log("No conectado");
return;
}
const txt = JSON.stringify(obj);
ws.send(txt);
log("→ " + txt);
}
document.getElementById('btnConnect').onclick = connect;
document.getElementById('btnDisconnect').onclick = disconnect;
document.getElementById('btnSolid').onclick = () => {
const {r,g,b} = rgbHexToObj(document.getElementById('color').value);
send({ action: "solid", r, g, b });
};
document.getElementById('btnOff').onclick = () => send({ action: "off" });
document.getElementById('btnRainbow').onclick = () => send({ action: "rainbow" });
document.getElementById('btnChase').onclick = () => send({ action: "chase" });
document.getElementById('brightness').oninput = (e) => {
document.getElementById('bval').textContent = e.target.value + "%";
};
document.getElementById('btnBr').onclick = () => {
const v = parseInt(document.getElementById('brightness').value, 10) / 100.0;
send({ action: "brightness", value: v });
};
// Intento autoconectar tras cargar la página
setTimeout(connect, 500);
</script>
</body>
</html>
Compilación/flash/ejecución
En MicroPython no hay “compilación” al uso para este caso; subiremos los archivos a la Pico W con mpremote.
1) Asegúrate de haber flasheado MicroPython 1.21.0 (ver sección anterior).
2) Conecta la Pico W por USB (modo normal) y verifica el dispositivo:
ls /dev/ttyACM*
Debe aparecer /dev/ttyACM0 (o similar).
3) Crea una carpeta de proyecto en el host y coloca los archivos:
mkdir -p ~/pico-w-websocket
cd ~/pico-w-websocket
# Crea main.py e index.html (copia y pega los contenidos anteriores en estos ficheros)
nano main.py
nano index.html
Edita en main.py tus credenciales Wi‑Fi: WIFI_SSID y WIFI_PASS.
4) Activa el entorno virtual e instala (si no lo hiciste antes):
source ~/venvs/pico-w-ws/bin/activate
pip install mpremote==1.22.0
5) Sube los archivos a la Pico W:
mpremote connect list
# Si ves "ttyACM0" u otro, conecta:
mpremote connect /dev/ttyACM0 fs cp main.py :
mpremote connect /dev/ttyACM0 fs cp index.html :
El “:” indica el directorio raíz del sistema de archivos de MicroPython en la Pico.
6) Reinicia la Pico W para ejecutar main.py al arranque:
mpremote connect /dev/ttyACM0 reset
7) Observa logs por REPL (opcional para diagnosis):
mpremote connect /dev/ttyACM0 repl
# Para salir del REPL: Ctrl-]
Debes ver mensajes “Conectando Wi‑Fi…” y “Wi‑Fi OK: (‘IP’, ‘MASK’, …)” y “Servidor en http://IP:80”.
8) Prueba desde un navegador en la misma red:
– Visita: http://IP_DE_LA_PICO/
– Ajusta color, brillo y efectos; la tira debe responder en tiempo real.
9) Cliente de prueba (host, Python 3.11 con websockets==11.0.3):
Crea un cliente mínimo para enviar un comando “solid”:
cat > ws_test.py << 'PY'
import asyncio, json, websockets, sys
IP = sys.argv[1] if len(sys.argv) > 1 else "192.168.1.50"
URL = f"ws://{IP}/ws"
async def main():
async with websockets.connect(URL) as ws:
print("Conectado a", URL)
# Recibe saludo
hello = await ws.recv()
print("←", hello)
# Envía rojo sólido
cmd = {"action": "solid", "r": 255, "g": 0, "b": 0}
await ws.send(json.dumps(cmd))
print("→", cmd)
resp = await ws.recv()
print("←", resp)
# Baja brillo
cmd2 = {"action":"brightness","value":0.1}
await ws.send(json.dumps(cmd2))
print("→", cmd2)
print("OK")
asyncio.run(main())
PY
python ws_test.py 192.168.1.50
Validación paso a paso
1) Validar arranque y Wi‑Fi:
– Usa mpremote repl para ver:
– “Wi‑Fi OK: (‘X.Y.Z.W’, …)”
– “Servidor en http://X.Y.Z.W:80”
– Si no aparece, revisa credenciales y cobertura.
2) Validar HTTP:
– En el host, ejecuta:
curl -i http://X.Y.Z.W/
Esperado: “HTTP/1.1 200 OK” y el HTML de index.html.
3) Validar WebSocket desde navegador:
– Abre http://X.Y.Z.W/
– Debes ver estado “Conectado”. Al pulsar “Aplicar” con un color, la tira pasa a ese color.
– Ajusta brillo; el consumo y luminosidad deben variar.
4) Validar WebSocket desde script:
– Ejecuta python ws_test.py X.Y.Z.W.
– Esperado:
– Mensaje de saludo JSON con “hello”: “pico-w” y “led_count”.
– Respuestas con “ok”: true tras cada comando.
5) Validar efectos:
– Arcoíris: el patrón debe desplazarse suavemente por la tira (actualización ~30 ms).
– Chase: un píxel de color base recorre la tira sobre fondo tenue.
6) Validar rendimiento/estabilidad:
– Navega entre efectos durante 2–3 minutos. No debería colgarse.
– Si tu tira es larga, limita brillo a 0.2–0.3 para evitar caídas de tensión.
7) Validar integridad eléctrica:
– Toca levemente el cableado (sin cortocircuitar) para detectar falsos contactos.
– Si ves parpadeos aleatorios, refuerza GND, usa la resistencia de 330 Ω y el condensador.
Troubleshooting
1) No se conecta al Wi‑Fi (OSError: Wi‑Fi timeout)
– Causas: SSID/clave incorrecta, canal 2.4 GHz saturado, bloqueo MAC, país/región Wi‑Fi mal configurado.
– Soluciones:
– Verifica WIFI_SSID y WIFI_PASS en main.py.
– Acerca la Pico W al AP; cambia a canal menos saturado (1/6/11).
– En el host, configura WLAN Country con raspi-config.
– Reintenta con cifrado WPA2‑PSK (evita WPA3 puro).
2) Navegador no conecta al WebSocket (estado “Error WS”)
– Causas: firewall, IP incorrecta, servidor HTTP no activo, ruta /ws mala.
– Soluciones:
– Asegura que navegas a http://IP_DE_LA_PICO/ (no HTTPS).
– Haz curl -i http://IP/ para confirmar HTTP.
– Verifica en REPL que “Servidor en http://IP:80” está activo.
– Comprueba que el navegador y la Pico están en la misma subred.
3) LEDs no encienden o colores incorrectos
– Causas: cableado errado (DIN↔DOUT), GND no común, falta de resistencia serie, inversión GRB/RGB de la tira.
– Soluciones:
– Verifica que usas el extremo “DIN”.
– Asegura GND común entre Pico y fuente/tira.
– Añade la resistencia de 330 Ω y el condensador.
– Si los colores están “desplazados”, ajusta el orden en el driver (algunas tiras usan GRB). En MicroPython, el módulo neopixel para WS2812B estándar ya asume GRB a bajo nivel; si observas desplazamiento, intercambia canales al escribir en np[i].
4) Parpadeos o inestabilidad al subir brillo
– Causas: caída de tensión por cable fino/largo, insuficiente fuente 5 V, ruido de conmutación.
– Soluciones:
– Reduce brillo ({"action":"brightness","value":0.2}).
– Usa fuente 5 V dedicada con suficiente margen y alimenta por ambos extremos de la tira si es larga.
– Coloca condensador 1000 µF cerca de la tira y mantén cables cortos.
5) “OSError: [Errno 98] EADDRINUSE” al reiniciar rápidamente
– Causa: socket en TIME_WAIT si el bucle de eventos no cerró del todo.
– Solución:
– Espera 2–3 segundos antes de re‑ejecutar.
– Ya usamos SO_REUSEADDR, pero si persiste, pulsa el botón RUN para reiniciar la Pico.
6) WebSocket se cierra al enviar mensajes “grandes”
– Causa: implementación mínima de frames no maneja fragmentación o payloads enormes.
– Soluciones:
– Envía JSON pequeños (pocos cientos de bytes).
– Si necesitas binarios grandes, usa HTTP o mejora el parser WS para soportar frames 126/127 de forma completa (ya hay soporte básico; evita MB).
7) Pico W no aparece como /dev/ttyACM0
– Causas: cable USB solo carga, permisos, falta de grupo dialout.
– Soluciones:
– Usa un cable USB con datos.
– lsusb para ver si aparece.
– Asegúrate de sudo usermod -aG dialout $USER y relogin.
8) Conexión igual “sin respuesta” tras handshake
– Causas: el navegador intenta wss://, CORS/HTTPS mixto, o se bloquea por extensión.
– Soluciones:
– Accede por http://IP/ (no https).
– Si corres detrás de un proxy HTTPS, necesitarás terminación TLS y proxypass de WS (avanzado, ver mejoras).
Mejoras/variantes
- Seguridad y producción:
- Termina TLS (wss://) en un proxy inverso (nginx/traefik) en tu LAN y proxy‑pass a ws://Pico:80/ws. Así proteges credenciales y evitas contenido mixto.
- Autenticación por token (Bearer) en el canal WebSocket y validación básica en el servidor (añade un header en la petición y compruébalo antes del upgrade).
- Descubrimiento y DNS:
- mDNS/zeroconf con un anunciador ligero en el host para resolver pico‑w.local (MicroPython en Pico W no provee mDNS de serie; considera un bridge en la LAN).
- Efectos avanzados:
- Añade paletas, transiciones suaves (ease‑in/out), mapas por índice de LED.
- Implementa una cola de animaciones con control de tiempo y cancelación.
- Rendimiento de LED:
- Cambia a control basado en PIO (rp2.StateMachine) con rutina específica para WS2812B si necesitas timings más estrictos con tareas concurrencia elevada.
- Doble buffer de color y solo
np.write()cuando cambien frames. - Persistencia:
- Guarda el último estado en un pequeño JSON en el filesystem de la Pico y recupéralo al arranque.
- Integración IoT:
- Puente MQTT↔WebSocket en el host (Python) para interoperar con dashboards (p. ej., Home Assistant vía MQTT).
- Sincroniza con NTP para timestamping de comandos y logging.
Checklist de verificación
- [ ] He montado la tira WS2812B a 5 V con GND común y resistencia serie de 330 Ω en DIN.
- [ ] He añadido el condensador de 1000 µF entre +5 V y GND de la tira.
- [ ] La Pico W tiene MicroPython 1.21.0 (UF2 correcto).
- [ ] En el host (Raspberry Pi OS Bookworm 64‑bit) tengo Python 3.11 y un venv activo.
- [ ] He instalado mpremote 1.22.0 y (opcional) websockets 11.0.3 en el venv.
- [ ] He cargado main.py e index.html en la Pico W con mpremote.
- [ ] He configurado correctamente WIFI_SSID y WIFI_PASS y veo la IP de la Pico W por REPL.
- [ ] Puedo abrir http://IP_DE_LA_PICO/ y el estado indica “Conectado”.
- [ ] Al enviar “Color sólido”, la tira cambia al color elegido.
- [ ] Los efectos “Arcoíris” y “Chase” funcionan sin parpadeos extraños.
- [ ] He validado brillo y consumo sin sobrecargar la fuente (si hay inestabilidad, reduzco brillo).
Con este caso práctico “websocket‑neopixel‑iot‑control” has desplegado un servidor WebSocket sobre una Raspberry Pi Pico W para controlar una tira WS2812B desde un navegador y desde un cliente Python, usando una toolchain concreta (Raspberry Pi OS Bookworm 64‑bit, Python 3.11, MicroPython 1.21.0, mpremote 1.22.0 y websockets 11.0.3) y manteniendo coherencia total con el modelo “Raspberry Pi Pico W + WS2812B strip”.
Encuentra este producto y/o libros sobre este tema en Amazon
Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.



