diff --git a/app/display/neopixel_64x64.py b/app/display/neopixel_64x64.py index de406ea..caffd87 100644 --- a/app/display/neopixel_64x64.py +++ b/app/display/neopixel_64x64.py @@ -1,390 +1,418 @@ from machine import Pin # type: ignore from neopixel import NeoPixel # type: ignore -from ..utils import char_width +from app.utils import char_width -from .fonts.font_5x7 import font_5x7 -from ..utils.colors import GRAY, RAINBOW, BLACK, WHITE -from ..utils.time_utils import ( - get_german_timestamp_short, - get_datetime_string, - get_german_time_ticks, - get_german_date_ticks, +from app.display import fonts +from app.utils import colors +from app.utils.time_utils import ( + get_german_timestamp_short, + get_datetime_string, + get_german_time_ticks, + get_german_date_ticks, ) import time import math class NeoPixel_64x64(NeoPixel): - MATRIX_WIDTH = 64 - MATRIX_HEIGHT = 64 + MATRIX_WIDTH = 64 + MATRIX_HEIGHT = 64 - def __init__(self, pin=28): - """ - Initialize LED Display + 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) + 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) + # Font configuration + self.set_font(fonts.font_5x7) - def set_font(self, font): - """ - Set the current font for text rendering + def set_font(self, font): + """ + Set the current font for text rendering - Args: - font: Font dictionary from fonts_array - """ - self.selected_font = font + 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 ersten Wert des Fonts - first_char = next(iter(self.selected_font)) - self.font_height = len(self.selected_font[first_char]) + # Höhe des Fonts, Anzahl der Zeilen für jedes Zeichen + # wir holen den ersten Wert des Fonts + first_char = next(iter(self.selected_font)) + self.font_height = len(self.selected_font[first_char]) - def set_pixel(self, x, y, color): - """ - Set a single pixel to the specified color + 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 + 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(self): + """Clear the entire display (turn off all pixels)""" + for i in range(len(self)): + self[i] = colors.BLACK + self.write() - def clear_row(self, row: int, effect: bool = False) -> None: - """löscht eine Zeile im Display entsprechend des eingestellten Fonts + def clear_box( + self, + from_row: int, + from_col: int, + to_row: int, + to_col: int, + color: tuple[3] = colors.BLACK, + ) -> None: + """löscht einen Bereich (Box) im Display - Args: - row (int): in Pixel start bei 0 !!! - effect (bool, optional): zeilenweise bzw. pixelweise löschen. Defaults to False. - """ - start = row * self.MATRIX_WIDTH - ende = start + self.font_height * self.MATRIX_WIDTH - 1 + Args: + from_row (int): Start-Zeile + from_col (int): Start-Spalte + to_row (int): End-Zeile + to_col (int): End-Spalte + """ + for row in range(from_row, to_row): + for col in range(from_col, to_col + 1): + idx: int = row * self.MATRIX_WIDTH + col + self[idx] = color - 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() - self.write() + def clear_row(self, row: int, effect: bool = False) -> None: + """löscht eine Zeile im Display entsprechend des eingestellten Fonts - def draw_letter(self, letter, x, y, color): - """ - Draw a single letter using current font with optimized width + Args: + row (int): in Pixel start bei 0 !!! + effect (bool, optional): zeilenweise bzw. pixelweise löschen. Defaults to False. + """ + start = row * self.MATRIX_WIDTH + ende = start + self.font_height * self.MATRIX_WIDTH - 1 - Args: - letter: Character to draw - x: X position - y: Y position - color: RGB color tuple - """ + print(f"clear row: {row} --> pixels {start} to {ende}") + for i in range(start, ende): + self[i] = colors.BLACK + if effect and i % self.MATRIX_WIDTH == 0: + self.write() - if letter in self.selected_font: - char_data = self.selected_font[letter] - charwidth = char_width(char_data) + self.write() - # background for the letter (full font size) - [ - # print(xpos, ypos) - self.set_pixel(xpos, ypos, GRAY) - for xpos in range( - x, x + charwidth - ) # 8 because full with of character representation - for ypos in range(y, y + self.font_height) - ] + def draw_letter(self, letter, x, y, color): + """ + Draw a single letter using current font with optimized width - for row in range(self.font_height): - row_data = char_data[row] + Args: + letter: Character to draw + x: X position + y: Y position + color: RGB color tuple + """ - for col in range(charwidth): - # Check if pixel should be lit (MSB first) - # Only check bits within the actual character width - if row_data & (1 << ((charwidth - 1) - col)): - self.set_pixel(x + col, y + row, color) - else: - print(f'oops, letter does not exist in the font -> {letter}') + if letter in self.selected_font: + char_data = self.selected_font[letter] + charwidth = char_width(char_data) - def draw_text(self, text, x, y, color): - """ - Draw text with optimized character spacing + # background for the letter (full font size) + [ + # print(xpos, ypos) + self.set_pixel(xpos, ypos, colors.GRAY) + for xpos in range( + x, x + charwidth + ) # 8 because full with of character representation + for ypos in range(y, y + self.font_height) + ] - 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 - charwidth = char_width(self.selected_font[char]) - current_x += charwidth + 1 + for row in range(self.font_height): + row_data = char_data[row] - def show_hello(self): - """Display HELLO with timestamp""" - self.clear() + for col in range(charwidth): + # Check if pixel should be lit (MSB first) + # Only check bits within the actual character width + if row_data & (1 << ((charwidth - 1) - col)): + self.set_pixel(x + col, y + row, color) + else: + print(f"oops, letter does not exist in the font -> {letter}") - # Draw HELLO in rainbow colors - self.draw_text('HELLO!', 6, 4, RAINBOW[2]) + def draw_text(self, text, x, y, color): + """ + Draw text with optimized character spacing - # Show timestamp - datetimestr = get_german_timestamp_short() - self.draw_text(datetimestr, 2, 15, RAINBOW[4]) + 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 + charwidth = char_width(self.selected_font[char]) + current_x += charwidth + 1 - self.write() + def show_hello(self): + """Display HELLO with timestamp""" + self.clear() - def vertical_floating_text( - self, text, x, color=RAINBOW[0], float_range=3, speed=0.2, duration=10 - ): - """ - Vertical floating text animation + # Draw HELLO in rainbow colors + self.draw_text("HELLO!", 6, 4, colors.RAINBOW[2]) - 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() + # Show timestamp + datetimestr = get_german_timestamp_short() + self.draw_text(datetimestr, 2, 15, colors.RAINBOW[4]) - 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.write() - self.clear() + def vertical_floating_text( + self, text, x, color=colors.RAINBOW[0], float_range=3, speed=0.2, duration=10 + ): + """ + Vertical floating text animation - # 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) + 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() - self.write() - time.sleep(0.05) + 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 + ) - def horizontal_floating_text( - self, text, y, color=RAINBOW[0], float_range=3, speed=0.2, duration=10 - ): - """ - Horizontal floating text animation + self.clear() - 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 + # 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) - 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.write() + time.sleep(0.05) - self.clear() + def horizontal_floating_text( + self, text, y, color=colors.RAINBOW[0], float_range=3, speed=0.2, duration=10 + ): + """ + Horizontal floating text animation - # Draw text at floating position - for i, char in enumerate(text): - charwidth = char_width(self.selected_font[char]) - char_x = current_x + (i * (charwidth + 1)) - # Keep text within matrix bounds - if 0 <= char_x < self.MATRIX_WIDTH - charwidth: - self.draw_letter(char, char_x, y, color) + 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 - self.write() - counter += speed - time.sleep(0.05) + while time.time() - start_time < duration: + # Calculate floating offset using sine wave + offset = math.sin(counter) * float_range + current_x = int(offset) # to right - def rotate_text_left_continuous(self, text, y, color=RAINBOW[0], speed=1.0, duration=10): - """ - Continuous left rotation - text wraps around seamlessly + self.clear() - 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([char_width(self.selected_font[char]) for char in text]) + # Draw text at floating position + for i, char in enumerate(text): + charwidth = char_width(self.selected_font[char]) + char_x = current_x + (i * (charwidth + 1)) + # Keep text within matrix bounds + if 0 <= char_x < self.MATRIX_WIDTH - charwidth: + self.draw_letter(char, char_x, y, color) - position = 0 + self.write() + counter += speed + time.sleep(0.05) - while time.time() - start_time < duration: - self.clear() + def rotate_text_left_continuous( + self, text, y, color=colors.RAINBOW[0], speed=1.0, duration=10 + ): + """ + Continuous left rotation - text wraps around seamlessly - # Draw text at current position - for i, char in enumerate(text): - charwidth = char_width(self.selected_font[char]) - char_x = int(position + (i * (charwidth + 1))) - # Handle wrapping - if char_x < -charwidth: - char_x += self.MATRIX_WIDTH + text_width_overall - if 0 <= char_x < self.MATRIX_WIDTH: - self.draw_letter(char, char_x, y, color) + 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( + [char_width(self.selected_font[char]) for char in text] + ) - self.write() + position = 0 - # Move left and wrap around - position -= speed - if position < -text_width_overall: - position = self.MATRIX_WIDTH + while time.time() - start_time < duration: + self.clear() - time.sleep(0.05) + # Draw text at current position + for i, char in enumerate(text): + charwidth = char_width(self.selected_font[char]) + char_x = int(position + (i * (charwidth + 1))) + # Handle wrapping + if char_x < -charwidth: + char_x += self.MATRIX_WIDTH + text_width_overall + if 0 <= char_x < self.MATRIX_WIDTH: + self.draw_letter(char, char_x, y, color) - def draw_rectangle(self, x, y, width, height, color, fill=False): - """ - Draw a rectangle + self.write() - 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) + # Move left and wrap around + position -= speed + if position < -text_width_overall: + position = self.MATRIX_WIDTH - def draw_line(self, x1, y1, x2, y2, color): - """ - Draw a line using Bresenham's algorithm + time.sleep(0.05) - 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 + def draw_rectangle(self, x, y, width, height, color, fill=False): + """ + Draw a rectangle - 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 + 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 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 + def draw_line(self, x1, y1, x2, y2, color): + """ + Draw a line using Bresenham's algorithm - def screen_text(self, text: str): - """Text für einen Screen anpassen, - Anzahl der Zeichen je Zeile begrenzen, - ebenso wird die Höhe des Screen brücksichtigt + 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=colors.WHITE) -> None: + self.draw_text(text, xpos, ypos, color) # Pixel setzen + self.write() # und anzeigen + + def screen_text(self, text: str): + """Text für einen Screen anpassen, + Anzahl der Zeichen je Zeile begrenzen, + ebenso wird die Höhe des Screen brücksichtigt - Args: - text (str): Text der ausgegeben werden soll - font: Berechnungen für den Font - height (int): Pixel - width (int): Pixel - """ + Args: + text (str): Text der ausgegeben werden soll + font: Berechnungen für den Font + height (int): Pixel + width (int): Pixel + """ - def text_per_row(txt): - pixs = 0 - visible_text = '' - for a in txt: - pixs += char_width(self.selected_font[a]) + 1 - if pixs > self.MATRIX_WIDTH: - # Zeilenende erreicht - break - visible_text += a + def text_per_row(txt): + pixs = 0 + visible_text = "" + for a in txt: + pixs += char_width(self.selected_font[a]) + 1 + if pixs > self.MATRIX_WIDTH: + # Zeilenende erreicht + break + visible_text += a - return visible_text + return visible_text - # Ganzzahl Division - max_visible_rows = self.MATRIX_HEIGHT // self.font_height - print(f'rows_visible: {max_visible_rows}') + # Ganzzahl Division + max_visible_rows = self.MATRIX_HEIGHT // self.font_height + print(f"rows_visible: {max_visible_rows}") - text_left = text - scn_txt = [] - for _ in range(max_visible_rows): - visible_text = text_per_row(text_left) - visible_text_len = len(visible_text) - text_left = text_left[visible_text_len:] + text_left = text + scn_txt = [] + for _ in range(max_visible_rows): + visible_text = text_per_row(text_left) + visible_text_len = len(visible_text) + text_left = text_left[visible_text_len:] - scn_txt.append(visible_text) + scn_txt.append(visible_text) - if not text_left: - break + if not text_left: + break - scr_txt_dict = {'visible': scn_txt, 'invisible': text_left} - - return scr_txt_dict + scr_txt_dict = {"visible": scn_txt, "invisible": text_left} + return scr_txt_dict # Example usage -if __name__ == '__main__': - # Create display instance - display = NeoPixel_64x64() +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()}') + 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) + # 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) + display.vertical_floating_text("HELLO", 30, colors.RAINBOW[0], 5, 0.15, 5) + display.horizontal_floating_text("FLOAT", 28, colors.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() + # Draw some shapes + display.clear() + display.draw_rectangle(5, 5, 10, 10, colors.RAINBOW[0]) + display.draw_rectangle(20, 20, 15, 8, colors.RAINBOW[2], fill=True) + display.draw_line(0, 0, 63, 63, colors.RAINBOW[4]) + display.write() diff --git a/main.py b/main.py index dd69886..730d880 100644 --- a/main.py +++ b/main.py @@ -35,10 +35,12 @@ async def print_time_task() -> None: bottom_ypos = display.MATRIX_HEIGHT - display.font_height time_str: str = get_datetime_string("time") - display.clear_row(bottom_ypos) + display.clear_box(bottom_ypos, 0, bottom_ypos + display.font_height, 38) + + # display.clear_row(bottom_ypos) display.write_text(time_str[:8], 0, bottom_ypos, color=colors.NEON_YELLOW) - await asyncio.sleep(10) + await asyncio.sleep(5) async def sync_ntp_time_task() -> None: