Skip to content

Instantly share code, notes, and snippets.

@joadoumie
Last active January 12, 2024 21:21
Show Gist options
  • Save joadoumie/26df8686a94b49a35f76501da1020f5f to your computer and use it in GitHub Desktop.
Save joadoumie/26df8686a94b49a35f76501da1020f5f to your computer and use it in GitHub Desktop.
Adafruit Matrix Portal
# SPDX-FileCopyrightText: 2020 John Park for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Quote board matrix display
# uses AdafruitIO to serve up a quote text feed and color feed
# random quotes are displayed, updates periodically to look for new quotes
# avoids repeating the same quote twice in a row
import time
import random
import board
import terminalio
from adafruit_matrixportal.matrixportal import MatrixPortal
# --- Display setup ---
matrixportal = MatrixPortal(status_neopixel=board.NEOPIXEL, debug=True)
# Create a new label with the color and text selected
matrixportal.add_text(
text_font=terminalio.FONT,
text_position=(0, (matrixportal.graphics.display.height // 2) - 1),
scrolling=True,
)
# Static 'Connecting' Text
matrixportal.add_text(
text_font=terminalio.FONT,
text_position=(2, (matrixportal.graphics.display.height // 2) - 1),
)
QUOTES_FEED = "sign-quotes.signtext"
COLORS_FEED = "sign-quotes.signcolor"
SCROLL_DELAY = 0.02
UPDATE_DELAY = 600
quotes = ["Hello World!", "Here we go!", "Subscribe and comment if you like the content!"]
# create an array called colors with the colors of the rainbow, hex strings
colors = ["#FF0000", "#FF7F00", "#FFFF00", "#00FF00", "#0000FF", "#4B0082", "#8F00FF"]
last_color = None
last_quote = None
def update_data():
print("Updating data from Adafruit IO")
matrixportal.set_text("Connecting", 1)
try:
quotes_data = matrixportal.get_io_data(QUOTES_FEED)
quotes.clear()
for json_data in quotes_data:
quotes.append(matrixportal.network.json_traverse(json_data, ["value"]))
print(quotes)
# pylint: disable=broad-except
except Exception as error:
print(error)
try:
color_data = matrixportal.get_io_data(COLORS_FEED)
colors.clear()
for json_data in color_data:
colors.append(matrixportal.network.json_traverse(json_data, ["value"]))
print(colors)
# pylint: disable=broad-except
except Exception as error:
print(error)
if not quotes or not colors:
raise RuntimeError("Please add at least one quote and color to your feeds")
matrixportal.set_text(" ", 1)
update_data()
last_update = time.monotonic()
matrixportal.set_text(" ", 1)
quote_index = None
color_index = None
while True:
# Choose a random quote from quotes
if len(quotes) > 1 and last_quote is not None:
while quote_index == last_quote:
quote_index = random.randrange(0, len(quotes))
else:
quote_index = random.randrange(0, len(quotes))
last_quote = quote_index
# Choose a random color from colors
if len(colors) > 1 and last_color is not None:
while color_index == last_color:
color_index = random.randrange(0, len(colors))
else:
color_index = random.randrange(0, len(colors))
last_color = color_index
# Set the quote text
matrixportal.set_text(quotes[quote_index])
# Set the text color
matrixportal.set_text_color(colors[color_index])
# Scroll it
matrixportal.scroll_text(SCROLL_DELAY)
if time.monotonic() > last_update + UPDATE_DELAY:
update_data()
last_update = time.monotonic()
# SPDX-FileCopyrightText: 2020 Phillip Burgess for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
RASTER EYES for Adafruit Matrix Portal: animated spooky eyes.
"""
# pylint: disable=import-error
import math
import random
import time
import displayio
import adafruit_imageload
from adafruit_matrixportal.matrix import Matrix
# TO LOAD DIFFERENT EYE DESIGNS: change the middle word here (between
# 'eyes.' and '.data') to one of the folder names inside the 'eyes' folder:
#from eyes.werewolf.data import EYE_DATA
#from eyes.cyclops.data import EYE_DATA
#from eyes.kobold.data import EYE_DATA
#from eyes.adabot.data import EYE_DATA
from eyes.skull.data import EYE_DATA
# UTILITY FUNCTIONS AND CLASSES --------------------------------------------
# pylint: disable=too-few-public-methods
class Sprite(displayio.TileGrid):
"""Single-tile-with-bitmap TileGrid subclass, adds a height element
because TileGrid doesn't appear to have a way to poll that later,
object still functions in a displayio.Group.
"""
def __init__(self, filename, transparent=None):
"""Create Sprite object from color-paletted BMP file, optionally
set one color to transparent (pass as RGB tuple or list to locate
nearest color, or integer to use a known specific color index).
"""
bitmap, palette = adafruit_imageload.load(
filename, bitmap=displayio.Bitmap, palette=displayio.Palette)
if isinstance(transparent, (tuple, list)): # Find closest RGB match
closest_distance = 0x1000000 # Force first match
for color_index, color in enumerate(palette): # Compare each...
delta = (transparent[0] - ((color >> 16) & 0xFF),
transparent[1] - ((color >> 8) & 0xFF),
transparent[2] - (color & 0xFF))
rgb_distance = (delta[0] * delta[0] +
delta[1] * delta[1] +
delta[2] * delta[2]) # Actually dist^2
if rgb_distance < closest_distance: # but adequate for
closest_distance = rgb_distance # compare purposes,
closest_index = color_index # no sqrt needed
palette.make_transparent(closest_index)
elif isinstance(transparent, int):
palette.make_transparent(transparent)
super(Sprite, self).__init__(bitmap, pixel_shader=palette)
# ONE-TIME INITIALIZATION --------------------------------------------------
MATRIX = Matrix(bit_depth=6)
DISPLAY = MATRIX.display
# Order in which sprites are added determines the 'stacking order' and
# visual priority. Lower lid is added before the upper lid so that if they
# overlap, the upper lid is 'on top' (e.g. if it has eyelashes or such).
SPRITES = displayio.Group()
SPRITES.append(Sprite(EYE_DATA['eye_image'])) # Base image is opaque
SPRITES.append(Sprite(EYE_DATA['lower_lid_image'], EYE_DATA['transparent']))
SPRITES.append(Sprite(EYE_DATA['upper_lid_image'], EYE_DATA['transparent']))
SPRITES.append(Sprite(EYE_DATA['stencil_image'], EYE_DATA['transparent']))
DISPLAY.root_group = SPRITES
EYE_CENTER = ((EYE_DATA['eye_move_min'][0] + # Pixel coords of eye
EYE_DATA['eye_move_max'][0]) / 2, # image when centered
(EYE_DATA['eye_move_min'][1] + # ('neutral' position)
EYE_DATA['eye_move_max'][1]) / 2)
EYE_RANGE = (abs(EYE_DATA['eye_move_max'][0] - # Max eye image motion
EYE_DATA['eye_move_min'][0]) / 2, # delta from center
abs(EYE_DATA['eye_move_max'][1] -
EYE_DATA['eye_move_min'][1]) / 2)
UPPER_LID_MIN = (min(EYE_DATA['upper_lid_open'][0], # Motion bounds of
EYE_DATA['upper_lid_closed'][0]), # upper and lower
min(EYE_DATA['upper_lid_open'][1], # eyelids
EYE_DATA['upper_lid_closed'][1]))
UPPER_LID_MAX = (max(EYE_DATA['upper_lid_open'][0],
EYE_DATA['upper_lid_closed'][0]),
max(EYE_DATA['upper_lid_open'][1],
EYE_DATA['upper_lid_closed'][1]))
LOWER_LID_MIN = (min(EYE_DATA['lower_lid_open'][0],
EYE_DATA['lower_lid_closed'][0]),
min(EYE_DATA['lower_lid_open'][1],
EYE_DATA['lower_lid_closed'][1]))
LOWER_LID_MAX = (max(EYE_DATA['lower_lid_open'][0],
EYE_DATA['lower_lid_closed'][0]),
max(EYE_DATA['lower_lid_open'][1],
EYE_DATA['lower_lid_closed'][1]))
EYE_PREV = (0, 0)
EYE_NEXT = (0, 0)
MOVE_STATE = False # Initially stationary
MOVE_EVENT_DURATION = random.uniform(0.1, 3) # Time to first move
BLINK_STATE = 2 # Start eyes closed
BLINK_EVENT_DURATION = random.uniform(0.25, 0.5) # Time for eyes to open
TIME_OF_LAST_MOVE_EVENT = TIME_OF_LAST_BLINK_EVENT = time.monotonic()
# MAIN LOOP ----------------------------------------------------------------
while True:
NOW = time.monotonic()
# Eye movement ---------------------------------------------------------
if NOW - TIME_OF_LAST_MOVE_EVENT > MOVE_EVENT_DURATION:
TIME_OF_LAST_MOVE_EVENT = NOW # Start new move or pause
MOVE_STATE = not MOVE_STATE # Toggle between moving & stationary
if MOVE_STATE: # Starting a new move?
MOVE_EVENT_DURATION = random.uniform(0.08, 0.17) # Move time
ANGLE = random.uniform(0, math.pi * 2)
EYE_NEXT = (math.cos(ANGLE) * EYE_RANGE[0], # (0,0) in center,
math.sin(ANGLE) * EYE_RANGE[1]) # NOT pixel coords
else: # Starting a new pause
MOVE_EVENT_DURATION = random.uniform(0.04, 3) # Hold time
EYE_PREV = EYE_NEXT
# Fraction of move elapsed (0.0 to 1.0), then ease in/out 3*e^2-2*e^3
RATIO = (NOW - TIME_OF_LAST_MOVE_EVENT) / MOVE_EVENT_DURATION
RATIO = 3 * RATIO * RATIO - 2 * RATIO * RATIO * RATIO
EYE_POS = (EYE_PREV[0] + RATIO * (EYE_NEXT[0] - EYE_PREV[0]),
EYE_PREV[1] + RATIO * (EYE_NEXT[1] - EYE_PREV[1]))
# Blinking -------------------------------------------------------------
if NOW - TIME_OF_LAST_BLINK_EVENT > BLINK_EVENT_DURATION:
TIME_OF_LAST_BLINK_EVENT = NOW # Start change in blink
BLINK_STATE += 1 # Cycle paused/closing/opening
if BLINK_STATE == 1: # Starting a new blink (closing)
BLINK_EVENT_DURATION = random.uniform(0.03, 0.07)
elif BLINK_STATE == 2: # Starting de-blink (opening)
BLINK_EVENT_DURATION *= 2
else: # Blink ended,
BLINK_STATE = 0 # paused
BLINK_EVENT_DURATION = random.uniform(BLINK_EVENT_DURATION * 3, 4)
if BLINK_STATE: # Currently in a blink?
# Fraction of closing or opening elapsed (0.0 to 1.0)
RATIO = (NOW - TIME_OF_LAST_BLINK_EVENT) / BLINK_EVENT_DURATION
if BLINK_STATE == 2: # Opening
RATIO = 1.0 - RATIO # Flip ratio so eye opens instead of closes
else: # Not blinking
RATIO = 0
# Eyelid tracking ------------------------------------------------------
# Initial estimate of 'tracked' eyelid positions
UPPER_LID_POS = (EYE_DATA['upper_lid_center'][0] + EYE_POS[0],
EYE_DATA['upper_lid_center'][1] + EYE_POS[1])
LOWER_LID_POS = (EYE_DATA['lower_lid_center'][0] + EYE_POS[0],
EYE_DATA['lower_lid_center'][1] + EYE_POS[1])
# Then constrain these to the upper/lower lid motion bounds
UPPER_LID_POS = (min(max(UPPER_LID_POS[0],
UPPER_LID_MIN[0]), UPPER_LID_MAX[0]),
min(max(UPPER_LID_POS[1],
UPPER_LID_MIN[1]), UPPER_LID_MAX[1]))
LOWER_LID_POS = (min(max(LOWER_LID_POS[0],
LOWER_LID_MIN[0]), LOWER_LID_MAX[0]),
min(max(LOWER_LID_POS[1],
LOWER_LID_MIN[1]), LOWER_LID_MAX[1]))
# Then interpolate between bounded tracked position to closed position
UPPER_LID_POS = (UPPER_LID_POS[0] + RATIO *
(EYE_DATA['upper_lid_closed'][0] - UPPER_LID_POS[0]),
UPPER_LID_POS[1] + RATIO *
(EYE_DATA['upper_lid_closed'][1] - UPPER_LID_POS[1]))
LOWER_LID_POS = (LOWER_LID_POS[0] + RATIO *
(EYE_DATA['lower_lid_closed'][0] - LOWER_LID_POS[0]),
LOWER_LID_POS[1] + RATIO *
(EYE_DATA['lower_lid_closed'][1] - LOWER_LID_POS[1]))
# Move eye sprites -----------------------------------------------------
SPRITES[0].x, SPRITES[0].y = (int(EYE_CENTER[0] + EYE_POS[0] + 0.5),
int(EYE_CENTER[1] + EYE_POS[1] + 0.5))
SPRITES[2].x, SPRITES[2].y = (int(UPPER_LID_POS[0] + 0.5),
int(UPPER_LID_POS[1] + 0.5))
SPRITES[1].x, SPRITES[1].y = (int(LOWER_LID_POS[0] + 0.5),
int(LOWER_LID_POS[1] + 0.5))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment