Skip to content

Instantly share code, notes, and snippets.

@rpavlik
Created May 9, 2023 22:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rpavlik/c5c56e320fb5647c16dfe8538a0cf364 to your computer and use it in GitHub Desktop.
Save rpavlik/c5c56e320fb5647c16dfe8538a0cf364 to your computer and use it in GitHub Desktop.
AdaFruit LED Glasses Digital Rain, plus Blinky
# SPDX-FileCopyrightText: 2021-2023 Ryan Pavlik
#
# A little bit of board setup based on code that is:
#
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Display 'digital rain' aka 'the Matrix thing' on the Adafruit LED Glasses,
https://www.adafruit.com/product/5255
Should be adaptable to other displays however, code is very modular.
Some basic setup and supervisor code based on an Adafruit example.
Digital rain effect implemented and tuned by Ryan Pavlik.
Press and hold the button after the onboard neopixel starts blinking to boot up
in continuous mode rather than blinky mode.
Rename to code.py for use.
Version prior to refactor for use as a "blinky" project: https://gist.github.com/rpavlik/1e927800f22fb6c16038a76497cc3fc7
"""
import math
import random
import time
from supervisor import reload
import digitalio
import board
from busio import I2C
import adafruit_is31fl3741
from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses
# HARDWARE SETUP -----------------------
# Manually declare I2C (not board.I2C() directly) to access 1 MHz speed...
i2c = I2C(board.SCL, board.SDA, frequency=1000000)
# Initialize the IS31 LED driver, buffered for smoother animation
glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
glasses.show() # Clear any residue on startup
glasses.global_current = 10 # Just middlin' bright, please
# glasses.global_current = 15
# Set up switch
sw = digitalio.DigitalInOut(board.SWITCH)
sw.direction = digitalio.Direction.INPUT
sw.pull = digitalio.Pull.UP
# CONFIG ----------------------------
NUM_DROPS = 10
"""Max number of simultaneous 'drops" to display."""
DEFAULT_MAX_SPEED = 3
"""Max speed in rows per second"""
MIN_SPEED = 1
"""Minimum speed in rows per second"""
BLINKY_MODE = True
"""
Blink on and off? This will also override some of the default settings
to make it look better when blinking.
"""
BLINKY_ON_DURATION = 1
"""In blinky mode: How long to stay on"""
BLINKY_OFF_DURATION = 1
"""In blinky mode: How long to stay off"""
# Do not enter blinky mode if switch is held during startup
OVERRIDE_BLINKY = not sw.value
if BLINKY_MODE and OVERRIDE_BLINKY:
print("OVERRIDE: Blinky mode turned off!")
BLINKY_MODE = False
if BLINKY_MODE:
print("ENGAGE BLINKY MODE!!!")
# Make the effect more intense if we are in blinky mode so it's actually visible.
NUM_DROPS = 15
DEFAULT_MAX_SPEED = 5
MIN_SPEED = 3
# bump up the current too
glasses.global_current = 20
led = digitalio.DigitalInOut(board.LED)
led.direction = digitalio.Direction.OUTPUT
# CODE -----------------------------
def gamma_correct_intensity(v: float) -> float:
return math.pow(v, 2.2)
class DigitalRaindrop:
"""A single 'drop' in the digital rain"""
def __init__(self, grid, max_speed=DEFAULT_MAX_SPEED):
self._grid_width = grid.width
self._grid_height = grid.height
self.max_speed = max_speed
# self.color = 0x00FF00
self.max_length = 6
self.dead = False
"""Is this drop dead and no longer visible?"""
# We could put this in a reset method, but the NRF52840
# is beefy enough that we don't mind just destroying then
# later re-creating the drop each time.
self.col = random.randint(0, self._grid_width - 1)
"""Randomly chosen column in the grid"""
self.row: float = 0
"""**floating point** row position, starts at row 0"""
self.speed = random.uniform(MIN_SPEED, self.max_speed)
"""Randomly chosen speed"""
def get_intensity(self, row: int) -> float:
"""
Compute the intensity of this drop's influence in a given row.
Intensity ramps up linearly from 0 to 1 in a single row before the "current" row,
then slowly ramps linearly down over the max length.
(The ramp up improves how smooth the animation appears.)
Gamma correction of 2.2 is applied so it looks more linear.
"""
# More than 1 row ahead of our position, we have no intensity.
if row > self.row + 1:
return 0
# quick ramp up "ahead" of the end
if row > self.row:
linear = 1 - (row - self.row)
else:
# Do not need equality case because floats and this handles it fine too.
linear = max(0, 1 - (self.row - row) / self.max_length)
# Apply gamma correction so it looks more linear
return math.pow(linear, 2.2)
def _update_dead(self):
"""Update our dead state based on our row."""
if self.row - self.max_length > self._grid_height:
self.dead = True
return self.dead
def update(self, dt, glasses: LED_Glasses):
"""
Update our row.
Does not update the buffer for the pixels.
"""
if self._update_dead():
return
self.row += self.speed * dt
def populate_glasses(self, glasses: LED_Glasses):
"""Update pixels for this drop"""
# When doing "blinky" mode, a drop might fall quickly so recheck
if self._update_dead():
return
for y in range(0, glasses.height):
alpha = self.get_intensity(y)
# print(y, alpha)
glasses.pixel(self.col, int(y), int(alpha * 256) << 8)
class RaindropCollection:
"""
The state for a whole collection of digital raindrops.
Encapsulating this made it easier to implement blinky mode,
since we still update the digital rain while we have blinked 'off'
"""
def __init__(self):
self.rain = [DigitalRaindrop(glasses) for _ in range(NUM_DROPS)]
self.prev = time.monotonic()
"""Time of previous call to update."""
self.last_full = self.prev
"""Time of most recent update when the max drops were alive"""
self.last_on = self.prev
"""Time when we last went from blink-off to blink-on"""
def update(self, now: float):
"""Update state for a given time (in seconds)"""
dt = now - self.prev
self.prev = now
# Update existing drops
for drop in self.rain:
drop.update(dt, glasses)
if len(self.rain) == NUM_DROPS:
self.last_full = now
# Filter out dead "raindrops"
self.rain = [drop for drop in self.rain if not drop.dead]
# If we are not full yet, we may wish to add another drop.
# This gets more likely the longer it has been since we were full.
if len(self.rain) < NUM_DROPS:
if random.uniform(0, now - self.last_full) > 0.5:
self.rain.append(DigitalRaindrop(glasses))
def show(self):
"""Update the pixel buffer from the active drops and show it."""
try:
for drop in self.rain:
drop.populate_glasses(glasses)
glasses.show() # Buffered mode MUST use show() to refresh matrix
except OSError: # See "try" notes above regarding rare I2C errors.
print("Restarting")
reload()
def hide():
"""Turn all pixels off."""
glasses.fill(0)
try:
glasses.show()
except OSError: # See "try" notes above regarding rare I2C errors.
print("Restarting")
reload()
# Little easter egg for those looking at the code and/or serial terminal :-D
print("The Matrix is an exceptional trans allegory")
print("Pre-order 'Begin Transmission' by screenwriter (and friend!) Tilly Bridges for details :-P")
print("and/or visit https://tillystranstuesdays.com for more of her writing")
# MAIN LOOP ----------------------------
drops = RaindropCollection()
blinky_start = time.monotonic()
while True:
now = time.monotonic()
drops.update(now)
drops.show()
if BLINKY_MODE:
# Check to see if we're due to turn off
time_since_on = now - blinky_start
if time_since_on > BLINKY_ON_DURATION:
print(f"Blinky off: {now} - {time_since_on}")
# TODO Is there a way to turn off all pixels quicker?
hide()
led.value = False
while True:
now = time.monotonic()
# Update but do not show
drops.update(now)
time_since_on = now - blinky_start
if time_since_on > BLINKY_ON_DURATION + BLINKY_OFF_DURATION:
# done waiting!
print(f"Blinky on: {now} - {time_since_on}")
blinky_start = now
# Turn on LED on board for bonus blinky
led.value = True
break
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment