This sample code will display .bmp image that encourages folks to leave you a tip. Whenever someone drops a coin or a bill through the slot, a "Thank You" animation with fireworks will play.
You can create and upload your own images, and also customize the number of times the animation plays before resetting.
Libraries
We'll need to make sure we have these libraries installed. (Check out this link on installing libraries if needed.)
- adafruit_bus_device
- adafruit_debouncer
- adafruit_lis3dh
- adafruit_matrixportal
- adafruit_slideshow
- adafruit_vl6180x
Text Editor
Adafruit recommends using the Mu editor for editing your CircuitPython code. You can get more info in this guide.
Alternatively, you can use any text editor that saves simple text files.
Code
Click the Download: Project Zip File link below in the code window to get a zip file with all the files needed for the project. Copy code.py from the zip file and place on the CIRCUITPY drive.
You'll also need to copy the following files to the CIRCUITPY drive. See the graphic at the top of the page as to filenames and where they go):
- /bmps directory, which contains the graphics .bmp files.
The button below contains our sample graphics: a "Feeling Tipsy?" .bmp file, and a "Thank You" fireworks animation sprite sheet.
# SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries # SPDX-FileCopyrightText: 2020 Erin St Blaine for Adafruit Industries # # SPDX-License-Identifier: MIT """ Motion-sensing Animation Example using the Matrix Portal and 64 x 32 LED matrix display Written by Melissa LeBlanc-Williams and Erin St Blaine for Adafruit Industries A VL6180X sensor causes a sprite sheet animation to play """ import time import os import board import busio import displayio from digitalio import DigitalInOut, Pull from adafruit_matrixportal.matrix import Matrix from adafruit_debouncer import Debouncer import adafruit_vl6180x # pylint: disable=global-statement # Create I2C bus. i2c = busio.I2C(board.SCL, board.SDA) # Create sensor instance. sensor = adafruit_vl6180x.VL6180X(i2c) SPRITESHEET_FOLDER = "/bmps" DEFAULT_FRAME_DURATION = 0.1 # 100ms ANIMATION_DURATION = 5 AUTO_ADVANCE_LOOPS = 1 THRESHOLD = 20 # --- Display setup --- matrix = Matrix(bit_depth=4) sprite_group = displayio.Group() matrix.display.root_group = sprite_group # --- Button setup --- pin_down = DigitalInOut(board.BUTTON_DOWN) pin_down.switch_to_input(pull=Pull.UP) button_down = Debouncer(pin_down) pin_up = DigitalInOut(board.BUTTON_UP) pin_up.switch_to_input(pull=Pull.UP) button_up = Debouncer(pin_up) AUTO_ADVANCE = True file_list = sorted( [ f for f in os.listdir(SPRITESHEET_FOLDER) if (f.endswith(".bmp") and not f.startswith(".")) ] ) if len(file_list) == 0: raise RuntimeError("No images found") CURRENT_IMAGE = None CURRENT_FRAME = 0 CURRENT_LOOP = 0 FRAME_COUNT = 0 FRAME_DURATION = DEFAULT_FRAME_DURATION def load_image(): """ Load an image as a sprite """ # pylint: disable=global-statement global CURRENT_FRAME, CURRENT_LOOP, FRAME_COUNT, FRAME_DURATION while sprite_group: sprite_group.pop() filename = SPRITESHEET_FOLDER + "/" + file_list[CURRENT_IMAGE] # CircuitPython 6 & 7 compatible bitmap = displayio.OnDiskBitmap(open(filename, "rb")) sprite = displayio.TileGrid( bitmap, pixel_shader=getattr(bitmap, 'pixel_shader', displayio.ColorConverter()), tile_width=bitmap.width, tile_height=matrix.display.height, ) # # CircuitPython 7+ compatible # bitmap = displayio.OnDiskBitmap(filename) # sprite = displayio.TileGrid( # bitmap, # pixel_shader=bitmap.pixel_shader, # tile_width=bitmap.width, # tile_height=matrix.display.height, # ) sprite_group.append(sprite) FRAME_COUNT = int(bitmap.height / matrix.display.height) FRAME_DURATION = DEFAULT_FRAME_DURATION CURRENT_FRAME = 0 CURRENT_LOOP = 0 def advance_image(): """ Advance to the next image in the list and loop back at the end """ # pylint: disable=global-statement global CURRENT_IMAGE if CURRENT_IMAGE is not None: CURRENT_IMAGE += 1 if CURRENT_IMAGE is None or CURRENT_IMAGE >= len(file_list): CURRENT_IMAGE = 0 load_image() def advance_frame(): """Advance the frame""" # pylint: disable=global-statement global CURRENT_FRAME, CURRENT_LOOP CURRENT_FRAME = CURRENT_FRAME + 1 if CURRENT_FRAME >= FRAME_COUNT: CURRENT_FRAME = 0 CURRENT_LOOP = CURRENT_LOOP + 1 sprite_group[0][0] = CURRENT_FRAME def load_list_image(item): """Load the list item""" global CURRENT_IMAGE CURRENT_IMAGE = item load_image() def load_tipsy(): """Load the .bmp image""" load_list_image(1) def play_thankyou(): """load the thank you image""" load_list_image(0) while CURRENT_LOOP <= AUTO_ADVANCE_LOOPS: advance_frame() time.sleep(FRAME_DURATION) advance_image() while True: if sensor.range < THRESHOLD: play_thankyou() else: load_tipsy()
Pixel Art Specs
If you want to create your own artwork for display, these are the specifications to follow.
For the still .bmp image:
- Images should be a maximum of 64 pixels high
- Images can be up to 32 pixels wide
- Colors are 16-bit or 24-bit RGB
- Save files as .bmp format
- Since the orientation of the tip jar is vertical rather than horizontal, rotate your image 90% clockwise before uploading it.
We've found that crisp images (not too much anti-aliasing) work best.
We have a whole page on Pixel Art Fundamentals here!
For the Sprite Sheet:
A Sprite Sheet is like a filmstrip: a vertical series of animation frames saved as a single .bmp. Our code will parse this into animation frames and play them back at the speed you specify.
Check out this guide for detailed instructions on building your own a sprite sheet with Aseprite.
import time import os import board import busio import displayio from digitalio import DigitalInOut, Pull from adafruit_matrixportal.matrix import Matrix from adafruit_debouncer import Debouncer import adafruit_vl6180x
Next, we set up the sensor:
# Create I2C bus. i2c = busio.I2C(board.SCL, board.SDA) # Create sensor instance. sensor = adafruit_vl6180x.VL6180X(i2c)
Variables
Then, we'll set the variables for DEFAULT_FRAME_DURATION
(how quickly we move through the sprite sheet) and ANIMATION_DURATION
(how long each animation stays on the screen). AUTO_ADVANCE_LOOPS
determines how many times your sprite sheet plays through. And we'll set up the path to the image folders.
The THRESHOLD
variable is the distance in mm between your sensor and the edge of the money slot. If your tip jar isn't reacting, try increasing this number a bit. Or, open your serial monitor in the Mu editor for a readout from the sensor to see exactly what readings you're getting.
SPRITESHEET_FOLDER = "/bmps" DEFAULT_FRAME_DURATION = 0.1 # 100ms ANIMATION_DURATION = 5 AUTO_ADVANCE_LOOPS = 1 THRESHOLD = 20
Display/Pin Setup
Next, we set up the display and the pins used for the buttons. We aren't using the buttons for anything in this code sample, but they are set up for you in case you want to add button functionality.
# --- Display setup --- matrix = Matrix(bit_depth=4) sprite_group = displayio.Group() matrix.display.root_group = sprite_group # --- Button setup --- pin_down = DigitalInOut(board.BUTTON_DOWN) pin_down.switch_to_input(pull=Pull.UP) button_down = Debouncer(pin_down) pin_up = DigitalInOut(board.BUTTON_UP) pin_up.switch_to_input(pull=Pull.UP) button_up = Debouncer(pin_up)
Functions
Next we'll set up our functions. These will govern the loading and playback of the images.
def load_image(): """ Load an image as a sprite """ # pylint: disable=global-statement global CURRENT_FRAME, CURRENT_LOOP, FRAME_COUNT, FRAME_DURATION while sprite_group: sprite_group.pop() filename = SPRITESHEET_FOLDER + "/" + file_list[CURRENT_IMAGE] # CircuitPython 6 & 7 compatible bitmap = displayio.OnDiskBitmap(open(filename, "rb")) sprite = displayio.TileGrid( bitmap, pixel_shader=getattr(bitmap, 'pixel_shader', displayio.ColorConverter()), tile_width=bitmap.width, tile_height=matrix.display.height, ) # # CircuitPython 7+ compatible # bitmap = displayio.OnDiskBitmap(filename) # sprite = displayio.TileGrid( # bitmap, # pixel_shader=bitmap.pixel_shader, # tile_width=bitmap.width, # tile_height=matrix.display.height, # ) sprite_group.append(sprite) FRAME_COUNT = int(bitmap.height / matrix.display.height) FRAME_DURATION = DEFAULT_FRAME_DURATION CURRENT_FRAME = 0 CURRENT_LOOP = 0 def advance_image(): """ Advance to the next image in the list and loop back at the end """ # pylint: disable=global-statement global CURRENT_IMAGE if CURRENT_IMAGE is not None: CURRENT_IMAGE += 1 if CURRENT_IMAGE is None or CURRENT_IMAGE >= len(file_list): CURRENT_IMAGE = 0 load_image() def advance_frame(): """Advance the frame""" # pylint: disable=global-statement global CURRENT_FRAME, CURRENT_LOOP CURRENT_FRAME = CURRENT_FRAME + 1 if CURRENT_FRAME >= FRAME_COUNT: CURRENT_FRAME = 0 CURRENT_LOOP = CURRENT_LOOP + 1 sprite_group[0][0] = CURRENT_FRAME def load_list_image(item): """Load the list item""" global CURRENT_IMAGE CURRENT_IMAGE = item load_image() def load_tipsy(): """Load the .bmp image""" load_list_image(1) def play_thankyou(): """load the thank you image""" load_list_image(0) while CURRENT_LOOP <= AUTO_ADVANCE_LOOPS: advance_frame() time.sleep(FRAME_DURATION)
Main Loop
Since we did most of the heavy lifting in the functions, our main loop is really simple. Check the sensor and see if the THRESHOLD
value has been triggered (if someone puts a dollar into the jar). If so, the Thank You animation will play AUTO_ADVANCE_LOOPS
times through, then it will reset to playing the tipsy image.
while True: if sensor.range < THRESHOLD: play_thankyou() else: load_tipsy()
Uploading & Testing
Save your code to the CIRCUITPY drive on your computer as code.py, at the root of the CIRCUITPY drive. The code will start to run automatically.
If you see a "blinka" error message, here are some things to try:
- Have you installed all the libraries?
- Are you running the latest version of CircuitPython?
- Are your .bmp images uploaded to a folder called /bmps?
If it's still not working, check out this guide for more troubleshooting tips.
Text editor powered by tinymce.