I probably should have done this study before anything else, but the rush to see the games working on the console was greater and that ended up being left until the end.
When I started thinking about using Pixel_Framebuf to create games, I immediately came across an explanation of the different initialization parameters, but I had difficulty understanding how it worked. For example, I initially understood that the reverse_x=True parameter should be set by default. And I didn't really understand how the VERTICAL parameter worked.
For the Sesc workshop, I purchased six 32x8 Pixel panels, and perhaps it would have been more practical to have purchased the same number of 16x16 panels, so as not to have to change the Pixel_Framebuf library.
Right now I'm working with new 16x16 panels, using the standard library and trying to properly understand the initialization parameters. And this tutorial is about that.
Understanding raw pixel addressing
Don't do like me... The Pixel_Framebuf library assumes that the coordinate 0, 0 will be the pixel placed in the upper left corner. Then you need to rotate your panel manually and position its first pixel in that position.
With this, everything referred to as rotation, grid, position, inversion of axes, x-axis, y-axis, width, height... will be the same as what you are seeing before you.
This small script will help you understand the position of each pixel in your LED matrix.
'''
Understanding Pixel Position in Matrix Neopixel
'''
import board, time
import neopixel_spi as neopixel
# This is the original version of library when using 16x16 Panels
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
spi = board.SPI()
pixel_pin = board.D10 #board.MOSI
# Setting up the Neopixel Pannel - 16 x 16 Version
how_many = 2
panel_width = 16 # Two Pannels Wide.
panel_height = 16
num_pixels = how_many * panel_width * panel_height
pixels = neopixel.NeoPixel_SPI(
spi,
num_pixels,
brightness=0.2,
auto_write=False,
)
print ('Showing pure pixel position')
while True:
for n in range (num_pixels):
print ('Pixel ', n)
pixels[n] = 0xFF0000
if n>0:
pixels [n-1] = 0x000000
pixels.show()
# time.sleep(0.02)
The code above will light each pixel red (0xFF0000) and turn off (0x000000) the previous pixel, one at a time until the last pixel of your LED matrix. This will allow you to notice the pattern in which the pixels were connected in the matrix and the position in which they are placed.
Note that in this code we have not yet used the Pixel Framebuf library.
Start simpleAlthough the Adafruit tutorial has a series of initializations for some models of Neopixel LED matrices, it is recommended that you start your Neopixel screen with as few parameters as possible. I'm Brazilian and despite understanding English relatively well, I often fail to understand the nuances. When I read the Adafruit tutorial, I understood that those initialization sequences had to be observed exactly and this caused a lot of confusion in my project.
If you, like me, are going to use more than one panel, it is very important to understand how they are physically connected. In my case, when I add the 16x16 panels in sequence, the addressing of the first LED of the subsequent panel follows the width axis.
Understanding this, you can make some decisions when starting your panel. For example, I would like to use the panels in a configuration where the width is 16 and the height is 32.
If I initialize the panel with the rotation at 1, I will have resolved the change between height and width. But pixel 0, in this case, will become the pixel in the upper right position, which suggests a mirroring. But on which axis?
We will use the same script several times and only change the initialization step. The images I'm going to share are the result of the display on this 16x16 display model that I'm using. I haven't bought many different models of Neopixel matrices on Aliexpress, but I imagine you are well aware of the difficulty in finding some standardization in these products.
That's why I'm doing this step by step, because you can test and discover the best configuration for your neopixel matrix, depending on your project.
'''
Understanding Pixel Position in Matrix Neopixel
'''
import board, time
import neopixel_spi as neopixel
# This is the original version of library when using 16x16 Panels
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
spi = board.SPI()
pixel_pin = board.D10 #board.MOSI
# Setting up the Neopixel Pannel - 16 x 16 Version
how_many = 2
panel_width = 16 # Two Pannels Wide.
panel_height = 16
num_pixels = how_many * panel_width * panel_height
pixels = neopixel.NeoPixel_SPI(
spi,
num_pixels,
brightness=0.2,
auto_write=False,
)
# When 16x16 Using original Adafruit_Pixel_Framebuf Library
screen = PixelFramebuffer(
pixels,
panel_width * how_many, # because my panel is physically connected that way.
panel_height,
rotation = 0,
)
print ('Drawing a Rectangle')
screen.fill(0) # clear screen
screen.display() # showing the changes
print ('I will add some time.sleep just to you understand screen.display()')
rect_width = 8
rect_height = 4
rect_color = 0xFF0000
other_color = 0x00FF00
# Supposing x is related to width and y related to height
# this code will show a red rectangle in middle of two pannels
rect_x = (screen.width // 2) - (rect_width //2)
rect_y = (screen.height // 2) - (rect_height //2)
print ('Rect Position ', rect_x, rect_y)
while True:
screen.fill_rect (rect_x, rect_y, rect_width, rect_height, rect_color)
screen.display()
time.sleep (2)
print ('Contour')
screen.rect (rect_x, rect_y, rect_width, rect_height, other_color)
screen.display()
time.sleep (2)
Note that there are many errors in the drawing. The rectangles were drawn in the wrong position and with the wrong dimension. It looks like the outline is two pixels wide on the horizontal axis.
What is wrong?
At this point, there is no point looking for errors in the code's mathematics or anything along those lines. The problem is at startup.
Now, just change the screen initialization parameters, adding the VERTICAL orientation.
screen = PixelFramebuffer(
pixels,
panel_width * how_many, # because my panel is physically connected that way.
panel_height,
rotation = 0,
#reverse_x=True,
orientation=VERTICAL, # Add this, and the screen will work
)
Now it seems to be drawing correctly...
But before proceeding, let's understand what the 0, 0 coordinate of the screen is. Because, since we aligned the rectangles in the center of the screen, we're going to have a hard time understanding this. And also, let's check the coordinate on the opposite diagonal: 31.15.
'''
Understanding Pixel Position in Matrix Neopixel
'''
import board, time
import neopixel_spi as neopixel
# This is the original version of library when using 16x16 Panels
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
spi = board.SPI()
pixel_pin = board.D10 #board.MOSI
# Setting up the Neopixel Pannel - 16 x 16 Version
how_many = 2
panel_width = 16 # Two Pannels Wide.
panel_height = 16
num_pixels = how_many * panel_width * panel_height
pixels = neopixel.NeoPixel_SPI(
spi,
num_pixels,
brightness=0.2,
auto_write=False,
)
# When 16x16 Using original Adafruit_Pixel_Framebuf Library
screen = PixelFramebuffer(
pixels,
panel_width * how_many, # because my panel is physically connected that way.
panel_height,
rotation = 0,
#reverse_x=True,
orientation=VERTICAL,
)
screen.fill(0) # clear screen
screen.pixel(0,0, 0xFF0000)
screen.pixel(31,15,0x00FF00)
screen.display() # showing the changes
Before we change the screen rotation to understand what happens to the coordinates, let's check what methods and properties we can find inside the PixelFramebuf class.
After running the last script, type the command dir(screen), directly into the Circuitpython shell.
>>> dir (screen) # On Shell
['__class__', '__init__', '__module__', '__qualname__', 'format', '__dict__', 'blit', 'display', 'fill', 'height', 'rotation', 'width', 'pixel', '_width', '_height', '_grid', '_buffer', '_double_buffer', 'stride', 'buf', 'rect', 'fill_rect', '_font', '_rotation', 'hline', 'vline', 'circle', 'line', 'scroll', 'text', 'image']
Now let's try the first screen rotation and see what happens.
# When 16x16 Using original Adafruit_Pixel_Framebuf Library
screen = PixelFramebuffer(
pixels,
panel_width * how_many, # because my panel is physically connected that way.
panel_height,
rotation = 1,
#reverse_x=True,
orientation=VERTICAL,
)
The first adjustment I'm going to make is to match the height and width of the screen according to the arrangement I'm working on.
screen = PixelFramebuffer(
pixels,
panel_width ,
panel_height * how_many, # Now we have 32 Pixels Height
rotation = 1,
#reverse_x=True, # Comment all this
#reverse_y=True,
#orientation=VERTICAL,
)
We can try to fix this by just changing the rotation to 3. But first, let's draw the two rectangles again. Type this directly on the shell:
rect_width = 8
rect_height = 4
rect_color = 0xFF0000
other_color = 0x00FF00
# Supposing x is related to width and y related to height
# this code will show a red rectangle in middle of two pannels
rect_x = (screen.width // 2) - (rect_width //2)
rect_y = (screen.height // 2) - (rect_height //2)
print ('Rect Position ', rect_x, rect_y)
screen.fill_rect (rect_x, rect_y, rect_width, rect_height, 0xff0000)
screen.display()
Notice again that the rectangles were drawn completely out of position. This is because the drawing methods don't seem to take screen rotation into account. So, what we understand by width and height in this rotation is not the same as what Pixel_Framebuf understands.
Although the rotation is 90 degrees, the width and height values of the rectangle are passed as if the rotation were the default, which is 0 degrees.
The previous script did the following calculation, took a rectangle with a height of four pixels and a width of 8, placing it centered in the middle of the screen. In that circumstance, the screen measured 32 pixels wide and 16 pixels tall. But now, we rotate the screen. And we have a screen that is 16 pixels wide and 32 pixels tall.
If Pixel_Framebuf's drawing methods took screen rotation into account, adjustments would be made automatically. But that's not what happens. To obtain the image below, adjustments are needed in what we understand by height and width.
'''
Understanding Pixel Position in Matrix Neopixel
'''
import board, time
import neopixel_spi as neopixel
# This is the original version of library when using 16x16 Panels
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
spi = board.SPI()
pixel_pin = board.D10 #board.MOSI
# Setting up the Neopixel Pannel - 16 x 16 Version
how_many = 2
panel_width = 16 # Two Pannels Wide.
panel_height = 16
num_pixels = how_many * panel_width * panel_height
pixels = neopixel.NeoPixel_SPI(
spi,
num_pixels,
brightness=0.2,
auto_write=False,
)
# When 16x16 Using original Adafruit_Pixel_Framebuf Library
screen = PixelFramebuffer(
pixels,
panel_width ,
panel_height * how_many, # What?
rotation = 1, # Simple change rotation...
)
# We need to change this
rect_width = 4 # was 8
rect_height = 8 # was 4
rect_color = 0xFF0000
other_color = 0x00FF00
# And now, the x axis of rectangle is related to screen height
# and y axis of rectangle, is related to screen width
rect_x = (screen.height // 2) - (rect_width //2)
rect_y = (screen.width // 2) - (rect_height //2)
print (rect_x, rect_y)
screen.fill(0) # clear screen
screen.pixel(0,0, 0xFF0000)
screen.pixel(31,15,0x00FF00)
screen.fill_rect (rect_x, rect_y, rect_width, rect_height, rect_color)
screen.rect (rect_x, rect_y, rect_width, rect_height, other_color)
screen.display() # showing the changes
In summary: if you are using more than one panel, when you adjust the rotation of your screen to 90 or 270 degrees, remember to also change the height value, in my case, multiplying the height by the number of panels. At rotations 0 degrees and 180, this is not necessary.
The Pixel_Framebuf drawing methods derive from the Adafruit_Framebuf library and always assume that the screen rotation is at zero. This will change the organization of the width and height attributes of your object, whether it is a simple geometric shape, text or bitmap.
Displaying Bitmaps, with rotation correctionThe code below is an example of implementing screen rotation checking. This routine is being used in my game selection menu, which I share below.
def display_bitmap(self,screen, tile_width, tile_height, bitmap, frame_index=0):
'''
Draw a bitmap or sprite_sheet taking in account the screen rotation
'''
self.screen = screen
bitmap_width = bitmap.width
bitmap_height = bitmap.height
tiles_per_row = bitmap_width // tile_width
tiles_per_column = bitmap_height // tile_height
if tiles_per_row * tiles_per_column > 1:
total_tiles = tiles_per_row * tiles_per_column
if frame_index >= total_tiles:
raise ValueError("Tile index out of range")
tile_x = (frame_index % tiles_per_row) * tile_width
tile_y = (frame_index // tiles_per_row) * tile_height
else:
tile_x = 0
tile_y = 0
for x in range(tile_width):
for y in range(tile_height):
pixel_color = bitmap[tile_x + x, tile_y + y]
# Extract 16 bits RGB Components
r = (pixel_color >> 11) & 0x1F # RED
g = (pixel_color >> 5) & 0x3F # GREEN
b = pixel_color & 0x1F # BLUE
# Convert components from 16 bits to 8 bits (0-255)
r = (r * 255) // 31
g = (g * 255) // 63
b = (b * 255) // 31
# The entire magic happens here.
# This setup will draw a pixel, with rotation corrected
if self.screen.rotation == 0:
self.screen.pixel(x, 31 - y, (r, g, b))
elif self.screen.rotation == 1:
self.screen.pixel(31 - y, 15 - x, (r, g, b))
elif self.screen.rotation == 2:
self.screen.pixel(15 - x, y, (r, g, b))
elif self.screen.rotation == 3:
self.screen.pixel(y, x, (r, g, b))
And the menu system - still in progress...Below is the initial code for my menu system and console structure. This is my code.py file that will call each of the game modules.
I'm now just working on each of the game modules, to make sure that the objects are drawn correctly on the screen. Each of the games already works in isolation and now the goal is to create a complete arcade console system, with six simple games that can be selected via the menu.
I created a Hardware class, which is common to all games. Within this class, it includes the main hardware access methods.
import time, gc, asyncio
import board, random
import neopixel_spi as neopixel
import displayio
import adafruit_imageload
import adafruit_rtttl
# Using HT16K33 as Score and Message Display
from adafruit_ht16k33 import segments
# Treating Joystick
from analogio import AnalogIn
from digitalio import DigitalInOut, Direction, Pull
from simpleio import map_range
# Neopixel as Screen
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
class Hardware():
''' A class to init all hardware on my Console '''
def __init__(self):
# Prepare Joystick in Analog Pins
self.joystick_x = AnalogIn(board.A0)
self.joystick_y = AnalogIn(board.A1)
# Prepare Trigger Switch
self.trigger = DigitalInOut(board.D2)
self.trigger.direction = Direction.INPUT
self.trigger.pull = Pull.UP
# Setup Buzzer. Can be any digital Pin
self.buzzer = board.D3
# This is HT16K33 Four Digits 14 Segment Display
self.display = segments.Seg14x4(board.I2C())
# Init SPI Bus to used it with Neopixels
self.spi = board.SPI()
self.pixel_width = 16
self.pixel_height = 32
self.num_pixels = self.pixel_width * self.pixel_height
self.pixels = neopixel.NeoPixel_SPI(
self.spi,
self.pixel_width * self.pixel_height,
brightness=0.2,
auto_write=False,
)
self.screen = PixelFramebuffer(
self.pixels,
self.pixel_width,
self.pixel_height,
rotation=3, # Default to all games. Get a pattern...
#reverse_x=True,
#orientation=VERTICAL,
)
def play_rttl (self, song):
adafruit_rtttl.play (self.buzzer, song)
def get_joystick(self):
# Returns -1, 0, or 1 depending on joystick position
x_coord = int(map_range(self.joystick_x.value, 200, 65535, -2, 2))
y_coord = int(map_range(self.joystick_y.value, 200, 65535, -2, 2))
return x_coord, y_coord
def get_direction(self):
# This function is a little bit different than usual joystick function
x = int(map_range(self.joystick_x.value, 200, 65535, -1.5, 1.5)) # X up down
y = int(map_range(self.joystick_y.value, 200, 65535, -1.5, 1.5)) # Y Left Right
if abs(x) > abs(y):
return (x, 0) # Horizontal Move
else:
return (0, y) # Vertical Move
def get_pixel_color(self, x, y):
# Check if coordinates are within valid limits
if (0 <= x < self.screen.height) and (0 <= y < self.screen.width):
# Get pixel color
rgbint = self.screen.pixel(x, y)
return (rgbint >> 16 & 0xFF, rgbint >> 8 & 0xFF, rgbint & 0xFF)
# Return black (0, 0, 0) if out of bounds
return (0, 0, 0)
def check_wall(self,x, y, wall_color):
# Check Screen Limits First
if x < 0 or x >= self.screen._height or y < 0 or y >= self.screen._width:
return False
# Then check color
color = self.get_pixel_color(x, y)
return color != wall_color
def check_color(self, x, y, colorcheck):
# Convert colorcheck to RGB tuple if it's an integer
if isinstance(colorcheck, int):
colorcheck = ((colorcheck >> 16) & 0xFF, (colorcheck >> 8) & 0xFF, colorcheck & 0xFF)
color = self.get_pixel_color(x, y)
return color == colorcheck
def display_bitmap(self,tile_width, tile_height, bitmap, frame_index=0):
bitmap_width = bitmap.width
bitmap_height = bitmap.height
tiles_per_row = bitmap_width // tile_width
tiles_per_column = bitmap_height // tile_height
if tiles_per_row * tiles_per_column > 1:
total_tiles = tiles_per_row * tiles_per_column
if frame_index >= total_tiles:
raise ValueError("Tile index out of range")
tile_x = (frame_index % tiles_per_row) * tile_width
tile_y = (frame_index // tiles_per_row) * tile_height
else:
tile_x = 0
tile_y = 0
for x in range(tile_width):
for y in range(tile_height):
pixel_color = bitmap[tile_x + x, tile_y + y]
# Extrair os componentes RGB (vermelho, verde, azul) de 16 bits
r = (pixel_color >> 11) & 0x1F # Componente vermelho
g = (pixel_color >> 5) & 0x3F # Componente verde
b = pixel_color & 0x1F # Componente azul
# Converter os componentes de 16 bits em 8 bits (0-255)
r = (r * 255) // 31
g = (g * 255) // 63
b = (b * 255) // 31
# Ajustar as coordenadas para a tela de 32x16 de acordo com a rotação
if self.screen.rotation == 0:
self.screen.pixel(x, 31 - y, (r, g, b))
elif self.screen.rotation == 1:
self.screen.pixel(31 - y, 15 - x, (r, g, b))
elif self.screen.rotation == 2:
self.screen.pixel(15 - x, y, (r, g, b))
elif self.screen.rotation == 3:
self.screen.pixel(y, x, (r, g, b))
class Menu():
def __init__ (self, hardware):
self.game_list = ['Snake', 'Maze', 'Arkanoid', 'Tetris', 'Space Invaders', 'Enduro']
self.hardware = hardware
self.frame_index = 0
self.joystick_delay = 0.1
self.last_joystick_time = time.monotonic()
# Load the sprite sheet (bitmap) with all games
self.sprite_sheet, self.palette = adafruit_imageload.load(
# Image with 96 pixels width and 32 pixels height. Contains 6 frames 16x32
"images/allGames.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette,
)
self.start_game, self.palette_start = adafruit_imageload.load(
"images/StartGame.bmp",
bitmap=displayio.Bitmap,
palette=displayio.Palette,
)
self.hardware.screen.fill(0)
self.hardware.display_bitmap(16, 32, self.start_game)
self.hardware.screen.display()
#self.hardware.display.marquee ('Select Game ', loop = False)
self.update()
def coinSound(self):
self.hardware.play_rttl ('Coin:d=4,o=6,b=440:c6,8d6,8e6,8f6')
def menuLeft(self):
self.hardware.play_rttl ('menuleft:d=4,o=4,b=600:a4,f4')
def menuRight(self):
self.hardware.play_rttl ('menuright:d=4,o=4,b=600:f4,a4')
def update(self):
while True:
# Limpar a tela
self.hardware.screen.fill(0)
# Exibir o tile atual na tela Neopixel
self.hardware.display_bitmap(16, 32, self.sprite_sheet, self.frame_index)
self.hardware.screen.display()
self.hardware.display.print (self.game_list[self.frame_index][:4])
# Verificar a posição do joystick
current_time = time.monotonic()
if current_time - self.last_joystick_time >= self.joystick_delay:
x_coord, y_coord = self.hardware.get_joystick()
if y_coord == -1: # Joystick para a direita
self.menuRight()
self.frame_index = (self.frame_index + 1) % 6
if self.frame_index > 5:
self.frame_index = 0
self.last_joystick_time = current_time
elif y_coord == 1: # Joystick para a esquerda
self.menuLeft()
if self.frame_index < 0:
self.frame_index = 5
self.frame_index = (self.frame_index - 1) % 6
self.last_joystick_time = current_time
# Verificar se o botão trigger foi pressionado
if not self.hardware.trigger.value: # Botão pressionado (trigger é ativo-baixo)
print(self.game_list[self.frame_index])
self.coinSound()
self.hardware.screen.fill(0)
self.hardware.screen.display()
time.sleep(0.5) # Debounce delay
if self.game_list[self.frame_index] == 'Snake':
from snake_as_class import Snake
snakegame = Snake(self.hardware)
snakegame.play()
elif self.game_list[self.frame_index] == 'Maze':
gc.collect()
from maze_as_class import MazeGame
mazegame = MazeGame(self.hardware)
mazegame.play()
elif self.game_list[self.frame_index] == 'Arkanoid':
gc.collect()
from arkanoid_as_class import Arkanoid
arkanoid = Arkanoid(self.hardware)
arkanoid.play()
else:
break
hardware = Hardware()
menu = Menu (hardware)
Comments