# SPDX-License-Identifier: MIT
"""
WS2812 / WS2812B RGB LED strip driver (NeoPixel protocol).
Targets the common addressable-LED modules — the 8-LED "stick"
(WS2812B x8), rings, and cut-to-length strips — on a single data
GPIO. The bit-banged 800 kHz protocol itself comes from MicroPython's
built-in ``neopixel`` module; this wrapper adds what user code
actually wants on top of it:
* **Brightness scaling.** Raw WS2812s at full duty are dazzling and
hot; ``brightness=0.2`` is a comfortable indoor default (same
convention as the hub's onboard status LED). Colors are stored
unscaled and multiplied only at ``show()``, so ``strip[i]`` reads
back exactly what you assigned and changing ``brightness`` later
re-scales everything on the next ``show()``.
* **Buffered updates.** Item assignment only touches the buffer;
``show()`` pushes the whole strip in one wire transaction — an
animation frame is N assignments + one ``show()``, not N flickery
writes. ``fill()`` / ``clear()`` are one-call conveniences that
push immediately.
Usage::
from openbricks.drivers.ws2812 import WS2812
strip = WS2812(pin=21, n=8) # WS2812B x8 stick
strip.fill((0, 60, 0)) # everything green (pushed)
strip[0] = (255, 0, 0) # buffer only...
strip[7] = (0, 0, 255)
strip.show() # ...pushed together
Wiring the x8 stick: DIN → the data GPIO, 5V → 5 V supply, GND →
common ground. The ESP32's 3.3 V data line is out of spec for a
5 V-supplied WS2812B (V_IH = 0.7 × VDD = 3.5 V) but works with
virtually every module in practice; if you see glitches, power the
stick from 3.3 V (fine for the small x8 boards) or add a level
shifter.
"""
from machine import Pin
from openbricks import pins
[docs]
class WS2812:
"""A strip of ``n`` WS2812/WS2812B RGB LEDs on one data pin."""
def __init__(self, pin, n=8, brightness=0.2):
"""
Args:
pin: GPIO number the strip's DIN is wired to.
n: number of LEDs (8 for the common WS2812B x8 stick).
brightness: 0.0 – 1.0 scale applied to every channel at
``show()`` time. 0.2 is a comfortable indoor default.
Raises:
ValueError: on a non-positive ``n`` or a ``brightness``
outside 0.0 – 1.0.
openbricks.pins.ReservedPinError: when ``pin`` is
reserved (flash/USB) on the detected chip.
"""
n = int(n)
if n <= 0:
raise ValueError("n must be >= 1 (got %r)" % (n,))
pins.check(pin, "WS2812 data")
# Lazy import like the hub's NeoPixelLED: the module exists on
# firmware (and as a fake under test); importing at call time
# keeps CPython tooling that merely imports the driver happy.
import neopixel
self._np = neopixel.NeoPixel(Pin(pin), n)
self._n = n
self._buf = [(0, 0, 0)] * n
self._brightness = self._check_brightness(brightness)
# Known state at construction: all off, pushed to the wire.
self.clear()
@staticmethod
def _check_brightness(value):
value = float(value)
if not 0.0 <= value <= 1.0:
raise ValueError(
"brightness must be within 0.0 - 1.0 (got %r)" % (value,))
return value
# ---- container protocol (buffered; call show() to push) ----
def __len__(self):
return self._n
def __setitem__(self, index, color):
r, g, b = color
self._buf[index] = (int(r), int(g), int(b))
def __getitem__(self, index):
return self._buf[index]
# ---- pushing to the wire ----
[docs]
def show(self):
"""Push the buffered colors to the strip (one transaction),
applying the current ``brightness`` scale."""
scale = self._brightness
for i in range(self._n):
r, g, b = self._buf[i]
self._np[i] = (int(r * scale), int(g * scale), int(b * scale))
self._np.write()
[docs]
def fill(self, color):
"""Set every LED to ``color`` (an ``(r, g, b)`` tuple) and
push immediately."""
r, g, b = color
rgb = (int(r), int(g), int(b))
for i in range(self._n):
self._buf[i] = rgb
self.show()
[docs]
def clear(self):
"""All LEDs off, pushed immediately."""
self.fill((0, 0, 0))
# ---- brightness ----
@property
def brightness(self):
"""Global brightness scale 0.0 – 1.0. Assigning re-scales the
whole strip on the next ``show()`` (or right now via
``show()`` — colors are stored unscaled)."""
return self._brightness
@brightness.setter
def brightness(self, value):
self._brightness = self._check_brightness(value)