Files
weather-info/app/display/neopixel_64x64.py
2025-10-23 19:06:30 +02:00

353 lines
9.0 KiB
Python

from machine import Pin, RTC
from neopixel import NeoPixel
from .fonts.fonts_utils import char_width as charwidth
from .fonts.font_5x7 import font_5x7
from ..utils.colors import GRAY, RAINBOW, BLACK, WHITE, LIME
from ..utils.utils import (
get_german_timestamp_short,
get_datetime_string,
get_german_time_ticks,
get_german_date_ticks,
number_to_bitarray_msb,
)
import time
import math
class NeoPixel_64x64(NeoPixel):
MATRIX_WIDTH = 64
MATRIX_HEIGHT = 64
def __init__(self, pin=28):
"""
Initialize LED Display
Args:
width: Matrix width in pixels
height: Matrix height in pixels
pin: NeoPixel data pin
"""
super().__init__(Pin(pin), self.MATRIX_WIDTH * self.MATRIX_HEIGHT)
# Font configuration
self.set_font(font_5x7)
def set_font(self, font):
"""
Set the current font for text rendering
Args:
font: Font dictionary from fonts_array
"""
self.selected_font = font
# Höhe des Fonts, Anzahl der Zeilen für jedes Zeichen
# wir holen den Wert aus "A"
self.font_height = len(self.selected_font['A'])
print(f'Font set: width: per char; height: {self.font_height}')
def set_pixel(self, x, y, color):
"""
Set a single pixel to the specified color
Args:
x: X coordinate
y: Y coordinate
color: RGB color tuple
"""
index = y * self.MATRIX_WIDTH + x
if 0 <= x < self.MATRIX_WIDTH and 0 <= y < self.MATRIX_HEIGHT:
self[index] = color
def clear(self):
"""Clear the entire display (turn off all pixels)"""
for i in range(len(self)):
self[i] = BLACK
self.write()
def clear_row(self, row: int, effect: bool = False) -> None:
"""
Clear one row of the display (turn off all pixels)\n
PRESUMING: font_5x7\n
rows: range(0,3) [row-height: 8 pixels, row-length: MATRIX_WIDTH(64)]\n
\n
row-0: 0 - 511 (Pixel 0 bis Pixel 8 * MATRIX_WIDTH(64)-1)\n
row-1: 512 - 1023\n
row-2: 1024 - 1535\n
row-3: 1536 - 2047\n
etc. ...
"""
start = row * 8 * self.MATRIX_WIDTH
ende = start + 8 * self.MATRIX_WIDTH - 1
print(f'clear row: {row} --> pixels {start} to {ende}')
for i in range(start, ende):
self[i] = BLACK
if effect and i % self.MATRIX_WIDTH == 0:
self.write()
self.write()
def draw_letter(self, letter, x, y, color):
"""
Draw a single letter using current font with optimized width
Args:
letter: Character to draw
x: X position
y: Y position
color: RGB color tuple
"""
if letter in self.selected_font:
char_data = self.selected_font[letter]
char_width = charwidth(char_data)
# background for the letter (full font size)
[
# print(xpos, ypos)
self.set_pixel(xpos, ypos, GRAY)
for xpos in range(
x, x + char_width
) # 8 because full with of character representation
for ypos in range(y, y + self.font_height)
]
for row in range(self.font_height):
row_data = char_data[row]
for col in range(char_width):
# Check if pixel should be lit (MSB first)
# Only check bits within the actual character width
if row_data & (1 << ((char_width - 1) - col)):
self.set_pixel(x + col, y + row, color)
else:
print(f'oops, letter does not exist in the font -> {letter}')
def draw_text(self, text, x, y, color):
"""
Draw text with optimized character spacing
Args:
text: Text to draw
x: Starting X position
y: Starting Y position
color: RGB color tuple
"""
current_x = x
for char in text:
self.draw_letter(char, current_x, y, color)
# Move cursor by character width + 1 pixel spacing
char_width = charwidth(self.selected_font[char])
current_x += char_width + 1
def show_hello(self):
"""Display HELLO with timestamp"""
self.clear()
# Draw HELLO in rainbow colors
self.draw_text('HELLO!', 6, 4, RAINBOW[2])
# Show timestamp
datetimestr = get_german_timestamp_short()
self.draw_text(datetimestr, 2, 15, RAINBOW[4])
self.write()
def vertical_floating_text(
self, text, x, color=RAINBOW[0], float_range=3, speed=0.2, duration=10
):
"""
Vertical floating text animation
Args:
text: Text to display
x: X position (fixed)
color: Text color
float_range: How many pixels to float up/down
speed: Animation speed
duration: How long to run animation in seconds
"""
start_time = time.time()
while time.time() - start_time < duration:
# Calculate floating offset using sine wave
offset = math.sin(time.time() * speed) * float_range
current_y = int(self.MATRIX_HEIGHT // 2 + offset - (len(text) * self.font_height) // 2)
self.clear()
# Draw each letter vertically
for i, char in enumerate(text):
char_y = current_y + (i * self.font_height)
# Keep text within matrix bounds
if 0 <= char_y < self.MATRIX_HEIGHT - self.font_height:
self.draw_letter(char, x, char_y, color)
self.write()
time.sleep(0.05)
def horizontal_floating_text(
self, text, y, color=RAINBOW[0], float_range=3, speed=0.2, duration=10
):
"""
Horizontal floating text animation
Args:
text: Text to display
y: Y position (fixed)
color: Text color
float_range: How many pixels to float left/right
speed: Animation speed
duration: How long to run animation in seconds
"""
start_time = time.time()
counter = 0
while time.time() - start_time < duration:
# Calculate floating offset using sine wave
offset = math.sin(counter) * float_range
current_x = int(offset) # to right
self.clear()
# Draw text at floating position
for i, char in enumerate(text):
char_width = charwidth(self.selected_font[char])
char_x = current_x + (i * (char_width + 1))
# Keep text within matrix bounds
if 0 <= char_x < self.MATRIX_WIDTH - char_width:
self.draw_letter(char, char_x, y, color)
self.write()
counter += speed
time.sleep(0.05)
def rotate_text_left_continuous(self, text, y, color=RAINBOW[0], speed=1.0, duration=10):
"""
Continuous left rotation - text wraps around seamlessly
Args:
text: Text to display
y: Y position (fixed)
color: Text color
speed: Movement speed
duration: How long to run animation in seconds
"""
start_time = time.time()
# hier müssen wir die Länge/Breite in Pixel des gesamten Textes berechnen
# text_width_overall = len(text) * (self.font_width + 1)
### TODO: ACHTUNG: noch zu testen !!!
# Breite jedes Zeichens
text_width_overall = sum([charwidth(self.selected_font[char]) for char in text])
position = 0
while time.time() - start_time < duration:
self.clear()
# Draw text at current position
for i, char in enumerate(text):
char_width = charwidth(self.selected_font[char])
char_x = int(position + (i * (char_width + 1)))
# Handle wrapping
if char_x < -char_width:
char_x += self.MATRIX_WIDTH + text_width_overall
if 0 <= char_x < self.MATRIX_WIDTH:
self.draw_letter(char, char_x, y, color)
self.write()
# Move left and wrap around
position -= speed
if position < -text_width_overall:
position = self.MATRIX_WIDTH
time.sleep(0.05)
def draw_rectangle(self, x, y, width, height, color, fill=False):
"""
Draw a rectangle
Args:
x: Top-left X position
y: Top-left Y position
width: Rectangle width
height: Rectangle height
color: RGB color tuple
fill: Whether to fill the rectangle
"""
if fill:
for i in range(height):
for j in range(width):
self.set_pixel(x + j, y + i, color)
else:
# Top and bottom edges
for i in range(width):
self.set_pixel(x + i, y, color)
self.set_pixel(x + i, y + height - 1, color)
# Left and right edges
for i in range(height):
self.set_pixel(x, y + i, color)
self.set_pixel(x + width - 1, y + i, color)
def draw_line(self, x1, y1, x2, y2, color):
"""
Draw a line using Bresenham's algorithm
Args:
x1, y1: Start coordinates
x2, y2: End coordinates
color: RGB color tuple
"""
dx = abs(x2 - x1)
dy = abs(y2 - y1)
sx = 1 if x1 < x2 else -1
sy = 1 if y1 < y2 else -1
err = dx - dy
while True:
self.set_pixel(x1, y1, color)
if x1 == x2 and y1 == y2:
break
e2 = 2 * err
if e2 > -dy:
err -= dy
x1 += sx
if e2 < dx:
err += dx
y1 += sy
def write_text(self, text: str, xpos: int, ypos: int, color=WHITE) -> None:
self.draw_text(text, xpos, ypos, color) # Pixel setzen
self.write() # und anzeigen
# Example usage
if __name__ == '__main__':
# Create display instance
display = NeoPixel_64x64()
print('LED Matrix Display Initialized')
print(get_datetime_string())
print(f'German time: {get_german_time_ticks()}')
print(f'German date: {get_german_date_ticks()}')
# Demo various functions
display.show_hello()
time.sleep(3)
display.vertical_floating_text('HELLO', 30, RAINBOW[0], 5, 0.15, 5)
display.horizontal_floating_text('FLOAT', 28, RAINBOW[1], 10, 0.1, 5)
display.rotate_text_left_continuous('ROTATE FLOAT', 28, speed=8.0, duration=5)
# Draw some shapes
display.clear()
display.draw_rectangle(5, 5, 10, 10, RAINBOW[0])
display.draw_rectangle(20, 20, 15, 8, RAINBOW[2], fill=True)
display.draw_line(0, 0, 63, 63, RAINBOW[4])
display.write()