Now we're ready to tackle some code! There are two files that power this cube, and we can briefly talk through each.
Code.py
code.py is where the main loop lives. This is where we handle the accelerometer data and the network requests to fetch new data from AdafruitIO. This file is where we initialize a Cube class that will take care of the bulk of the logic for displaying stuff on the cube. In addition, there are two main functions defined and used in this file.
-
update_data()will make a call to AdafruitIO every time the scrolling word has finished one loop. This cadence was chosen to keep the scrolling animation relatively smooth and to reduce the amount of network calls. This function will update some global variables that are then passed into thecube.update()function -
orientation()will do some basic math and logic to detect the orientation of the cube in space. The resulting orientation is used to determine whichCubefunction to activate.
# SPDX-FileCopyrightText: 2022 Charlyn Gonda for Adafruit Industries
#
# SPDX-License-Identifier: MIT
from os import getenv
import ssl
import busio
import board
import adafruit_lis3dh
import wifi
import socketpool
import adafruit_requests
from adafruit_led_animation.color import (
PURPLE, AMBER, JADE, CYAN, BLUE, GOLD, PINK)
from adafruit_io.adafruit_io import IO_HTTP
from cube import Cube
# Get WiFi details and Adafruit IO keys, ensure these are setup in settings.toml
# (visit io.adafruit.com if you need to create an account, or if you need your Adafruit IO key.)
ssid = getenv("CIRCUITPY_WIFI_SSID")
password = getenv("CIRCUITPY_WIFI_PASSWORD")
aio_username = getenv("ADAFRUIT_AIO_USERNAME")
aio_key = getenv("ADAFRUIT_AIO_KEY")
if None in [ssid, password, aio_username, aio_key]:
raise RuntimeError(
"WiFi and Adafruit IO settings are kept in settings.toml, "
"please add them there. The settings file must contain "
"'CIRCUITPY_WIFI_SSID', 'CIRCUITPY_WIFI_PASSWORD', "
"'ADAFRUIT_AIO_USERNAME' and 'ADAFRUIT_AIO_KEY' at a minimum."
)
# Specify pins
top_cin = board.A0
top_din = board.A1
side_panels_cin = board.A2
side_panels_din = board.A3
bottom_cin = board.A4
bottom_din = board.A5
# Initialize cube with pins
cube = Cube(top_cin,
top_din,
side_panels_cin,
side_panels_din,
bottom_cin,
bottom_din)
# Initial display to indicate the cube is on
cube.waiting_mode()
# Setup for Accelerometer
i2c = busio.I2C(board.SCL1, board.SDA1)
lis3dh = adafruit_lis3dh.LIS3DH_I2C(i2c)
connected = False
while not connected:
try:
wifi.radio.connect(ssid, password)
print(f"Connected to {ssid}!")
print(f"My IP address is {wifi.radio.ipv4_address}")
connected = True
# pylint: disable=broad-except
except Exception as error:
print(error)
connected = False
# Setup for http requests
pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())
IO = IO_HTTP(aio_username, aio_key, requests)
# Data for top pixels, will be updated by update_data()
TOP_PIXELS_ON = []
TOP_PIXELS_COLOR = CYAN
TOP_PIXELS_COLOR_MAP = {
"PURPLE": PURPLE,
"AMBER": AMBER,
"JADE": JADE,
"CYAN": CYAN,
"BLUE": BLUE,
"GOLD": GOLD,
"PINK": PINK,
}
# Data for scrolling word, will be updated by update_data()
CUBE_WORD = "... ..."
def update_data():
# pylint: disable=global-statement
global CUBE_WORD, TOP_PIXELS_ON, TOP_PIXELS_COLOR
if connected:
print("Updating data from Adafruit IO")
try:
quote_feed = IO.get_feed('cube-words')
quotes_data = IO.receive_data(quote_feed["key"])
CUBE_WORD = quotes_data["value"]
pixel_feed = IO.get_feed('cube-pixels')
pixel_data = IO.receive_data(pixel_feed["key"])
color, pixels_list = pixel_data["value"].split("-")
TOP_PIXELS_ON = pixels_list.split(",")
TOP_PIXELS_COLOR = TOP_PIXELS_COLOR_MAP[color]
# pylint: disable=broad-except
except Exception as update_error:
print(update_error)
orientations = [
"UP",
"DOWN",
"RIGHT",
"LEFT",
"FRONT",
"BACK"
]
# pylint: disable=inconsistent-return-statements
def orientation(curr_x, curr_y, curr_z):
absX = abs(curr_x)
absY = abs(curr_y)
absZ = abs(curr_z)
if absX > absY and absX > absZ:
if x >= 0:
return orientations[1] # up
return orientations[0] # down
if absZ > absY and absZ > absX: # when "down" is "up"
if z >= 0:
return orientations[2] # left
return orientations[3] # right
if absY > absX and absY > absZ:
if y >= 0:
return orientations[4] # front
return orientations[5] # back
upside_down = False
while True:
x, y, z = lis3dh.acceleration
oriented = orientation(x, y, z)
# clear cube when on one side
# this orientation can be used while charging
if oriented == orientations[5]: # "back" side
cube.clear_cube(True)
continue
if oriented == orientations[1]:
upside_down = True
else:
upside_down = False
if not upside_down:
if cube.done_scrolling:
update_data()
cube.update(CUBE_WORD, TOP_PIXELS_COLOR, TOP_PIXELS_ON)
cube.scroll_word_and_update_top()
else:
cube.upside_down_mode()
Cube.py
cube.py contains a class called Cube that is responsible for all the cube display logic and keeping track of the cube's various states. It has 5 main functions that are used inside code.py:
update()is a convenience function to update both the word that scrolls through the cube and what pixels should be turned "on" for the top matrix, along what color those top pixels should bescroll_word_and_update_top()will continuously scroll through the given word and will show the pixel art on the top of the cubeclear_cube()can clear the sides of the cube, with the ability to clear the top of the cube if you setclearTop=Trueupside_down_mode()will display a specific cube animation, and this is triggered when the cube is upside-downwaiting_mode()just shows two pixels lit up on the top matrix, just to indicate that the cube is on when it first boots up.
When you assemble your cube, and you find that the word scroll orientation is not as you expected, you can try to flip the initialization of self.pixel_framebuf_sides inside the __init__ function of Cube to reverse_y=False, reverse_x=True to see if that will yield a better orientation.
Otherwise, there should be little to no modification needed for this code, but definitely feel free to modify!
# SPDX-FileCopyrightText: 2022 Charlyn Gonda for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import adafruit_dotstar
from adafruit_led_animation.animation.rainbowchase import RainbowChase
from adafruit_led_animation.color import AMBER, JADE, CYAN, GOLD, PINK
from adafruit_pixel_framebuf import PixelFramebuffer
class Cube():
def __init__(
self,
top_cin,
top_din,
side_panels_cin,
side_panels_din,
bottom_cin,
bottom_din):
# static numbers
self.num_pixels = 64*4
self.num_pixels_topbottom = 64
self.pixel_width = 8*4
self.pixel_height = 8
# top pixels
top_pixels = adafruit_dotstar.DotStar(
top_cin, top_din, self.num_pixels_topbottom,
brightness=0.03, auto_write=False)
self.pixel_framebuf_top = PixelFramebuffer(
top_pixels,
self.pixel_height,
self.pixel_height,
rotation=1,
alternating=False,
)
# side pixels
self.side_pixels = adafruit_dotstar.DotStar(
side_panels_cin, side_panels_din, self.num_pixels,
brightness=0.03, auto_write=False)
self.pixel_framebuf_sides = PixelFramebuffer(
self.side_pixels,
self.pixel_height,
self.pixel_width,
rotation=1,
alternating=False,
reverse_y=True,
reverse_x=False
)
self.rainbow_sides = RainbowChase(
self.side_pixels, speed=0.1, size=3, spacing=6)
# bottom pixels
pixels_bottom = adafruit_dotstar.DotStar(
bottom_cin, bottom_din, self.num_pixels_topbottom,
brightness=0.03, auto_write=False)
self.pixel_framebuf_bottom = PixelFramebuffer(
pixels_bottom,
self.pixel_height,
self.pixel_height,
rotation=0,
alternating=False,
reverse_y=False,
reverse_x=True
)
# scrolling word state vars
self.last_color_time = -1
self.color_wait = 1
self.word = ''
self.total_scroll_len = 0
self.scroll_x_pos = -self.pixel_width
self.color_idx = 0
self.color_list = [AMBER, JADE, CYAN, PINK, GOLD]
self.done_scrolling = False
# whether or not the cube is already clear
self.clear = False
# top pixel vars
self.top_pixel_coords = []
self.top_pixel_color = CYAN
# upside down mode
self.bottom_last = -1
self.bottom_wait = 1
self.bottom_squares = [[0, 0, 8, 8], [
1, 1, 6, 6], [2, 2, 4, 4], [3, 3, 2, 2]]
self.bottom_squares_idx = 0
def update(self, word, color, coords):
self.word = word
self.total_scroll_len = (len(self.word) * 5) + len(self.word)
self.top_pixel_coords = coords
self.top_pixel_color = color
def scroll_word_and_update_top(self):
if self.scroll_x_pos >= self.total_scroll_len:
self.scroll_x_pos = -self.pixel_width
self.clear_cube()
self.done_scrolling = True
else:
self.done_scrolling = False
self.clear = False
self.__scroll_framebuf(self.word, self.scroll_x_pos, 0)
self.__display_top_pixels()
self.scroll_x_pos = self.scroll_x_pos + 1
def clear_cube(self, clear_top=False):
if not self.clear:
self.pixel_framebuf_sides.fill(0)
self.pixel_framebuf_sides.display()
self.side_pixels.fill(0)
self.side_pixels.show()
self.pixel_framebuf_bottom.fill(0)
self.pixel_framebuf_bottom.display()
if clear_top:
self.pixel_framebuf_top.fill(0)
self.pixel_framebuf_top.display()
self.clear = True
def upside_down_mode(self):
self.clear_cube(True)
self.rainbow_sides.animate()
now = time.monotonic()
self.__bottom_square_animation(now)
def waiting_mode(self):
self.pixel_framebuf_top.pixel(3, 3, CYAN)
self.pixel_framebuf_top.pixel(4, 4, PINK)
self.pixel_framebuf_top.display()
def __bottom_square_animation(self, now):
self.pixel_framebuf_bottom.fill(0)
color_int = self._rgb_to_int(CYAN)
if now > self.bottom_last + self.bottom_wait:
self.__coord_wrap()
self.bottom_last = now
x, y, w, h = self.bottom_squares[self.bottom_squares_idx]
self.pixel_framebuf_bottom.rect(x, y, w, h, color_int)
self.pixel_framebuf_bottom.display()
def __coord_wrap(self):
self.bottom_squares_idx = self.bottom_squares_idx + 1
if self.bottom_squares_idx >= len(self.bottom_squares):
self.bottom_squares_idx = 0
def __display_top_pixels(self):
self.pixel_framebuf_top.fill(0)
for coord in self.top_pixel_coords:
x, y = coord.split(":")
self.pixel_framebuf_top.pixel(int(x), int(y), self.top_pixel_color)
self.pixel_framebuf_top.display()
def __scroll_framebuf(self, word, shift_x, shift_y):
self.pixel_framebuf_sides.fill(0)
color = self.__next_color()
color_int = self._rgb_to_int(color)
# negate x so that the word can be shown from left to right
self.pixel_framebuf_sides.text(word, -shift_x, shift_y, color_int)
self.pixel_framebuf_sides.display()
def __next_color(self):
if self.color_idx >= len(self.color_list):
self.color_idx = 0
result = self.color_list[self.color_idx]
now = time.monotonic()
if now >= self.last_color_time + self.color_wait:
self.color_idx = self.color_idx + 1
self.last_color_time = now
return result
@staticmethod
def _rgb_to_int(rgb):
return rgb[0] << 16 | rgb[1] << 8 | rgb[2]
Testing before final assembly
It will be a good idea to make sure that all the soldering and wiring we've done in the previous step went correctly. Upload both code.py and cube.py into your CIRCUITPY drive, and you should see this brief animation to verify that everything is working:
If you're seeing stuff on the matrices, that means you're good to go! You might even wiggle the accelerometer a bit, it should trigger the "upside down" animation which can help to make sure that the bottom matrix is also good to go.
Now we can move on to final assembly! Take a pause here and admire your work, you're almost done.
Page last edited January 22, 2025
Text editor powered by tinymce.