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 whichCube
function to activate.
# SPDX-FileCopyrightText: 2022 Charlyn Gonda for Adafruit Industries # # SPDX-License-Identifier: MIT from secrets import secrets 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 # 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(secrets["ssid"], secrets["password"]) print("Connected to %s!" % secrets["ssid"]) print("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(secrets["aio_username"], secrets["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=True
upside_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.
Text editor powered by tinymce.