Compare commits

...

14 commits

Author SHA1 Message Date
62ab21f4dc
remove unneeded workaround 2023-08-17 11:49:40 -05:00
Anne Barela
0f50d831a9
Merge pull request #2592 from makermelissa/main
Animated Message Board Working
2023-08-14 17:20:35 -04:00
Melissa LeBlanc-Williams
d00b87c851 Remove unused fonts 2023-08-14 14:14:36 -07:00
Melissa LeBlanc-Williams
3ca8f9a715 Animated Message Board Working 2023-08-14 14:05:12 -07:00
Anne Barela
b5eb25620a
Merge pull request #2591 from kattni/propmaker-featherwing-updates
Update PropMaker FeatherWing examples.
2023-08-14 11:24:31 -04:00
Kattni Rembor
2c5449756e Update PropMaker FeatherWing examples. 2023-08-14 11:15:24 -04:00
Anne Barela
d453e1f1e1
Merge pull request #2588 from adafruit/metro_rp2040_sd
Adding arduino Metro RP2040 SD card example
2023-08-14 09:32:06 -04:00
Liz
c8035738b4 updating again 2023-08-14 09:04:32 -04:00
Liz
329a3c9c3f test only 2023-08-14 08:39:16 -04:00
Liz
23a14e88b8 updating example to use sdfat fork 2023-08-14 08:33:12 -04:00
Limor "Ladyada" Fried
726c835078
Merge pull request #2590 from jedgarpark/ambient-machine
adjusted buffers
2023-08-13 19:35:35 -04:00
John Park
f9495e0736 adjusted buffers 2023-08-12 20:58:46 -07:00
Limor "Ladyada" Fried
16497b25c0
Merge pull request #2589 from ladyada/main
dox 4 tux
2023-08-12 20:16:15 -04:00
ladyada
b75b68d8dd dox 4 tux 2023-08-11 20:48:36 -04:00
25 changed files with 1316 additions and 69 deletions

View file

@ -4,47 +4,45 @@
/*
SD card read/write
This example shows how to read and write data to and from an SD card file
The circuit:
SD card attached to SPI bus as follows:
** MISO - pin 20
** MOSI - pin 19
** CS - pin 23
** SCK - pin 18
This example shows how to read and write data to and from an SD card file
The circuit:
* SD card attached to SPI0 bus as follows:
** MOSI - pin 19
** MISO - pin 20
** CLK - pin 18
created Nov 2010
by David A. Mellis
modified 9 Apr 2012
by Tom Igoe
created Nov 2010
by David A. Mellis
modified 9 Apr 2012
by Tom Igoe
modified 14 Feb 2023
by Liz Clark
This example code is in the public domain.
This example code is in the public domain.
*/
*/
// SPI0 pins for Metro RP2040
// Pins connected to onboard SD card slot
const int _MISO = 20;
const int _MOSI = 19;
const int _CS = 23;
const int _SCK = 18;
#include "SdFat.h"
SdFat sd;
#include <SPI.h>
#include <SD.h>
#define SD_FAT_TYPE 1
File myFile;
// default CS pin is 23 for Metro RP2040
#define SD_CS_PIN 23
File32 myFile;
void setup() {
// Open serial communications and wait for port to open:
Serial.begin(115200);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
Serial.print("Initializing SD card...");
// Ensure the SPI pinout the SD card is connected to is configured properly
SPI.setRX(_MISO);
SPI.setTX(_MOSI);
SPI.setSCK(_SCK);
if (!SD.begin(_CS)) {
if (!sd.begin(SD_CS_PIN)) {
Serial.println("initialization failed!");
return;
}
@ -52,11 +50,12 @@ void setup() {
// open the file. note that only one file can be open at a time,
// so you have to close this one before opening another.
myFile = SD.open("test.txt", FILE_WRITE);
myFile.open("test.txt", FILE_WRITE);
// if the file opened okay, write to it:
if (myFile) {
Serial.print("Writing to test.txt...");
myFile.println("testing 1, 2, 3.");
myFile.println("hello metro rp2040!");
// close the file:
myFile.close();
@ -67,7 +66,7 @@ void setup() {
}
// re-open the file for reading:
myFile = SD.open("test.txt");
myFile.open("test.txt");
if (myFile) {
Serial.println("test.txt:");

View file

@ -1,17 +1,30 @@
# SPDX-FileCopyrightText: 2019 Kattni Rembor for Adafruit Industries
# SPDX-FileCopyrightText: 2019, 2023 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""Simple rainbow swirl example for 3W LED"""
import time
import pwmio
import board
from rainbowio import colorwheel
import digitalio
enable = digitalio.DigitalInOut(board.D10)
enable.direction = digitalio.Direction.OUTPUT
enable.value = True
def colorwheel(pos):
if pos < 0 or pos > 255:
return 0, 0, 0
if pos < 85:
return int(255 - pos * 3), int(pos * 3), 0
if pos < 170:
pos -= 85
return 0, int(255 - pos * 3), int(pos * 3)
pos -= 170
return int(pos * 3), 0, int(255 - pos * 3)
red = pwmio.PWMOut(board.D11, duty_cycle=0, frequency=20000)
green = pwmio.PWMOut(board.D12, duty_cycle=0, frequency=20000)
blue = pwmio.PWMOut(board.D13, duty_cycle=0, frequency=20000)
@ -22,3 +35,4 @@ while True:
red.duty_cycle = int(r * 65536 / 256)
green.duty_cycle = int(g * 65536 / 256)
blue.duty_cycle = int(b * 65536 / 256)
time.sleep(0.05)

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2019 Kattni Rembor for Adafruit Industries
# SPDX-FileCopyrightText: 2019, 2023 Kattni Rembor for Adafruit Industries
#
# SPDX-License-Identifier: MIT
@ -6,16 +6,23 @@
# This example only works on Feathers that have analog audio out!
import digitalio
import board
import audioio
import audiocore
try:
from audioio import AudioOut
except ImportError:
try:
from audiopwmio import PWMAudioOut as AudioOut
except ImportError:
pass # not always supported by every board!
WAV_FILE_NAME = "StreetChicken.wav" # Change to the name of your wav file!
enable = digitalio.DigitalInOut(board.D10)
enable.direction = digitalio.Direction.OUTPUT
enable.value = True
with audioio.AudioOut(board.A0) as audio: # Speaker connector
with AudioOut(board.A0) as audio: # Speaker connector
wave_file = open(WAV_FILE_NAME, "rb")
wave = audiocore.WaveFile(wave_file)

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: MIT
# Ambient Machine inspired by Yuri Suzuki https://www.yurisuzuki.com/projects/the-ambient-machine
import os
import gc
import board
import busio
import audiocore
@ -26,8 +27,6 @@ for p in (8,9,10,11,12,4,3,2,1,0):
pin.switch_to_input(pull=Pull.UP)
switches.append(Debouncer(pin))
switch_states = [False] * 20 # list of switch states
wav_files = []
for filename in os.listdir('/samples/'): # on board flash
@ -36,62 +35,57 @@ for filename in os.listdir('/samples/'): # on board flash
print(filename)
wav_files.sort() # put in alphabetical/numberical order
gc.collect()
# Metro M7 pins for the I2S amp:
lck_pin, bck_pin, dat_pin = board.D9, board.D10, board.D12
audio = audiobusio.I2SOut(bit_clock=bck_pin, word_select=lck_pin, data=dat_pin)
mixer = audiomixer.Mixer(voice_count=len(wav_files), sample_rate=22050, channel_count=1,
bits_per_sample=16, samples_signed=True, buffer_size=8192)
bits_per_sample=16, samples_signed=True, buffer_size=2048)
audio.play(mixer)
for i in range(10): # start playing all wavs on loop w levels down
wave = audiocore.WaveFile(open(wav_files[i], "rb"))
wave = audiocore.WaveFile(wav_files[i], bytearray(1024))
mixer.voice[i].play(wave, loop=True)
mixer.voice[i].level = 0.0
LOW_VOL = 0.2
HIGH_VOL = 0.5
while True:
for i in range(len(switches)):
switches[i].update()
if i < 5: # first row plays five samples
switch_row = i // 5
if switch_row == 0: # first row plays five samples
if switches[i].fell:
if switch_states[i+5] is True: # check volume switch
mixer.voice[i].level = 0.4 # if up
if switches[i+5].value is False: # check vol switch (pull-down, so 'False' is 'on')
mixer.voice[i].level = HIGH_VOL # if up
else:
mixer.voice[i].level = 0.2 # if down
switch_states[i] = not switch_states[i]
mixer.voice[i].level = LOW_VOL # if down
if switches[i].rose:
mixer.voice[i].level = 0.0
switch_states[i] = not switch_states[i]
elif 4 < i < 10: # second row adjusts volume of first row
if switch_row == 1: # second row adjusts volume of first row
if switches[i].fell:
if switch_states[i-5] is True: # raise volume if it is on
mixer.voice[i-5].level = 0.4
switch_states[i] = not switch_states[i]
if switches[i-5].value is False: # raise volume if it is on
mixer.voice[i-5].level = HIGH_VOL
if switches[i].rose:
if switch_states[i-5] is True: # lower volume if it is on
mixer.voice[i-5].level = 0.2
switch_states[i] = not switch_states[i]
if switches[i-5].value is False: # lower volume if it is on
mixer.voice[i-5].level = LOW_VOL
elif 9 < i < 15: # third row plays five different samples
if switch_row == 2: # third row plays five different samples
if switches[i].fell:
if switch_states[i+5] is True:
mixer.voice[i-5].level = 0.4
if switches[i+5].value is False:
mixer.voice[i-5].level = HIGH_VOL
else:
mixer.voice[i-5].level = 0.2
switch_states[i] = not switch_states[i]
mixer.voice[i-5].level = LOW_VOL
if switches[i].rose:
mixer.voice[i-5].level = 0.0
switch_states[i] = not switch_states[i]
elif 14 < i < 20: # fourth row adjust volumes of third row
if switch_row == 3: # fourth row adjust volumes of third row
if switches[i].fell:
if switch_states[i-5] is True:
mixer.voice[i-10].level = 0.4
switch_states[i] = not switch_states[i]
if switches[i-5].value is False:
mixer.voice[i-10].level = HIGH_VOL
if switches[i].rose:
if switch_states[i-5] is True:
mixer.voice[i-10].level = 0.2
switch_states[i] = not switch_states[i]
if switches[i-5].value is False:
mixer.voice[i-10].level = LOW_VOL

View file

@ -139,9 +139,6 @@ class WrappedTextDisplay:
def refresh(self):
text = '\n'.join(self.lines[self.line_offset : self.line_offset + max_lines])
# Work around https://github.com/adafruit/Adafruit_CircuitPython_Display_Text/issues/183
while '\n\n' in text:
text = text.replace('\n\n', '\n \n')
terminal.text = text
board.DISPLAY.refresh()
wrapped_text_display = WrappedTextDisplay()

82
ESP32S3_Linux/Dockerfile Normal file
View file

@ -0,0 +1,82 @@
# Dockerfile port of https://gist.github.com/jcmvbkbc/316e6da728021c8ff670a24e674a35e6
# wifi details http://wiki.osll.ru/doku.php/etc:users:jcmvbkbc:linux-xtensa:esp32s3wifi
# we need python 3.10 not 3.11
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get -y install gperf bison flex texinfo help2man gawk libtool-bin git unzip ncurses-dev rsync zlib1g zlib1g-dev xz-utils cmake wget bzip2 g++ python3 python3-dev python3-pip cpio bc virtualenv libusb-1.0 && \
ln -s /usr/bin/python3 /usr/bin/python
WORKDIR /app
# install autoconf 2.71
RUN wget https://ftp.gnu.org/gnu/autoconf/autoconf-2.71.tar.xz && \
tar -xf autoconf-2.71.tar.xz && \
cd autoconf-2.71 && \
./configure --prefix=`pwd`/root && \
make && \
make install
ENV PATH="$PATH:/app/autoconf-2.71/root/bin"
# dynconfig
RUN git clone https://github.com/jcmvbkbc/xtensa-dynconfig -b original --depth=1 && \
git clone https://github.com/jcmvbkbc/config-esp32s3 esp32s3 --depth=1 && \
make -C xtensa-dynconfig ORIG=1 CONF_DIR=`pwd` esp32s3.so
ENV XTENSA_GNU_CONFIG="/app/xtensa-dynconfig/esp32s3.so"
# ct-ng cannot run as root, we'll just do everything else as a user
RUN useradd -d /app/build -u 3232 esp32 && mkdir build && chown esp32:esp32 build
USER esp32
# toolchain
RUN cd build && \
git clone https://github.com/jcmvbkbc/crosstool-NG.git -b xtensa-fdpic --depth=1 && \
cd crosstool-NG && \
./bootstrap && \
./configure --enable-local && \
make && \
./ct-ng xtensa-esp32s3-linux-uclibcfdpic && \
CT_PREFIX=`pwd`/builds ./ct-ng build || echo "Completed" # the complete ct-ng build fails but we still get what we wanted!
RUN [ -e build/crosstool-NG/builds/xtensa-esp32s3-linux-uclibcfdpic/bin/xtensa-esp32s3-linux-uclibcfdpic-gcc ] || exit 1
# kernel and rootfs
RUN cd build && \
git clone https://github.com/jcmvbkbc/buildroot -b xtensa-2023.02-fdpic --depth=1 && \
make -C buildroot O=`pwd`/build-xtensa-2023.02-fdpic-esp32s3 esp32s3wifi_defconfig && \
buildroot/utils/config --file build-xtensa-2023.02-fdpic-esp32s3/.config --set-str TOOLCHAIN_EXTERNAL_PATH `pwd`/crosstool-NG/builds/xtensa-esp32s3-linux-uclibcfdpic && \
buildroot/utils/config --file build-xtensa-2023.02-fdpic-esp32s3/.config --set-str TOOLCHAIN_EXTERNAL_PREFIX '$(ARCH)-esp32s3-linux-uclibcfdpic' && \
buildroot/utils/config --file build-xtensa-2023.02-fdpic-esp32s3/.config --set-str TOOLCHAIN_EXTERNAL_CUSTOM_PREFIX '$(ARCH)-esp32s3-linux-uclibcfdpic' && \
make -C buildroot O=`pwd`/build-xtensa-2023.02-fdpic-esp32s3
RUN [ -f build/build-xtensa-2023.02-fdpic-esp32s3/images/xipImage -a -f build/build-xtensa-2023.02-fdpic-esp32s3/images/rootfs.cramfs ] || exit 1
# bootloader
ENV IDF_PATH="/app/build/esp-hosted/esp_hosted_ng/esp/esp_driver/esp-idf"
RUN cd build && \
git clone https://github.com/jcmvbkbc/esp-hosted -b shmem --depth=1 && \
cd esp-hosted/esp_hosted_ng/esp/esp_driver && cmake . && \
cd esp-idf && . ./export.sh && \
cd ../network_adapter && idf.py set-target esp32s3 && \
cp sdkconfig.defaults.esp32s3 sdkconfig && idf.py build
# move files over
RUN cd build && mkdir release && \
cp esp-hosted/esp_hosted_ng/esp/esp_driver/network_adapter/build/bootloader/bootloader.bin release && \
cp esp-hosted/esp_hosted_ng/esp/esp_driver/network_adapter/build/partition_table/partition-table.bin release && \
cp esp-hosted/esp_hosted_ng/esp/esp_driver/network_adapter/build/network_adapter.bin release && \
cp build-xtensa-2023.02-fdpic-esp32s3/images/xipImage release && \
cp build-xtensa-2023.02-fdpic-esp32s3/images/rootfs.cramfs release
# keep docker running so we can debug/rebuild :)
USER root
ENTRYPOINT ["tail", "-f", "/dev/null"]
# grab the files with `docker cp CONTAINER_NAME:/app/build/release/\* .`
# now you can burn the files from the 'release' folder with:
# python esptool.py --chip esp32s3 -p /dev/ttyUSB0 -b 921600 --before=default_reset --after=hard_reset write_flash 0x0 bootloader.bin 0x10000 network_adapter.bin 0x8000 partition-table.bin
# next we can burn in the kernel and filesys with parttool, which is part of esp-idf
# parttool.py write_partition --partition-name linux --input xipImage
# parttool.py write_partition --partition-name rootfs --input rootfs.cramfs

View file

@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
from adafruit_matrixportal.matrix import Matrix
from messageboard import MessageBoard
from messageboard.fontpool import FontPool
from messageboard.message import Message
matrix = Matrix(width=128, height=16, bit_depth=5)
messageboard = MessageBoard(matrix)
messageboard.set_background("images/background.bmp")
fontpool = FontPool()
fontpool.add_font("arial", "fonts/Arial-10.pcf")
while True:
message = Message(fontpool.find_font("arial"), mask_color=0xFF00FF, opacity=0.8)
message.add_image("images/maskedstar.bmp")
message.add_text("Hello World!", color=0xFFFF00, x_offset=2, y_offset=2)
messageboard.animate(message, "Scroll", "in_from_right")
time.sleep(1)
messageboard.animate(message, "Scroll", "out_to_left")

View file

@ -0,0 +1,81 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
from adafruit_matrixportal.matrix import Matrix
from messageboard import MessageBoard
from messageboard.fontpool import FontPool
from messageboard.message import Message
matrix = Matrix(width=128, height=16, bit_depth=5)
messageboard = MessageBoard(matrix)
messageboard.set_background("images/background.bmp")
fontpool = FontPool()
fontpool.add_font("arial", "fonts/Arial-10.pcf")
fontpool.add_font("comic", "fonts/Comic-10.pcf")
fontpool.add_font("dejavu", "fonts/DejaVuSans-10.pcf")
message = Message(fontpool.find_font("terminal"), opacity=0.8)
message.add_image("images/maskedstar.bmp")
message.add_text("Hello World!", color=0xFFFF00, x_offset=2, y_offset=2)
message1 = Message(fontpool.find_font("dejavu"))
message2 = Message(fontpool.find_font("comic"), mask_color=0x00FF00)
print("add blinka")
message2.add_image("images/maskedblinka.bmp")
print("add text")
message2.add_text("CircuitPython", color=0xFFFF00, y_offset=-2)
message3 = Message(fontpool.find_font("dejavu"))
message3.add_text("circuitpython.com", color=0xFF0000)
message4 = Message(fontpool.find_font("arial"))
message4.add_text("Buy Electronics", color=0xFFFFFF)
while True:
message1.clear()
message1.add_text("Scroll Text In", color=0xFF0000)
messageboard.animate(message1, "Scroll", "in_from_left")
time.sleep(1)
message1.clear()
message1.add_text("Change Messages")
messageboard.animate(message1, "Static", "show")
time.sleep(1)
message1.clear()
message1.add_text("And Scroll Out")
messageboard.animate(message1, "Static", "show")
messageboard.animate(message1, "Scroll", "out_to_right")
time.sleep(1)
message1.clear()
message1.add_text("Or more effects like looping ", color=0xFFFF00)
messageboard.animate(
message1, "Split", "in_vertically"
) # Split never completely joins
messageboard.animate(
message1, "Loop", "left"
) # Text too high (probably from split)
messageboard.animate(
message1, "Static", "flash", count=3
) # Flashes in weird positions
messageboard.animate(message1, "Split", "out_vertically")
time.sleep(1)
messageboard.animate(message2, "Static", "fade_in")
time.sleep(1)
messageboard.animate(message2, "Static", "fade_out")
messageboard.set_background(0x00FF00)
messageboard.animate(message3, "Scroll", "in_from_top")
time.sleep(1)
messageboard.animate(message3, "Scroll", "out_to_bottom")
messageboard.set_background("images/background.bmp")
messageboard.animate(message4, "Scroll", "in_from_right")
time.sleep(1)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View file

@ -0,0 +1,158 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import bitmaptools
import displayio
import adafruit_imageload
from .doublebuffer import DoubleBuffer
from .message import Message
class MessageBoard:
def __init__(self, matrix):
self.fonts = {}
self.display = matrix.display
self._buffer_width = self.display.width * 2
self._buffer_height = self.display.height * 2
self._dbl_buf = DoubleBuffer(
self.display, self._buffer_width, self._buffer_height
)
self._background = None
self.set_background() # Set to black
self._position = (0, 0)
def set_background(self, file_or_color=0x000000):
"""The background image to a bitmap file."""
if isinstance(file_or_color, str): # its a filenme:
background, bg_shader = adafruit_imageload.load(file_or_color)
self._dbl_buf.shader = bg_shader
self._background = background
elif isinstance(file_or_color, int):
# Make a background color fill
bg_shader = displayio.ColorConverter(
input_colorspace=displayio.Colorspace.RGB565
)
background = displayio.Bitmap(
self.display.width, self.display.height, 65535
)
background.fill(displayio.ColorConverter().convert(file_or_color))
self._dbl_buf.shader = bg_shader
self._background = background
else:
raise RuntimeError("Unknown type of background")
def animate(self, message, animation_class, animation_function, **kwargs):
anim_class = __import__(
f"{self.__module__}.animations.{animation_class.lower()}"
)
anim_class = getattr(anim_class, "animations")
anim_class = getattr(anim_class, animation_class.lower())
anim_class = getattr(anim_class, animation_class)
animation = anim_class(
self.display, self._draw, self._position
) # Instantiate the class
# Call the animation function and pass kwargs along with the message (positional)
anim_func = getattr(animation, animation_function)
anim_func(message, **kwargs)
def _draw(
self,
image,
x,
y,
opacity=None,
mask_color=0xFF00FF,
blendmode=bitmaptools.BlendMode.Normal,
post_draw_position=None,
):
"""Draws a message to the buffer taking its current settings into account.
It also sets the current position and performs a swap.
"""
self._position = (x, y)
buffer_x_offset = self._buffer_width - self.display.width
buffer_y_offset = self._buffer_height - self.display.height
# Image can be a message in which case its properties will be used
if isinstance(image, Message):
if opacity is None:
opacity = image.opacity
mask_color = image.mask_color
blendmode = image.blendmode
image = image.buffer
if opacity is None:
opacity = 1.0
if mask_color > 65535:
mask_color = displayio.ColorConverter().convert(mask_color)
# Blit the background
bitmaptools.blit(
self._dbl_buf.active_buffer,
self._background,
buffer_x_offset,
buffer_y_offset,
)
# If the image is wider than the display buffer, we need to shrink it
if x + buffer_x_offset < 0:
new_image = displayio.Bitmap(
image.width - self.display.width, image.height, 65535
)
bitmaptools.blit(
new_image,
image,
0,
0,
x1=self.display.width,
y1=0,
x2=image.width,
y2=image.height,
)
x += self.display.width
image = new_image
# If the image is taller than the display buffer, we need to shrink it
if y + buffer_y_offset < 0:
new_image = displayio.Bitmap(
image.width, image.height - self.display.height, 65535
)
bitmaptools.blit(
new_image,
image,
0,
0,
x1=0,
y1=self.display.height,
x2=image.width,
y2=image.height,
)
y += self.display.height
image = new_image
# Clear the foreground buffer
foreground_buffer = displayio.Bitmap(
self._buffer_width, self._buffer_height, 65535
)
foreground_buffer.fill(mask_color)
bitmaptools.blit(
foreground_buffer, image, x + buffer_x_offset, y + buffer_y_offset
)
# Blend the foreground buffer into the main buffer
bitmaptools.alphablend(
self._dbl_buf.active_buffer,
self._dbl_buf.active_buffer,
foreground_buffer,
displayio.Colorspace.RGB565,
1.0,
opacity,
blendmode=blendmode,
skip_source2_index=mask_color,
)
self._dbl_buf.show()
# Allow for an override of the position after drawing (needed for split effects)
if post_draw_position is not None and isinstance(post_draw_position, tuple):
self._position = post_draw_position

View file

@ -0,0 +1,24 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
class Animation:
def __init__(self, display, draw_callback, starting_position=(0, 0)):
self._display = display
self._position = starting_position
self._draw = draw_callback
@staticmethod
def _wait(start_time, duration):
"""Uses time.monotonic() to wait from the start time for a specified duration"""
while time.monotonic() < (start_time + duration):
pass
return time.monotonic()
def _get_centered_position(self, message):
return int(self._display.width / 2 - message.buffer.width / 2), int(
self._display.height / 2 - message.buffer.height / 2
)

View file

@ -0,0 +1,146 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import displayio
import bitmaptools
from . import Animation
class Loop(Animation):
def _create_loop_image(self, image, x_offset, y_offset, mask_color):
"""Attach a copy of an image by a certain offset so it can be looped."""
if 0 < x_offset < self._display.width:
x_offset = self._display.width
if 0 < y_offset < self._display.height:
y_offset = self._display.height
loop_image = displayio.Bitmap(
image.width + x_offset, image.height + y_offset, 65535
)
loop_image.fill(mask_color)
bitmaptools.blit(loop_image, image, 0, 0)
bitmaptools.blit(loop_image, image, x_offset, y_offset)
return loop_image
def left(self, message, duration=1, count=1):
"""Loop a message towards the left side of the display over a certain period of time by a
certain number of times. The message will re-enter from the right and end up back a the
starting position.
:param message: The message to animate.
:param float count: (optional) The number of times to loop. (default=1)
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:type message: Message
"""
current_x, current_y = self._position
distance = max(message.buffer.width, self._display.width)
loop_image = self._create_loop_image(
message.buffer, distance, 0, message.mask_color
)
for _ in range(count):
for _ in range(distance):
start_time = time.monotonic()
current_x -= 1
if current_x < 0 - message.buffer.width:
current_x += distance
self._draw(
loop_image,
current_x,
current_y,
message.opacity,
)
self._wait(start_time, duration / distance / count)
def right(self, message, duration=1, count=1):
"""Loop a message towards the right side of the display over a certain period of time by a
certain number of times. The message will re-enter from the left and end up back a the
starting position.
:param message: The message to animate.
:param float count: (optional) The number of times to loop. (default=1)
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:type message: Message
"""
current_x, current_y = self._position
distance = max(message.buffer.width, self._display.width)
loop_image = self._create_loop_image(
message.buffer, distance, 0, message.mask_color
)
for _ in range(count):
for _ in range(distance):
start_time = time.monotonic()
current_x += 1
if current_x > 0:
current_x -= distance
self._draw(
loop_image,
current_x,
current_y,
message.opacity,
)
self._wait(start_time, duration / distance / count)
def up(self, message, duration=0.5, count=1):
"""Loop a message towards the top side of the display over a certain period of time by a
certain number of times. The message will re-enter from the bottom and end up back a the
starting position.
:param message: The message to animate.
:param float count: (optional) The number of times to loop. (default=1)
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:type message: Message
"""
current_x, current_y = self._position
distance = max(message.buffer.height, self._display.height)
loop_image = self._create_loop_image(
message.buffer, 0, distance, message.mask_color
)
for _ in range(count):
for _ in range(distance):
start_time = time.monotonic()
current_y -= 1
if current_y < 0 - message.buffer.height:
current_y += distance
self._draw(
loop_image,
current_x,
current_y,
message.opacity,
)
self._wait(start_time, duration / distance / count)
def down(self, message, duration=0.5, count=1):
"""Loop a message towards the bottom side of the display over a certain period of time by a
certain number of times. The message will re-enter from the top and end up back a the
starting position.
:param message: The message to animate.
:param float count: (optional) The number of times to loop. (default=1)
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:type message: Message
"""
current_x, current_y = self._position
distance = max(message.buffer.height, self._display.height)
loop_image = self._create_loop_image(
message.buffer, 0, distance, message.mask_color
)
for _ in range(count):
for _ in range(distance):
start_time = time.monotonic()
current_y += 1
if current_y > 0:
current_y -= distance
self._draw(
loop_image,
current_x,
current_y,
message.opacity,
)
self._wait(start_time, duration / distance / count)

View file

@ -0,0 +1,170 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
from . import Animation
class Scroll(Animation):
def scroll_from_to(self, message, duration, start_x, start_y, end_x, end_y):
"""
Scroll the message from one position to another over a certain period of
time.
:param message: The message to animate.
:param float duration: The period of time to perform the animation over in seconds.
:param int start_x: The Starting X Position
:param int start_yx: The Starting Y Position
:param int end_x: The Ending X Position
:param int end_y: The Ending Y Position
:type message: Message
"""
steps = max(abs(end_x - start_x), abs(end_y - start_y))
if not steps:
return
increment_x = (end_x - start_x) / steps
increment_y = (end_y - start_y) / steps
for i in range(steps + 1):
start_time = time.monotonic()
current_x = start_x + round(i * increment_x)
current_y = start_y + round(i * increment_y)
self._draw(message, current_x, current_y)
if i <= steps:
self._wait(start_time, duration / steps)
def out_to_left(self, message, duration=1):
"""Scroll a message off the display from its current position towards the left
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:type message: Message
"""
current_x, current_y = self._position
self.scroll_from_to(
message, duration, current_x, current_y, 0 - message.buffer.width, current_y
)
def in_from_left(self, message, duration=1, x=0):
"""Scroll a message in from the left side of the display over a certain period of
time. The final position is centered.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:param int x: (optional) The amount of x-offset from the center position (default=0)
:type message: Message
"""
center_x, center_y = self._get_centered_position(message)
self.scroll_from_to(
message,
duration,
0 - message.buffer.width,
center_y,
center_x + x,
center_y,
)
def in_from_right(self, message, duration=1, x=0):
"""Scroll a message in from the right side of the display over a certain period of
time. The final position is centered.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:param int x: (optional) The amount of x-offset from the center position (default=0)
:type message: Message
"""
center_x, center_y = self._get_centered_position(message)
self.scroll_from_to(
message, duration, self._display.width - 1, center_y, center_x + x, center_y
)
def in_from_top(self, message, duration=1, y=0):
"""Scroll a message in from the top side of the display over a certain period of
time. The final position is centered.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:param int y: (optional) The amount of y-offset from the center position (default=0)
:type message: Message
"""
center_x, center_y = self._get_centered_position(message)
self.scroll_from_to(
message,
duration,
center_x,
0 - message.buffer.height,
center_x,
center_y + y,
)
def in_from_bottom(self, message, duration=1, y=0):
"""Scroll a message in from the bottom side of the display over a certain period of
time. The final position is centered.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:param int y: (optional) The amount of y-offset from the center position (default=0)
:type message: Message
"""
center_x, center_y = self._get_centered_position(message)
self.scroll_from_to(
message,
duration,
center_x,
self._display.height - 1,
center_x,
center_y + y,
)
def out_to_right(self, message, duration=1):
"""Scroll a message off the display from its current position towards the right
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:type message: Message
"""
current_x, current_y = self._position
self.scroll_from_to(
message, duration, current_x, current_y, self._display.width - 1, current_y
)
def out_to_top(self, message, duration=1):
"""Scroll a message off the display from its current position towards the top
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:type message: Message
"""
current_x, current_y = self._position
self.scroll_from_to(
message,
duration,
current_x,
current_y,
current_x,
0 - message.buffer.height,
)
def out_to_bottom(self, message, duration=1):
"""Scroll a message off the display from its current position towards the bottom
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over in seconds. (default=1)
:type message: Message
"""
current_x, current_y = self._position
self.scroll_from_to(
message, duration, current_x, current_y, current_x, self._display.height - 1
)

View file

@ -0,0 +1,222 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
import displayio
import bitmaptools
from . import Animation
class Split(Animation):
def out_horizontally(self, message, duration=0.5):
"""Show the effect of a message splitting horizontally
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over. (default=0.5)
:type message: Message
"""
current_x, current_y = self._position
image = message.buffer
left_image = displayio.Bitmap(image.width // 2, image.height, 65535)
bitmaptools.blit(
left_image, image, 0, 0, x1=0, y1=0, x2=image.width // 2, y2=image.height
)
right_image = displayio.Bitmap(image.width // 2, image.height, 65535)
bitmaptools.blit(
right_image,
image,
0,
0,
x1=image.width // 2,
y1=0,
x2=image.width,
y2=image.height,
)
distance = self._display.width // 2
for i in range(distance + 1):
start_time = time.monotonic()
effect_buffer = displayio.Bitmap(
self._display.width + image.width, image.height, 65535
)
effect_buffer.fill(message.mask_color)
bitmaptools.blit(effect_buffer, left_image, distance - i, 0)
bitmaptools.blit(
effect_buffer, right_image, distance + image.width // 2 + i, 0
)
self._draw(
effect_buffer,
current_x - self._display.width // 2,
current_y,
message.opacity,
post_draw_position=(current_x - self._display.width // 2, current_y),
)
self._wait(start_time, duration / distance)
def out_vertically(self, message, duration=0.5):
"""Show the effect of a message splitting vertically
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over. (default=0.5)
:type message: Message
"""
current_x, current_y = self._position
image = message.buffer
top_image = displayio.Bitmap(image.width, image.height // 2, 65535)
bitmaptools.blit(
top_image, image, 0, 0, x1=0, y1=0, x2=image.width, y2=image.height // 2
)
bottom_image = displayio.Bitmap(image.width, image.height // 2, 65535)
bitmaptools.blit(
bottom_image,
image,
0,
0,
x1=0,
y1=image.height // 2,
x2=image.width,
y2=image.height,
)
distance = self._display.height // 2
effect_buffer_width = self._display.width
if current_x < 0:
effect_buffer_width -= current_x
for i in range(distance + 1):
start_time = time.monotonic()
effect_buffer = displayio.Bitmap(
effect_buffer_width, self._display.height + image.height, 65535
)
effect_buffer.fill(message.mask_color)
bitmaptools.blit(effect_buffer, top_image, 0, distance - i)
bitmaptools.blit(
effect_buffer, bottom_image, 0, distance + image.height // 2 + i + 1
)
self._draw(
effect_buffer,
current_x,
current_y - self._display.height // 2,
message.opacity,
post_draw_position=(current_x, current_y - self._display.height // 2),
)
self._wait(start_time, duration / distance)
def in_horizontally(self, message, duration=0.5):
"""Show the effect of a split message joining horizontally
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over. (default=0.5)
:type message: Message
"""
current_x = int(self._display.width / 2 - message.buffer.width / 2)
current_y = int(self._display.height / 2 - message.buffer.height / 2)
image = message.buffer
left_image = displayio.Bitmap(image.width // 2, image.height, 65535)
bitmaptools.blit(
left_image, image, 0, 0, x1=0, y1=0, x2=image.width // 2, y2=image.height
)
right_image = displayio.Bitmap(image.width // 2, image.height, 65535)
bitmaptools.blit(
right_image,
image,
0,
0,
x1=image.width // 2,
y1=0,
x2=image.width,
y2=image.height,
)
distance = self._display.width // 2
effect_buffer = displayio.Bitmap(
self._display.width + image.width, image.height, 65535
)
effect_buffer.fill(message.mask_color)
for i in range(distance + 1):
start_time = time.monotonic()
bitmaptools.blit(effect_buffer, left_image, i, 0)
bitmaptools.blit(
effect_buffer,
right_image,
self._display.width + image.width // 2 - i + 1,
0,
)
self._draw(
effect_buffer,
current_x - self._display.width // 2,
current_y,
message.opacity,
post_draw_position=(current_x, current_y),
)
self._wait(start_time, duration / distance)
def in_vertically(self, message, duration=0.5):
"""Show the effect of a split message joining vertically
over a certain period of time.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over. (default=0.5)
:type message: Message
"""
current_x = int(self._display.width / 2 - message.buffer.width / 2)
current_y = int(self._display.height / 2 - message.buffer.height / 2)
image = message.buffer
top_image = displayio.Bitmap(image.width, image.height // 2, 65535)
bitmaptools.blit(
top_image, image, 0, 0, x1=0, y1=0, x2=image.width, y2=image.height // 2
)
bottom_image = displayio.Bitmap(image.width, image.height // 2, 65535)
bitmaptools.blit(
bottom_image,
image,
0,
0,
x1=0,
y1=image.height // 2,
x2=image.width,
y2=image.height,
)
distance = self._display.height // 2
effect_buffer_width = self._display.width
if current_x < 0:
effect_buffer_width -= current_x
effect_buffer = displayio.Bitmap(
effect_buffer_width, self._display.height + image.height, 65535
)
effect_buffer.fill(message.mask_color)
for i in range(distance + 1):
start_time = time.monotonic()
bitmaptools.blit(effect_buffer, top_image, 0, i + 1)
bitmaptools.blit(
effect_buffer,
bottom_image,
0,
self._display.height + image.height // 2 - i + 1,
)
self._draw(
effect_buffer,
current_x,
current_y - self._display.height // 2,
message.opacity,
post_draw_position=(current_x, current_y),
)
self._wait(start_time, duration / distance)

View file

@ -0,0 +1,101 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import time
from . import Animation
class Static(Animation):
def show(self, message):
"""Show the message at its current position.
:param message: The message to show.
:type message: Message
"""
x, y = self._position
self._draw(message, x, y)
def hide(self, message):
"""Hide the message at its current position.
:param message: The message to hide.
:type message: Message
"""
x, y = self._position
self._draw(message, x, y, opacity=0)
def blink(self, message, count=3, duration=1):
"""Blink the foreground on and off a centain number of
times over a certain period of time.
:param message: The message to animate.
:param float count: (optional) The number of times to blink. (default=3)
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:type message: Message
"""
delay = duration / count / 2
for _ in range(count):
start_time = time.monotonic()
self.hide(message)
start_time = self._wait(start_time, delay)
self.show(message)
self._wait(start_time, delay)
def flash(self, message, count=3, duration=1):
"""Fade the foreground in and out a centain number of
times over a certain period of time.
:param message: The message to animate.
:param float count: (optional) The number of times to flash. (default=3)
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:type message: Message
"""
delay = duration / count / 2
steps = 50 // count
for _ in range(count):
self.fade_out(message, duration=delay, steps=steps)
self.fade_in(message, duration=delay, steps=steps)
def fade_in(self, message, duration=1, steps=50):
"""Fade the foreground in over a certain period of time
by a certain number of steps. More steps is smoother, but too high
of a number may slow down the animation too much.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:param float steps: (optional) The number of steps to perform the animation. (default=50)
:type message: Message
"""
current_x = int(self._display.width / 2 - message.buffer.width / 2)
current_y = int(self._display.height / 2 - message.buffer.height / 2)
delay = duration / (steps + 1)
for opacity in range(steps + 1):
start_time = time.monotonic()
self._draw(message, current_x, current_y, opacity=opacity / steps)
self._wait(start_time, delay)
def fade_out(self, message, duration=1, steps=50):
"""Fade the foreground out over a certain period of time
by a certain number of steps. More steps is smoother, but too high
of a number may slow down the animation too much.
:param message: The message to animate.
:param float duration: (optional) The period of time to perform the animation
over. (default=1)
:param float steps: (optional) The number of steps to perform the animation. (default=50)
:type message: Message
"""
delay = duration / (steps + 1)
for opacity in range(steps + 1):
start_time = time.monotonic()
self._draw(
message,
self._position[0],
self._position[1],
opacity=(steps - opacity) / steps,
)
self._wait(start_time, delay)

View file

@ -0,0 +1,58 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import displayio
class DoubleBuffer:
def __init__(self, display, width, height, shader=None, bit_depth=16):
self._buffer_group = (displayio.Group(), displayio.Group())
self._buffer = (
displayio.Bitmap(width, height, 2**bit_depth - 1),
displayio.Bitmap(width, height, 2**bit_depth - 1),
)
self._x_offset = display.width - width
self._y_offset = display.height - height
self.display = display
self._active_buffer = 0 # The buffer we are updating
if shader is None:
shader = displayio.ColorConverter()
buffer0_sprite = displayio.TileGrid(
self._buffer[0],
pixel_shader=shader,
x=self._x_offset,
y=self._y_offset,
)
self._buffer_group[0].append(buffer0_sprite)
buffer1_sprite = displayio.TileGrid(
self._buffer[1],
pixel_shader=shader,
x=self._x_offset,
y=self._y_offset,
)
self._buffer_group[1].append(buffer1_sprite)
def show(self, swap=True):
self.display.show(self._buffer_group[self._active_buffer])
if swap:
self.swap()
def swap(self):
self._active_buffer = 0 if self._active_buffer else 1
@property
def active_buffer(self):
return self._buffer[self._active_buffer]
@property
def shader(self):
return self._buffer_group[0][0].pixel_shader
@shader.setter
def shader(self, shader):
self._buffer_group[0][0].pixel_shader = shader
self._buffer_group[1][0].pixel_shader = shader

View file

@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import terminalio
from adafruit_bitmap_font import bitmap_font
class FontPool:
def __init__(self):
"""Create a pool of fonts for reuse to avoid loading duplicates"""
self._fonts = {}
self.add_font("terminal")
def add_font(self, name, file=None):
if name in self._fonts:
return
if name == "terminal":
font = terminalio.FONT
else:
font = bitmap_font.load_font(file)
self._fonts[name] = font
def find_font(self, name):
if name in self._fonts:
return self._fonts[name]
return None

View file

@ -0,0 +1,144 @@
# SPDX-FileCopyrightText: 2023 Melissa LeBlanc-Williams for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import bitmaptools
import displayio
import adafruit_imageload
from adafruit_display_text import bitmap_label
class Message:
def __init__(
self,
font,
opacity=1.0,
mask_color=0xFF00FF,
blendmode=bitmaptools.BlendMode.Normal,
):
self._current_font = font
self._current_color = 0xFF0000
self._buffer = displayio.Bitmap(0, 0, 65535)
self._cursor = [0, 0]
self.opacity = opacity
self._blendmode = blendmode
self._mask_color = 0
self.mask_color = mask_color
self._width = 0
self._height = 0
def _enlarge_buffer(self, width, height):
"""Resize the message buffer to grow as necessary"""
new_width = self._width
if self._cursor[0] + width >= self._width:
new_width = self._cursor[0] + width
new_height = self._height
if self._cursor[1] + height >= self._height:
new_height = self._cursor[1] + height
if new_width > self._width or new_height > self._height:
new_buffer = displayio.Bitmap(new_width, new_height, 65535)
if self._mask_color is not None:
bitmaptools.fill_region(
new_buffer, 0, 0, new_width, new_height, self._mask_color
)
bitmaptools.blit(new_buffer, self._buffer, 0, 0)
self._buffer = new_buffer
self._width = new_width
self._height = new_height
def _add_bitmap(self, bitmap, x_offset=0, y_offset=0):
new_width, new_height = (
self._cursor[0] + bitmap.width + x_offset,
self._cursor[1] + bitmap.height + y_offset,
)
# Resize the buffer if necessary
self._enlarge_buffer(new_width, new_height)
# Blit the image into the buffer
source_left, source_top = 0, 0
if self._cursor[0] + x_offset < 0:
source_left = 0 - (self._cursor[0] + x_offset)
x_offset = 0
if self._cursor[1] + y_offset < 0:
source_top = 0 - (self._cursor[1] + y_offset)
y_offset = 0
bitmaptools.blit(
self._buffer,
bitmap,
self._cursor[0] + x_offset,
self._cursor[1] + y_offset,
x1=source_left,
y1=source_top,
)
# Move the cursor
self._cursor[0] += bitmap.width + x_offset
def add_text(
self,
text,
color=None,
font=None,
x_offset=0,
y_offset=0,
):
if font is None:
font = self._current_font
if color is None:
color = self._current_color
color_565value = displayio.ColorConverter().convert(color)
# Create a bitmap label and add it to the buffer
bmp_label = bitmap_label.Label(font, text=text)
color_overlay = displayio.Bitmap(
bmp_label.bitmap.width, bmp_label.bitmap.height, 65535
)
color_overlay.fill(color_565value)
mask_overlay = displayio.Bitmap(
bmp_label.bitmap.width, bmp_label.bitmap.height, 65535
)
mask_overlay.fill(self._mask_color)
bitmaptools.blit(color_overlay, bmp_label.bitmap, 0, 0, skip_source_index=1)
bitmaptools.blit(
color_overlay, mask_overlay, 0, 0, skip_dest_index=color_565value
)
bmp_label = None
self._add_bitmap(color_overlay, x_offset, y_offset)
def add_image(self, image, x_offset=0, y_offset=0):
# Load the image with imageload and add it to the buffer
bmp_image, _ = adafruit_imageload.load(image)
self._add_bitmap(bmp_image, x_offset, y_offset)
def clear(self):
"""Clear the canvas content, but retain all of the style settings"""
self._buffer = displayio.Bitmap(0, 0, 65535)
self._cursor = [0, 0]
self._width = 0
self._height = 0
@property
def buffer(self):
"""Return the current buffer"""
if self._width == 0 or self._height == 0:
raise RuntimeError("No content in the message")
return self._buffer
@property
def mask_color(self):
"""Get or Set the mask color"""
return self._mask_color
@mask_color.setter
def mask_color(self, value):
self._mask_color = displayio.ColorConverter().convert(value)
@property
def blendmode(self):
"""Get or Set the blendmode"""
return self._blendmode
@blendmode.setter
def blendmode(self, value):
if value in bitmaptools.BlendMode:
self._blendmode = value