Once you've finished setting up your RP2040 Prop-Maker Feather with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2023 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import random
import board
import audiocore
import audiobusio
import audiomixer
import pwmio
from digitalio import DigitalInOut, Direction
import neopixel
from adafruit_motor import servo
from adafruit_ticks import ticks_ms, ticks_add, ticks_diff
from adafruit_led_animation.animation.pulse import Pulse
from adafruit_led_animation.color import RED, BLACK, GREEN
import adafruit_display_text.label
import displayio
import framebufferio
import rgbmatrix
import terminalio
import adafruit_vl53l4cd
distance_trigger = 90 # cm
text="Here lies Fred"
text_color = 0xff0000
# how often to check for a new trigger from ToF
pause_time = 30 # seconds
# speed for scrolling the text on the matrix
scroll_time = 0.1 # seconds
displayio.release_displays()
# enable external power pin
# provides power to the external components
external_power = DigitalInOut(board.EXTERNAL_POWER)
external_power.direction = Direction.OUTPUT
external_power.value = True
i2c = board.I2C()
vl53 = adafruit_vl53l4cd.VL53L4CD(i2c)
vl53.inter_measurement = 0
vl53.timing_budget = 200
matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=4,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.D25, board.D24, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
line1 = adafruit_display_text.label.Label(
terminalio.FONT,
color=text_color,
text=text)
line1.x = 1
line1.y = 14
def scroll(line):
line.x = line.x - 1
line_width = line.bounding_box[2]
if line.x < -line_width:
line.x = display.width
g = displayio.Group()
g.append(line1)
display.root_group = g
wavs = []
for filename in os.listdir('/tomb_sounds'):
if filename.lower().endswith('.wav') and not filename.startswith('.'):
wavs.append("/tomb_sounds/"+filename)
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True)
mixer.voice[0].level = 1
audio.play(mixer)
wav_length = len(wavs) - 1
def open_audio(num):
n = wavs[num]
f = open(n, "rb")
w = audiocore.WaveFile(f)
return w
PIXEL_PIN = board.EXTERNAL_NEOPIXELS
BRIGHTNESS = 0.3
NUM_PIXELS = 2
PIXELS = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, auto_write=True)
pulse = Pulse(PIXELS, speed=0.05, color=RED, period=3)
COLORS = [RED, GREEN, BLACK]
SERVO_PIN = board.EXTERNAL_SERVO
PWM = pwmio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50)
SERVO = servo.Servo(PWM)
SERVO.angle = 0
clock = ticks_ms()
the_time = 5000
x = 0
scroll_clock = ticks_ms()
scroll_time = int(scroll_time * 1000)
pause_clock = ticks_ms()
pause_time = pause_time * 1000
pause = False
vl53.start_ranging()
while True:
vl53.clear_interrupt()
if vl53.distance < distance_trigger:
if not pause:
print("Distance: {} cm".format(vl53.distance))
SERVO.angle = 90
wave = open_audio(random.randint(0, wav_length))
mixer.voice[0].play(wave)
while mixer.playing:
pulse.color = COLORS[x]
pulse.animate()
if ticks_diff(ticks_ms(), scroll_clock) >= scroll_time:
scroll(line1)
display.refresh(minimum_frames_per_second=0)
scroll_clock = ticks_add(scroll_clock, scroll_time)
x = (x + 1) % 2
pause = True
print("paused")
pause_clock = ticks_add(pause_clock, pause_time)
else:
if ticks_diff(ticks_ms(), pause_clock) >= pause_time:
print("back to sensing")
pause = False
print("still paused")
if ticks_diff(ticks_ms(), scroll_clock) >= scroll_time:
print("Distance: {} cm".format(vl53.distance))
scroll(line1)
display.refresh(minimum_frames_per_second=0)
scroll_clock = ticks_add(scroll_clock, scroll_time)
SERVO.angle = 0
pulse.color = COLORS[2]
pulse.animate()
Upload the Code and Libraries to the RP2040 Prop-Maker Feather
After downloading the Project Bundle, plug your RP2040 Prop-Maker Feather into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the RP2040 Prop-Maker Feather's CIRCUITPY drive.
- lib folder
- tomb_sounds folder
- code.py
Your RP2040 Prop-Maker Feather CIRCUITPY drive should look like this after copying the lib folder, tomb_sounds folder and the code.py file.
How the CircuitPython Code Works
At the top of the code are a few parameters that you can modify to customize the tombstone. distance_trigger is the distance in centimeters where the time of flight sensor will trigger. text is the text that will scroll across the RGB matrix. text_color is the color of the scrolling text. pause_time is the time in seconds that the time of flight sensor will delay after being initially triggered. scroll_time is the speed in seconds that the text scrolls across the RGB matrix.
distance_trigger = 90 # cm text="Here lies Fred" text_color = 0xff0000 # how often to check for a new trigger from ToF pause_time = 30 # seconds # speed for scrolling the text on the matrix scroll_time = 0.1 # seconds
Time of Flight and RGB Matrix Setup
The time of flight sensor is instantiated over I2C, and the RGB matrix object is set up to use the FeatherWing pinout for the RP2040.
i2c = board.I2C()
vl53 = adafruit_vl53l4cd.VL53L4CD(i2c)
vl53.inter_measurement = 0
vl53.timing_budget = 200
matrix = rgbmatrix.RGBMatrix(
width=64, height=32, bit_depth=4,
rgb_pins=[board.D6, board.D5, board.D9, board.D11, board.D10, board.D12],
addr_pins=[board.D25, board.D24, board.A3, board.A2],
clock_pin=board.D13, latch_pin=board.D0, output_enable_pin=board.D1)
DisplayIO on the RGB Matrix
The matrix is passed as a FramebufferDisplay so that displayio can be used with the RGB matrix. The text is created as a Label. The scroll function moves the text by one pixel at a time across the display.
display = framebufferio.FramebufferDisplay(matrix, auto_refresh=True)
line1 = adafruit_display_text.label.Label(
terminalio.FONT,
color=text_color,
text=text)
line1.x = 1
line1.y = 14
def scroll(line):
line.x = line.x - 1
line_width = line.bounding_box[2]
if line.x < -line_width:
line.x = display.width
g = displayio.Group()
g.append(line1)
display.root_group = g
Sound Effects
The audio files in the /tomb_sounds folder are added to the wavs list. If you change or add more sound effects to the folder, they will be added to the list with no additional modification in the code. Audio playback is handled with the I2S amp on the Feather. Playback is routed through a Mixer object to allow for software volume control. The open_audio function takes an index location for a wave file in the wavs list and preps it for playback.
wavs = []
for filename in os.listdir('/tomb_sounds'):
if filename.lower().endswith('.wav') and not filename.startswith('.'):
wavs.append("/tomb_sounds/"+filename)
audio = audiobusio.I2SOut(board.I2S_BIT_CLOCK, board.I2S_WORD_SELECT, board.I2S_DATA)
mixer = audiomixer.Mixer(voice_count=1, sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True)
mixer.voice[0].level = 1
audio.play(mixer)
wav_length = len(wavs) - 1
def open_audio(num):
n = wavs[num]
f = open(n, "rb")
w = audiocore.WaveFile(f)
return w
PIXEL_PIN = board.EXTERNAL_NEOPIXELS BRIGHTNESS = 0.3 NUM_PIXELS = 2 PIXELS = neopixel.NeoPixel(PIXEL_PIN, NUM_PIXELS, auto_write=True) pulse = Pulse(PIXELS, speed=0.05, color=RED, period=3) COLORS = [RED, GREEN, BLACK] SERVO_PIN = board.EXTERNAL_SERVO PWM = pwmio.PWMOut(SERVO_PIN, duty_cycle=2 ** 15, frequency=50) SERVO = servo.Servo(PWM) SERVO.angle = 0
Time Keeping
The ticks library is used for time tracking without blocking in the loop. The scroll_clock handles the delay for scrolling the text, and the pause_clock adds a delay for triggering the time of flight sensor. ticks uses milliseconds for time keeping. The pause_time and scroll_time variables at the top of the code are multiplied by 1000 to convert them from seconds to milliseconds.
x = 0 scroll_clock = ticks_ms() scroll_time = int(scroll_time * 1000) pause_clock = ticks_ms() pause_time = pause_time * 1000 pause = False
The Loop
In the loop, if the time of flight sensor is triggered, then the servo moves to 90 degrees, and a randomized audio file from the wavs array is played through the speaker. While the audio is playing, the NeoPixels will pulse in either red or green. The x variable allows for switching between the two colors each time the time of flight sensor is triggered.
The pause_clock is used to avoid multiple triggers back to back in case someone is standing in front of the tombstone for a while. When the time of flight sensor is initially triggered, pause is set to True. It is not reset to False until the pause_clock is greater than the pause_time delay.
vl53.clear_interrupt()
if vl53.distance < distance_trigger:
if not pause:
print("Distance: {} cm".format(vl53.distance))
SERVO.angle = 90
wave = open_audio(random.randint(0, wav_length))
mixer.voice[0].play(wave)
while mixer.playing:
pulse.color = COLORS[x]
pulse.animate()
if ticks_diff(ticks_ms(), scroll_clock) >= scroll_time:
scroll(line1)
display.refresh(minimum_frames_per_second=0)
scroll_clock = ticks_add(scroll_clock, scroll_time)
x = (x + 1) % 2
pause = True
print("paused")
pause_clock = ticks_add(pause_clock, pause_time)
else:
if ticks_diff(ticks_ms(), pause_clock) >= pause_time:
print("back to sensing")
pause = False
print("still paused")
The scroll_clock allows the matrix to scroll the text by one pixel without blocking the other aspects of the code. Every time the scroll_time passes, the scroll function is called and moves the text by one pixel to the left.
if ticks_diff(ticks_ms(), scroll_clock) >= scroll_time:
print("Distance: {} cm".format(vl53.distance))
scroll(line1)
scroll_clock = ticks_add(scroll_clock, scroll_time)
The servo and NeoPixels are reset after being triggered by the time of flight sensor. The servo is moved back to 0 degrees, and the NeoPixel color is changed to BLACK or off.
SERVO.angle = 0 pulse.color = COLORS[2] pulse.animate()
Page last edited October 29, 2025
Text editor powered by tinymce.