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

How it Works

Libraries

Here's how the code works. First we import the libraries:

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.

This guide was first published on Nov 24, 2020. It was last updated on Oct 28, 2020.

This page (Code with CircuitPython) was last updated on Dec 02, 2023.

Text editor powered by tinymce.