Tree ornaments come in all shapes and sizes, and often include a personalized graphic or design for a specific person.
This project runs with this concept by building a digital tree ornament that features a person's face bouncing around on screen.
Build this ornament as a gift for a specific person, to add an interactive element to their tree that is eye-catching and fun to watch.
How It WorksThe ornament consists of electronics housed in a 3D-printed case, which can be easily hung from the branch of a Christmas tree.
An Adafruit Feather RP 2040 works as the main processor for the device, responsible for storing and running the code that controls the display.
Custom image animations appear on the ornament's 1.8-inch Adafruit TFT screen, which has a resolution of 128x160 pixels in full color.
A 3.7V LiPo battery plugs into the Feather to power the board, and uses a button switch to toggle the power on and off.
These electronics are wired together and mounted within the front and back pieces of the 3D-printed case. The two halves of the case pressure-fit together to form the fully assembled ornament.
A wire hook fits through the hook loop on top of the case, allowing the ornament to be hung from any tree branch.
When powered on, the ornament executes custom code to animate the personalized face icons that have been uploaded to the board.
Three face icons move continuously across the display, bouncing off the edges as they travel. The animation continues as long as the ornament remains powered on.
To recharge the ornament, connect a USB cable to the board's port, which is accessible through a hole in the case bottom.
Build InstructionsBelow are instructions on how to prototype and assemble the personalized tree ornament. These instructions assume you are familiar with the Adafruit Feather RP2040 and CircuitPython.
The complete project code and 3D print files are found within this project's attachments.
Board ConfigurationTo execute this project's CircuitPython code, the Feather RP2040 must first be configured appropriately.
Connect the Feather RP2040 to a computer via a USB cable.
This connection powers the board and opens file access to the board's onboard storage drive, listed as CIRCUITPYTHON
on the connected computer.
1. Install CircuitPython version 9.x
Multiple versions of CircuitPython exist, and the project code requires version 9.x. Follow the Adafruit CircuitPython Installation Guide to install version 9.x onto the RP2040.
2. Install the TFT display library
An additional code library is required to interface with the ornament's display. Follow the Adafruit CircuitPython Libraries Guide for instructions on how to install libraries, and install the adafruit_st7735r
library onto the RP2040.
A personalized face image must be created and uploaded to the board in order to be displayed and animated on the ornament.
Use the free image-editing software GIMP to crop, resize, and format a face image to meet the image specifications required by the project code.
1. Open the reference image in GIMP
With GIMP open, click File -> Open and select an image that features the personalized subject's face.
2. Crop the image
Within the tools, click the Rectangle Select tool. Check the Fixed option, and select Aspect Ratio from the dropdown. Enter 1:1.5 as the ratio value.
Drag a rectangular selection around the face within the image, resizing and positioning it so that it is tightly bordering the face.
Click Image -> Crop to Selection to crop the image to just the face.
3. Isolate the face
Erase any background image content and isolate the face against a black background. This step ensures the face icon will have a black border that blends seamlessly into the display's black background.
From the menu, select Layer -> Transparency -> Add Alpha Channel to add a transparent channel to the image.
Next, click the Color Picker and select Black as the foreground color. Select Layer -> New Layer, and for Fill select Foreground Color. Click OK.
Within the Layers menu, select the newly created layer and drag it below the original image layer, then re-select the original image layer.
From the tools, select the Eraser Tool and carefully erase all of the image content surrounding the face, exposing the black background underneath.
Finally, select Layer -> Merge Down from the menu to combine the image and background layers into a single layer.
4. Resize the image
Resize the image to the project's required dimensions by selecting Image -> ScaleImage from the menu.
Set the Width value to 33 and the Height value to 50, and click Apply to perform the scaling.
5. Format Image Colors
Create a transparency key by setting the image background to a unique color and making it the first color in the image colormap. When displayed, the project code will make all pixels matching this color appear transparent.
Click the Color Picker tool and select a bright green color, then use the Pencil Tool to color all of the background pixels this color.
From the menu, select Image -> Mode -> Indexed. In the Colormap menu, select Generate optimum palette and set the Maximum number of colors to 255, then click Convert.
Next, select Colors -> Map -> Set Colormap. Click and drag the green background color to the first position in the mapping, and click OK.
6. Export Image as Bitmap
CircuitPython can only load and display images in bitmap format. Export the image in this format onto the board's filesystem, where it can be utilized by the project code.
With the RP2040 connected via USB, select File -> Export As from the menu.
Click the CIRCUITPYTHON drive within the file selector. Next, click Create Folder and create a new folder called img
. Save the image as face.bmp
within the newly created img
folder.
To help troubleshoot component connections and programming issues before final assembly, a prototype of the ornament is first built on a breadboard.
Place the device components on a breadboard and use jumper wires to make the connections seen in the following circuit diagram:
The prototype layout should resemble the following circuit:
The Feather board automatically executes the code.py
file located in its main directory each time it powers on. This file needs to be edited to contain the project code.
Follow the below steps to build the code that powers the ornament. The complete code is also included in this project's attachments.
1. With the board connected to the computer via USB, open code.py
from the CIRCUITPYTHON drive in a code editor such as Mu.
2. Delete any existing lines of code from this file so that it is empty.
3. Add the following lines of code to the top of the file:
import time
import random
import board
import displayio
import pwmio
import adafruit_st7735r
Here the code's necessary CircuitPython libraries are imported, providing additional code for interfacing with the board and the display.
4. Next, initialize a TFT display object:
# Initialze TFT display
displayio.release_displays()
spi = board.SPI()
tft_cs = board.D5
tft_dc = board.D6
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=board.D9)
display = adafruit_st7735r.ST7735R(display_bus, width=128, height=160, rotation=0, bgr=True)
display.auto_refresh = False
This code block creates an instance of the ST7735R
class, display
, which is used to control the TFT display. The display is set to refresh manually.
3. Set the brightness of the display with the following:
# Change backlight brightness
brightness = 0.3
duty_cycle = int(brightness * 65535)
frequency = 500
backlight_pwm = pwmio.PWMOut(board.D10, duty_cycle=duty_cycle, frequency=frequency)
Display brightness is controlled by a PWM pin connected to the display's LED pin. Here, the brightness is set to 30% of full brightness.
4. Define the face icon settings:
# Icon settings
icon_fname = 'img/face.bmp'
icon_height = 50
icon_width = 33
The file path and dimension values of the previously created face icon are defined here.
5. Create the display group:
# Create main display groups
main_group = displayio.Group()
display.root_group = main_group
Displayio is a CircuitPython library for drawing images to displays. It uses groups to organize and manage image elements.
These lines create a main_group
and sets it as the root group of the display
, allowing icon elements to be added to it and displayed.
6. Define a bounding icon class:
# Define bouncing icon class
class BouncingIcon:
def __init__(self, fname, x, y, speed_x, speed_y):
image = displayio.OnDiskBitmap(fname)
image.pixel_shader.make_transparent(0)
self.icon = displayio.TileGrid(
image,
pixel_shader=image.pixel_shader,
x=x,
y=y
)
main_group.append(self.icon)
self.speed_x = speed_x
self.speed_y = speed_y
To display multiple animated icons, each icon needs its own image data, screen position, and movement speed.
These properties are bundled together in the BouncingIcon
class, which manages an icon's loaded image content along with its current position and velocity values.
An instance of this class is initialized with an image file path (fname
), initial coordinate values (x
, y
), and velocity values (speed_x
, speed_y
).
The class creates an OnDiskBitmap
object to load the image from the file and sets the image's top-left pixel color as the transparent key.
It then creates a TileGrid
object from the image and positions it at the specified coordinates. This TileGrid is stored as the object's icon attribute and added to the display's main_group
.
The velocity values are stored for future animation updates.
7. Next, initialize the bouncing icons:
# Create bouncing icon instances
icons = []
for i in range(3):
icons.append(BouncingIcon(
icon_fname,
random.randint(0, display.width - icon_width),
random.randint(0, display.height - icon_height),
random.choice([-1, 1]),
random.choice([-1, 1])
))
An array icons
stores three instances of the BouncingIcon
class.
Each instance is initialized with the face image file path, randomized position values that fall within the display dimensions, and speed values that are randomly chosen as either -1 or 1.
8. Finally, define the script's main processing loop:
# Miain loop
while True:
# Update icon positions
for icon in icons:
# Update x/y values
icon.icon.x += icon.speed_x
icon.icon.y += icon.speed_y
# Bounce off the right and left edges
if icon.icon.x + icon_width >= display.width or icon.icon.x <= 0:
icon.speed_x = -icon.speed_x
# Bounce off the bottom and top edges
if icon.icon.y + icon_height >= display.height or icon.icon.y <= 0:
icon.speed_y = -icon.speed_y
# Refresh display
display.refresh()
This processing loop will run repeatedly as long as the board is powered on. On each iteration, the position of each icon is updated by adding its speed_x
and speed_y
values to the x
and y
coordinates of the icon
TileGrid.
When an icon reaches an edge of the display, its direction is reversed by negating the object's speed value in the direction of the edge.
The display refreshes after all icons are updated to show their new positions, creating an animation where the icons bounce around within the display.
9. Save the finalized code
After adding all of the code blocks from the previous steps into code.py
, save it to update the file within the Feather's filesystem.
With the updated code.py
saved on the board, the project code will automatically run and begin controlling the display on the prototype circuit.
Three bouncing face icons should appear on screen, and continue their bouncing animation as long as the board is powered.
If the prototype is not functioning as expected, there may be an issue with the components, their connections, or the CircuitPython script. Troubleshoot these pieces to get the prototype working before moving on to final assembly.
Ornament AssemblyAfter the prototype has been tested, its components can be assembled into the finished ornament design.
1. 3D print the ornament's case.
Parts for the case include a front and back piece. 3D model files (.stl) are provided in this project's attachments.
2. Solder the component connections
Remove any header pins from both the Feather RP2040 and the TFT display.
Following the circuit diagram, solder the required connections between the board and display using jumper wires. 30AWG wires are recommended due to their thinness and flexibility.
Use 30AWG wire to splice the toggle switch into the battery's ground wire.
3. Mount the components within the case
Insert the switch button through the hole in the side wall of the case. Apply glue between the button base and the wall to secure it in place.
Next, connect the battery to the Feather board's JST socket. Then place the battery underneath the board, and secure the board to the mounting holes within the case back using four M2.5x3 machine screws.
Finally, secure the display to the mounting holes within the case front using four M2.5x3 machine screws.
4. Assemble the case
Gently press the front and back halves of the case together, ensuring that the connection wires are not damaged. The two halves stay secured by a tight pressure fit between them.
A wire hook can be inserted through the hook loop on top of the case.
With these steps complete, the fully assembled device is ready to be hung from any tree branch and powered on.
Comments