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
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 boot.py and code.py 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 = usb_cdc.data encoder = rotaryio.IncrementalEncoder(board.ROTA, board.ROTB) button = digitalio.DigitalInOut(board.SWITCH) button.switch_to_input(pull=digitalio.Pull.UP) 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 print(button.value) 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 boot.py 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 pygame_player.py and animatedgif.py 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:
wget https://github.com/adafruit/Adafruit_Learning_System_Guides/raw/main/Raspberry_Pi_Animated_Gif_Player/pygame_player.py wget https://github.com/adafruit/Adafruit_Learning_System_Guides/raw/main/Raspberry_Pi_Animated_Gif_Player/animatedgif.py
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:
python3 pygame_player.py
# 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( detect_port(), 9600, parity="N", rtscts=False, xonxoff=False, exclusive=True, ) 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 += port.read(port.in_waiting) while ( self._running and not len(self._incoming_packet) and self._incoming_packet.decode().find(",") ): self._incoming_packet += port.read(port.in_waiting) self.check_pygame_events() 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 self.display.blit( pygame.image.fromstring(pilImage.tobytes(), pilImage.size, pilImage.mode), (0, 0), ) pygame.display.flip() pygame.init() pygame.mouse.set_visible(False) screen = pygame.display.set_mode((0, 0), pygame.FULLSCREEN) gif_player = PyGameAnimatedGif(screen, include_delays=False, folder="images") pygame.mouse.set_visible(True) port.close()
# 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: self.load_files(folder) self.run() 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 = Image.open(gif_folder + 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 = Image.open(self._gif_files[self._index]) print("Loading {}...".format(self._gif_files[self._index])) if "duration" in image.info: self._duration = image.info["duration"] else: self._duration = 0 if "loop" in image.info: self._loop = image.info["loop"] else: self._loop = 1 self._frame_count = image.n_frames self._frames.clear() for frame in range(self._frame_count): image.seek(frame) # 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 image.info: frame_object.duration = image.info["duration"] frame_object.image = ImageOps.pad( # pylint: disable=no-member image.convert("RGB"), (self._width, self._height), method=Image.NEAREST, color=(0, 0, 0), centering=(0.5, 0.5), ) self._frames.append(frame_object) def play(self): self.preload() 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 self.update_display(self._frames[current_frame].image) while self._running: action = self.get_next_value() if action: if not last_action: last_action = action if action == "click": self.advance() return False elif int(action) < int(last_action): current_frame -= 1 else: current_frame += 1 current_frame %= self._frame_count frame_object = self._frames[current_frame] start_time = time.monotonic() self.update_display(frame_object.image) if self.include_delays: remaining_delay = frame_object.duration / 1000 - ( time.monotonic() - start_time ) if remaining_delay > 0: time.sleep(remaining_delay) last_action = action if self._loop == 1: return True if self._loop > 0: self._loop -= 1 def run(self): while self._running: auto_advance = self.play() if auto_advance: self.advance()
Text editor powered by tinymce.