POV Wand

Use your Raspberry Breadstick as a POV (Persistance Of Vision) wand to draw images in the air using the LEDs!

Required Libraries

Code

"""
Raspberry Breadstick POV Display
www.learn.breadstick.ca
Michael Rangen
May 19, 2024

Loads an indexed bitmap impage and generates a
persistance of vision display as the Breadstick is waved
through the air.

To create an image, start in MSPaint, set the image
height to 24 pixels, and as wide as you want. Alternatively,
copy an image, paste it into paint, and scale the image to
24 pixels tall. Fill the background with black, and save as
as 24-bit bmp file. Open the image in GIMP, and in the toolbar
click Image > Mode > Indexed > Convert before returning to the
toolbar and clicking File > Overwrite your_image.bmp.

SK9822 LEDs are not initialized as DotStar objects, instead
data is sent to them by passing prepared bytearrays to a
bitbangio.SPI object clocking at 12MHz.

The bytearrays are prepared by using adafruit_imageload and
the displayio libraries to extract pixel data from an indexed
bitmap file stored in the root directory of the Breadstick.
A buffer of black columns is added to either side of the image.
Pixel data is padded with necessary bites defined by the SK9822
LED datasheet.

The LSM6DS IMU provides gyroscope data over I2C to determine
which direction the Breadstick is being waved and to control the
delay between each column being sent to the LEDs.
"""


"""
User Controls
"""
IMAGE_PATH = "HappyCat.bmp"
LED_BRIGHTNESS = 0.001
PIXEL_DELAY = 250
IMAGE_PADDING = 4


"""
Library Imports
"""
from board import *
import time

# For SK9822 LEDs
from displayio import Bitmap, Palette
import adafruit_imageload
import bitbangio
from microcontroller import delay_us

# For IMU (Gryoscope & Accellerometer)
import busio
from adafruit_lsm6ds.lsm6ds3trc import LSM6DS3TRC as LSM6DS
from adafruit_lsm6ds import Rate, AccelRange, GyroRange

# Garbage Collector
import gc

"""
LED Setup
"""
spi = bitbangio.SPI(DOTSTAR_CLOCK, DOTSTAR_DATA)
while not spi.try_lock():
    pass
spi.configure(baudrate=12000000)


"""
IMAGE Setup
"""
my_bitmap, my_palette = adafruit_imageload.load(
    IMAGE_PATH, bitmap=Bitmap, palette=Palette
)

columns = []

start = [0, 0, 0, 0]
end = [255, 255, 255, 255]
bright = 225 + int(LED_BRIGHTNESS * 30)

# Pad the left side of the image with black columns
for x in range(0, IMAGE_PADDING, 1):
    column = []
    column.extend(start)
    for y in range(my_bitmap.height - 1, -1, -1):
        column.append(bright)
        r = 0
        g = 0
        b = 0
        column.append(b)
        column.append(g)
        column.append(r)
    column.extend(end)
    columns.append(bytearray(column))
# Create columns from the indexed bitmap file.
# If you're getting errors about ColorConverter
# then your bitmap likely is in RGB mode.
# Open it in GIMP
# Image > Mode > Indexed > Convert
# File > Overwrite your_image.bmp
for x in range(0, my_bitmap.width, 1):
    column = []
    column.extend(start)
    for y in range(my_bitmap.height - 1, -1, -1):
        column.append(bright)
        pixel = my_palette[my_bitmap[x, y]]
        r = (pixel >> 16) & 0xFF
        g = (pixel >> 8) & 0xFF
        b = pixel & 0xFF

        column.append(b)
        column.append(g)
        column.append(r)
    column.extend(end)
    columns.append(bytearray(column))
# Pad the right side of the image with black columns
for x in range(0, IMAGE_PADDING, 1):
    column = []
    column.extend(start)
    for y in range(my_bitmap.height - 1, -1, -1):
        column.append(bright)
        r = 0
        g = 0
        b = 0
        column.append(b)
        column.append(g)
        column.append(r)
    column.extend(end)
    columns.append(bytearray(column))
"""
IMU Setup
"""
i2c = busio.I2C(IMU_SCL, IMU_SDA)
IMU = LSM6DS(i2c)
IMU.accelerometer_range = AccelRange.RANGE_4G
print("Accelerometer range set to: %d G" % AccelRange.string[IMU.accelerometer_range])
IMU.gyro_range = GyroRange.RANGE_1000_DPS
print("Gyro range set to: %d DPS" % GyroRange.string[IMU.gyro_range])
IMU.accelerometer_data_rate = Rate.RATE_1_66K_HZ
print("Accelerometer rate set to: %d HZ" % Rate.string[IMU.accelerometer_data_rate])
IMU.gyro_data_rate = Rate.RATE_1_66K_HZ
print("Gyro rate set to: %d HZ" % Rate.string[IMU.gyro_data_rate])

"""
Free Up Memory
"""
gc.collect()


"""
Main Program Loop
"""
while True:

    gyro_x, gyro_y, gyro_z = IMU.gyro
    if abs(gyro_z) < 0.001:
        gyro_z = 0.001
    col_delay = abs(int(PIXEL_DELAY / gyro_z))

    if gyro_z < 0:
        for i in range(0, len(columns), 1):
            gyro_x, gyro_y, gyro_z = IMU.gyro
            if abs(gyro_z) < 0.001:
                gyro_z = 0.001
            col_delay = abs(int(PIXEL_DELAY / gyro_z))
            if gyro_z > 0:
                break
            else:
                spi.write(columns[i])
                delay_us(col_delay)
    else:
        for i in range(len(columns) - 1, -1, -1):
            gyro_x, gyro_y, gyro_z = IMU.gyro
            if abs(gyro_z) < 0.001:
                gyro_z = 0.001
            col_delay = abs(int(PIXEL_DELAY / gyro_z))
            if gyro_z < 0:
                break
            else:
                spi.write(columns[i])
                delay_us(col_delay)

Last updated