The QWIIC/STEMMA-QT ecosystem is kind of neat…standard connectors for I2C gadgets to allow you to solderlessly connect hardware to your microcontroller. But it is suprisingly difficult to find an RGB LED in this ecosystem…

Goals

  • The current claude-light relies on some PWM driven RGB LEDs attached by protoboard.

  • Our goal is to turn this into a completely solderless plug-and-play system to make it easy for others to replicate. Eventually make a 3d-printed case for the sensor, LED, and camera.

  • Use a Sparkfun QWIIC/Stemma-QT SHIM for the raspberry pi($2, also sold by Adafruit for $2.50) for the electrical connections and send information by I2C. In the Sparkfun reviews, some folks found it to be unreliable, and recommended instead just using a female jumper to Qwiic ($2) connector instead. Still admits solderless assembly approach with everything on the same digital I2C bus.

    • In my testing of this, I found that the SHIM made a nice press-fit on an old Raspberry Pi 2, but was too loose to make a reliable connection on a Raspberry Pi 4b (the pins have a slightly different pitch?)
    • More reliable was to use the Qwiic HAT for Raspberry Pi ($8) instead; this also removes any need for daisychaining devices

Background on standards

  • First, there are many standards for this type of plug-and-play work besides QWIIC and STEMMA-QT—two common ones are Grove and Gravity. See comparison chart. Warning: While DFRobot Gravity and STEMMA use the same shaped connectors, they are electrically incompatible (different wiring order)

  • The trick is that even the “digital” versions of RGB LED carrier boards are not necessarily I2C based…

Product landscape (01 Aug 2025)

Programming the Modulino Pixels

Let’s assume that we’re going to use the Modulino and want to connect it to a Raspberry Pi. How do we control it?

  1. Enable I2C on the Raspberry Pi and sudo apt-get install -y i2c-tools
  2. Use i2c-tools to scan the bus and confirm connected devices
  3. Either Use py-smbus to programmatically interact with the i2c devices from Raspberry Pi
    • and then read the datasheet and micropython code and just code up the raw py-smbus calls.
  4. OR use blinka to use premade circuitpython libraries on the Raspberry Pi. This is probably preferable, as the existing claude-light code uses blinka to interface with the spectral sensor

Under the hood of the Modulino Pixels

It’s easier to understand what is happening by reading the C code than the micropython code:

  • Each LED has a setting of R / G/ B/ Brightness. R, G, B are uint8 values (0-255, of 0x00-0xFF), sent in that order. Brightness is only 5 bits (0-31 or 0x00-0x1F). It’s unspecified how final global brighness is implemented under the hood, but my guess is that it just scales the PWM values again.
  • We pad out the remaining preceding (highest) 3-bits in the last byte with 0xE0 (11100000)
  • In practice this looks like: r << 24 | g<<16| b << 8 | brightness | 0xE0
  • Store an array of NUM_LED uints32 (i.e., NUM_LED * 4 uint8s) containing the state of all pixels
  • A show() command just writes all 8x4 bytes down the I2C command.
  • But device expects these send in little-endian order, whereas python’s bytearray is naturally big-endian (at least on Raspberry Pi), so you can just send these as bytearray([ brightness|0xE0, b ,g,r]*NUM_LEDS)
  • By default (unless you reprogram it), the Modulino Pixels is I2C device ID 0x36

Minimal Circuit Python / Blinka Sample Code:

import board
from adafruit_bus_device.i2c_device import I2CDevice


PIXELS_ADDRESS = 0x36
NUM_LEDS = 8

i2c = board.I2C()
print("found i2c devices:", [hex(x) for x in i2c.scan()])

pixels = I2CDevice(i2c, PIXELS_ADDRESS)

# simply cycle around 
with pixels:
    while True:
        clear_all = bytearray([0xE0, 0x00, 0x00, 0x00] * NUM_LEDS )
        print("clear")
        pixels.write(clear_all)
        input("Press Enter to continue...")
        red = bytearray([0x1F | 0xE0, 0x00, 0x00, 0xFF]*NUM_LEDS )
        print("red")
        pixels.write(red)
        input("Press Enter to continue...")
        green = bytearray([ 0x1F | 0xE0, 0x00, 0xFF, 0x00]*NUM_LEDS)
        print("green")
        pixels.write(green)
        input("Press Enter to continue...")
        blue = bytearray([0x1F | 0xE0, 0xFF, 0x00, 0x00]*NUM_LEDS )
        pixels.write(blue)
        input("Press Enter to continue...")

print("done")