This one is a bit more complicated because it uses 2 devices running Python that need to communicate with each other through serial.

How it works

This player is in two parts. It uses a Rotary Trinkey running CircuitPython to receive the input and translate it into serial data and the second part runs on the Raspberry Pi and reads that serial data and decodes that to the original inputs and uses that to determine the when to advance the animated Gif frames. This is the same technique used in the MacroPad Remote Procedure Calls over USB to Control Home Assistant guide.

For the Rotary Trinkey, the code is very similar to the standard example code, but with a few key differences. It will communicate using a second CDC_Data serial port so that unnecessary characters are not added to the stream by CircuitPython. It will also print out the word "click" when the button is pressed and everything is separated by commas instead of printing onto a new line.

For the Python program, it starts off by detecting the serial port that the Rotary Trinkey is connected to and initializing it with PySerial. Once it is connected, PyGame is initialized in fullscreen mode and the mouse cursor is hidden. It then preloads any Gif images found in the folder and stores them in a list. It starts off with the first image in whatever order the os module returns the files in and loads up the first one.

Once an image is loaded, it waits for data and depending on the direction you turn the know it will either go forward or backwards in frames. A modulus operator is used to keep the current frame value within the index boundaries. Once the frame is extracted with Pillow, it is loaded into PyGame as a surface using the blit() command and the screen is updated with the flip() command.


Wiring is super easy on this player because all you need to do is connect the Rotary Trinkey to any of the USB ports. We recommend using a USB extension if you have the Display mounted on a stand because the orientation of the Raspberry Pi causes the Trinkey to only be accessible from the rear.

If you bought the SmartiPi Pro case, you can find assembly instructions on their website.

Rotary Trinkey Setup

You'll want to put the latest version of CircuitPython onto the Rotary Trinkey first. If you're not sure how to do it, you can refer to CircuitPython installation page of the Adafruit Rotary Trinkey guide.

For this project, there are no libraries needed. Everything is built into CircuitPython itself.

You will need to copy both the and files onto the Trinkey, in the root of the CIRCUITPY drive that should show up. You can download them by clicking on the Download Project Bundle below.

# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT

import usb_cdc
import rotaryio
import board
import digitalio

serial =
encoder = rotaryio.IncrementalEncoder(board.ROTA, board.ROTB)
button = digitalio.DigitalInOut(board.SWITCH)

last_position = None
button_state = False

while True:
    position = encoder.position
    if last_position is None or position != last_position:
        serial.write(bytes(str(position) + ",", "utf-8"))
    last_position = position
    if not button.value and not button_state:
        button_state = True
    if button.value and button_state:
        serial.write(bytes("click,", "utf-8"))
        button_state = False

The file has the setting sin it you need to enable the CDC Data Device. You can read more about it in our Customizing USB Devices in CircuitPython guide.

# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT

import usb_cdc

usb_cdc.enable(console=True, data=True)

Raspberry Pi Setup

You will want to start by installing the latest version of the Raspberry Pi Desktop onto an SD card. There's a nice utility available from Raspberry Pi called the Raspberry Pi Imager that makes installing images very easy. You can also refer to the CircuitPython Libraries on Linux and Raspberry Pi guide for more help setting it up.

Once you have everything set up, you will need to open a terminal and install Blinka. We recommend referring to the Installing CircuitPython Libraries on Raspberry Pi page of that guide to quickly get up and running.

Install Required Libraries

You will need to have a few libraries installed before the script will run on your computer.

Install Adafruit_Board_Toolkit:

pip3 install adafruit-board-toolkit

Install PySerial next:

pip3 install pyserial

Install PyGame next:

pip3 install pygame

DejaVu TTF Font

Raspberry Pi usually comes with the DejaVu font already installed, but in case it didn't, you can run the following to install it:

sudo apt-get install fonts-dejavu

This package was previously calls ttf-dejavu, so if you are running an older version of Raspberry Pi OS, it may be called that.

Pillow Library

We also need PIL, the Python Imaging Library, to allow graphics and using text with custom fonts. There are several system libraries that PIL relies on, so installing via a package manager is the easiest way to bring in everything:

sudo apt-get install python3-pil

Download the Code

Copy and onto the Raspberry Pi. You can either copy them out of the bundle that you downloaded in the Rotary Trinkey Setup step or you can use wget to copy them right off the web into your current folder:


Run the Script

The code must be run from a terminal window on the Pi itself and not from an SSH session. This is because PyGame will try and create a window on the current device you are typing from. Just run it using the following command:


Full Player Code

# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT

This example is for use on (Linux) computers that are using CPython with
Adafruit Blinka to support CircuitPython libraries. CircuitPython does
not support PIL/pillow (python imaging library)!

Author(s): Melissa LeBlanc-Williams for Adafruit Industries
import serial
import adafruit_board_toolkit.circuitpython_serial
from animatedgif import AnimatedGif
import pygame

port = None

def detect_port():
    Detect the port automatically
    comports = adafruit_board_toolkit.circuitpython_serial.data_comports()
    ports = [comport.device for comport in comports]
    if len(ports) >= 1:
        if len(ports) > 1:
            print("Multiple devices detected, using the first detected port.")
        return ports[0]
    raise RuntimeError(
        "Unable to find any CircuitPython Devices with the CDC data port enabled."

port = serial.Serial(

class PyGameAnimatedGif(AnimatedGif):
    def __init__(self, display, include_delays=True, folder=None):
        self._width, self._height = pygame.display.get_surface().get_size()
        self._incoming_packet = b""
        super().__init__(display, include_delays=include_delays, folder=folder)

    def get_next_value(self):
        if not port:
            return None
        while port.in_waiting:
            self._incoming_packet +=
        while (
            and not len(self._incoming_packet)
            and self._incoming_packet.decode().find(",")
            self._incoming_packet +=

        all_values = self._incoming_packet.decode().split(",")
        value = all_values.pop(0)
        self._incoming_packet = ",".join(all_values).encode("utf-8")
        return value

    def check_pygame_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self._running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self._running = False

    def update_display(self, image):
        pilImage = image
            pygame.image.fromstring(pilImage.tobytes(), pilImage.size, pilImage.mode),
            (0, 0),

screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
gif_player = PyGameAnimatedGif(screen, include_delays=False, folder="images")
# SPDX-FileCopyrightText: 2021 Melissa LeBlanc-Williams for Adafruit Industries
# SPDX-License-Identifier: MIT

import os
import time
from PIL import Image, ImageOps

# pylint: disable=too-few-public-methods
class Frame:
    def __init__(self, duration=0):
        self.duration = duration
        self.image = None

# pylint: enable=too-few-public-methods

class AnimatedGif:
    def __init__(self, display, include_delays=True, folder=None):
        self._frame_count = 0
        self._loop = 0
        self._index = 0
        self._duration = 0
        self._gif_files = []
        self._frames = []
        self._running = True
        self.display = display
        self.include_delays = include_delays
        if folder is not None:

    def advance(self):
        self._index = (self._index + 1) % len(self._gif_files)

    def back(self):
        self._index = (self._index - 1 + len(self._gif_files)) % len(self._gif_files)

    def load_files(self, folder):
        gif_files = [f for f in os.listdir(folder) if f.endswith(".gif")]
        gif_folder = folder
        if gif_folder[:-1] != "/":
            gif_folder += "/"
        for gif_file in gif_files:
            image = + gif_file)
            # Only add animated Gifs
            if image.is_animated:
                self._gif_files.append(gif_folder + gif_file)

        print("Found", self._gif_files)
        if not self._gif_files:
            print("No Gif files found in current folder")
            exit()  # pylint: disable=consider-using-sys-exit

    def preload(self):
        image =[self._index])
        print("Loading {}...".format(self._gif_files[self._index]))
        if "duration" in
            self._duration =["duration"]
            self._duration = 0
        if "loop" in
            self._loop =["loop"]
            self._loop = 1
        self._frame_count = image.n_frames
        for frame in range(self._frame_count):
            # Create blank image for drawing.
            # Make sure to create image with mode 'RGB' for full color.
            frame_object = Frame(duration=self._duration)
            if "duration" in
                frame_object.duration =["duration"]
            frame_object.image = ImageOps.pad(  # pylint: disable=no-member
                (self._width, self._height),
                color=(0, 0, 0),
                centering=(0.5, 0.5),

    def play(self):
        current_frame = 0
        last_action = None
        # Check if we have loaded any files first
        if not self._gif_files:
            print("There are no Gif Images loaded to Play")
            return False
        while self._running:
            action = self.get_next_value()
            if action:
                if not last_action:
                    last_action = action
                if action == "click":
                    return False
                elif int(action) < int(last_action):
                    current_frame -= 1
                    current_frame += 1
                current_frame %= self._frame_count
                frame_object = self._frames[current_frame]
                start_time = time.monotonic()
                if self.include_delays:
                    remaining_delay = frame_object.duration / 1000 - (
                        time.monotonic() - start_time
                    if remaining_delay > 0:
                last_action = action
                if self._loop == 1:
                    return True
                if self._loop > 0:
                    self._loop -= 1

    def run(self):
        while self._running:
            auto_advance =
            if auto_advance:

This guide was first published on Sep 28, 2021. It was last updated on Sep 28, 2021.

This page (PyGame Player) was last updated on Sep 25, 2023.

Text editor powered by tinymce.