v.0.10.0 redesign prj structure, multi dev containers

This commit is contained in:
tiijay
2025-11-20 11:50:19 +00:00
parent 53b1b96fb3
commit 76a8203458
52 changed files with 63 additions and 83 deletions

View File

@@ -0,0 +1,8 @@
from .system_load import show_system_load
from .colors import *
from .time_utils import *
from .font_utils import *
from .math_utils import *
from .url_encode import URLEncoder
from .http_utils import http_message
from .simple_counter import SimpleCounter

View File

@@ -0,0 +1,98 @@
# colors.py - Color library for LED matrix
# Basic Colors
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# Primary Colors
YELLOW = (255, 255, 0)
MAGENTA = (255, 0, 255)
CYAN = (0, 255, 255)
# White and Black
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
# Grayscale
GRAY = (128, 128, 128)
LIGHT_GRAY = (192, 192, 192)
DARK_GRAY = (64, 64, 64)
# Warm Colors
ORANGE = (255, 165, 0)
PINK = (255, 192, 203)
HOT_PINK = (255, 105, 180)
CORAL = (255, 127, 80)
TOMATO = (255, 99, 71)
# Cool Colors
PURPLE = (128, 0, 128)
INDIGO = (75, 0, 130)
VIOLET = (238, 130, 238)
LAVENDER = (230, 230, 250)
# Earth Tones
BROWN = (165, 42, 42)
CHOCOLATE = (210, 105, 30)
SANDY_BROWN = (244, 164, 96)
GOLD = (255, 215, 0)
# Bright Colors
LIME = (0, 255, 0)
AQUA = (0, 255, 255)
TURQUOISE = (64, 224, 208)
SPRING_GREEN = (0, 255, 127)
# Pastel Colors
PASTEL_RED = (255, 128, 128)
PASTEL_GREEN = (128, 255, 128)
PASTEL_BLUE = (128, 128, 255)
PASTEL_YELLOW = (255, 255, 128)
PASTEL_PURPLE = (255, 128, 255)
PASTEL_CYAN = (128, 255, 255)
# Neon Colors
NEON_RED = (255, 0, 0)
NEON_GREEN = (57, 255, 20)
NEON_BLUE = (0, 0, 255)
NEON_YELLOW = (255, 255, 0)
NEON_PINK = (255, 0, 128)
NEON_ORANGE = (255, 128, 0)
# Rainbow Colors (for rainbow effects)
RAINBOW = [
(255, 0, 0), # Red
(255, 127, 0), # Orange
(255, 255, 0), # Yellow
(0, 255, 0), # Green
(0, 0, 255), # Blue
(75, 0, 130), # Indigo
(148, 0, 211), # Violet
]
# Holiday Colors
CHRISTMAS_RED = (255, 0, 0)
CHRISTMAS_GREEN = (0, 255, 0)
HALLOWEEN_ORANGE = (255, 140, 0)
HALLOWEEN_PURPLE = (128, 0, 128)
# Utility function to create custom colors
def rgb(r, g, b):
"""Create a color from RGB values (0-255)"""
return (r, g, b)
def hex_to_rgb(hex_color):
"""Convert hex color (#RRGGBB) to RGB tuple"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
def fade_color(color1, color2, factor):
"""Fade between two colors (factor 0.0 to 1.0)"""
r = int(color1[0] + (color2[0] - color1[0]) * factor)
g = int(color1[1] + (color2[1] - color1[1]) * factor)
b = int(color1[2] + (color2[2] - color1[2]) * factor)
return (r, g, b)

View File

@@ -0,0 +1,58 @@
from display.neopixel_64x64 import NeoPixel_64x64
from utils import get_datetime_string, text_width, find_first_mismatch
from utils import colors
class DigitalClock:
stored_time_str: str = "99:99:99"
text_color: tuple[int[3]] = colors.NEON_YELLOW
clear_color: tuple[int[3]] = colors.BLACK
def __init__(self, display: NeoPixel_64x64, xpos: int, ypos: int):
self.display = display
self.xpos = xpos
self.ypos = ypos
def _toggle_clear_color(self) -> tuple[int]:
self.clear_color = (
colors.BLACK if self.clear_color != colors.BLACK else colors.MAGENTA
)
return self.clear_color
def _text_width(self, text: str) -> int:
return text_width(text, self.display.selected_font)
async def tick(self):
time_str: str = get_datetime_string("time")[:8]
# ab welcher Position malen wir den String neu
mismatch_pos: int = find_first_mismatch(self.stored_time_str, time_str)
untouched_part: str = time_str[:mismatch_pos]
fresh_part: str = time_str[mismatch_pos:]
part_to_clear: str = self.stored_time_str[mismatch_pos:]
print(f"untouched_part: {untouched_part}[{part_to_clear}-->{fresh_part}] ")
textwidth: int = self._text_width(self.stored_time_str)
print(f"{self.stored_time_str}: #{textwidth}")
textwidth_untouched_part: int = self._text_width(untouched_part)
clear_x_start_pos: int = self.xpos + textwidth_untouched_part + 1
textwidth_part_to_clear: int = self._text_width(part_to_clear)
clear_x_end_pos: int = clear_x_start_pos + textwidth_part_to_clear - 1
self.display.clear_box(
self.ypos,
clear_x_start_pos,
self.ypos + self.display.font_height,
clear_x_end_pos,
color=self.clear_color,
)
self.display.write_text(
fresh_part, clear_x_start_pos, self.ypos, color=self.text_color
)
self.stored_time_str = time_str

View File

@@ -0,0 +1,108 @@
from .math_utils import show_byte_matrix
def text_width(text: str, font: dict[str, list[int]]) -> int:
text_width_overall: int = sum([char_width(font[char]) for char in text])
return text_width_overall + len(text) - 1
def char_width(char_matrix) -> int:
"""Berechnung der Bits für die Zeichenbreite
Args:
char_matrix (int): Zeichen als Array[int]
Returns:
int: Anzahl Bits für die Zeichenbreite
"""
max_val = max(char_matrix)
val = max_val
cw = 0
while 0xFFFFFFFF & val:
"""rechts shiften, bis alles Nullen da sind"""
val >>= 1
cw += 1
return cw
def shift_letter_right(char, letter, debug=False) -> bool:
"""Prüfe ob das Zeichen auch komplett nach rechts gezogen ist
Args:
letter (_type_): Array[0..nBytes]
Returns:
bool: _description_
"""
def isLetterRight(letter):
def isByteRight(byte):
return True if 1 & byte == 1 else False
for byte in letter:
if isByteRight(byte):
return True
return False
def shiftLetterRight(letter):
def shift(letter):
return [byte >> 1 for byte in letter]
shifted = letter
for i in range(8): # max 1 Bit's
shifted = shift(shifted)
if isLetterRight(shifted):
break
return shifted
if isLetterRight(letter):
return letter
# letter is not right shifted
shifted = shiftLetterRight(letter)
if debug:
print("origin:")
show_byte_matrix(char, letter)
print("shifted")
show_byte_matrix(char, shifted)
return shifted
def align_font(font, debug=False):
chars = [char for char in font]
print(chars)
# Print the dictionary
print("font_pretty = {")
for char in sorted(font.keys()):
shifted = shift_letter_right(char=char, letter=font[char], debug=debug)
hex_values = [f"0x{val:02X}" for val in shifted]
print(f"\t'{char}': [{', '.join(hex_values)}],")
print("}")
# return f'#keys: {len(list(font.keys()))}'
return f"#keys: {len(font)}"
def find_first_mismatch(str1: str, str2: str) -> int:
"""
Compare two strings of equal length and return the position of first mismatch.
Args:
str1 (str): First string
str2 (str): Second string (same length as str1)
Returns:
int: Position of first mismatch, or -1 if strings are identical
"""
for i in range(len(str1)):
if str1[i] != str2[i]:
return i
return -1

View File

@@ -0,0 +1,6 @@
http_message = {
# Jedes Zeichen ist genau 3 Pixel breit, 5 Pixel hoch
200: "OK",
400: "Bad Request",
401: "Unauthorized",
}

View File

@@ -0,0 +1,9 @@
def show_byte_matrix(char, matrix):
print(f'byte_matrix: char({char})')
matrix_str = [f'0x{byte:02X}' for byte in matrix]
[print(f'{matrix_str[idx]} {number_to_bitarray_msb(byte)}') for idx, byte in enumerate(matrix)]
def number_to_bitarray_msb(number, bits=8):
"""Convert 8/16-bit number to bit array (MSB first)"""
return [(number >> i) & 1 for i in range(bits - 1, -1, -1)]

View File

@@ -0,0 +1,39 @@
class SimpleCounter:
_value: int
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._value = value
# Arithmetic operators
def __add__(self, other):
return self._value + other
def __sub__(self, other):
return self._value - other
# In-place operators
def __iadd__(self, other):
self._value += other
return self
def __isub__(self, other):
self._value -= other
return self
# Conversion and representation
def __int__(self):
return self._value
def __str__(self):
return str(self._value)
def __repr__(self):
return f"SimpleCounter(value={self._value})"

View File

@@ -0,0 +1,42 @@
import gc
import time
def show_system_load():
"""Display basic system load information"""
gc.collect() # Run garbage collection first
mem_free = gc.mem_free()
mem_alloc = gc.mem_alloc()
mem_total = mem_free + mem_alloc
mem_usage = (mem_alloc / mem_total) * 100 if mem_total > 0 else 0
print('=== System Load ===')
print(f'Memory: {mem_alloc}/{mem_total} bytes ({mem_usage:.1f}%)')
print(f'Free: {mem_free} bytes')
print(f'Garbage: {gc.mem_free() - mem_free} bytes')
try:
# CPU load approximation (not available on all ports)
start_time = time.ticks_ms()
# Do some work to measure CPU
for i in range(1000):
_ = i * i
end_time = time.ticks_ms()
cpu_time = time.ticks_diff(end_time, start_time)
print(f'CPU load: ~{cpu_time}ms for 1000 iterations')
except (AttributeError, TypeError, ValueError) as e:
print(f'CPU measurement not available: {e}')
print('==================')
def go():
# Run periodically
while True:
show_system_load()
time.sleep(5)
if __name__ == '__main__':
go()

View File

@@ -0,0 +1,136 @@
import time
import ntptime # type: ignore
_UTC_OFFSET: int = 1 * 3600 # +1 or +2 hours for CEST, adjust for your timezone
def local_time_with_offset() -> int:
# Set timezone offset (in seconds)
current_time = time.time() + _UTC_OFFSET
return time.localtime(current_time)
def get_datetime_string(format="full") -> str:
"""
Return date/time as string with different formats
Args:
format: "full", "date", "time", "short", "ticks"
"""
try:
year, month, day, hour, minute, second, weekday, yearday = (
local_time_with_offset()
)
if format == "full":
return (
f"{year:04d}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
)
elif format == "date":
return f"{year:04d}-{month:02d}-{day:02d}"
elif format == "time":
return f"{hour:02d}:{minute:02d}:{second:02d}"
elif format == "short":
return f"{month:02d}/{day:02d} {hour:02d}:{minute:02d}"
else:
return f"Ticks: {time.ticks_ms()} ms"
except:
return f"Ticks: {time.ticks_ms()} ms"
def get_german_datetime() -> str:
"""Return German date and time"""
try:
year, month, day, hour, minute, second, weekday, yearday = (
local_time_with_offset()
)
weekdays = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
weekday_name = weekdays[weekday]
return f"{weekday_name}.{day:02d}.{month:02d}.{year} {hour:02d}:{minute:02d}"
except:
ticks = time.ticks_ms() // 1000
return f"Zeit: {ticks} sek"
def get_german_timestamp_short() -> str:
"""Get German timestamp with short months (for Wokwi)"""
ticks = time.ticks_ms()
day = (ticks // 86400000) % 31 + 1
month = (ticks // 2592000000) % 12 + 1
hour = (ticks // 3600000) % 24
minute = (ticks // 60000) % 60
months = [
"Jan",
"Feb",
"Mär",
"Apr",
"Mai",
"Jun",
"Jul",
"Aug",
"Sep",
"Okt",
"Nov",
"Dez",
]
month_name = months[month - 1]
return f"{day}.{month_name} {hour:02d}:{minute:02d}"
def get_german_time_ticks() -> str:
"""Get German time using ticks (works in Wokwi)"""
ticks = time.ticks_ms()
# Simulate time progression
total_seconds = ticks // 1000
hours = (total_seconds // 3600) % 24
minutes = (total_seconds // 60) % 60
seconds = total_seconds % 60
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
def get_german_date_ticks() -> str:
"""Get German date using ticks"""
ticks = time.ticks_ms()
# Simulate date progression (starting from Jan 15, 2024)
days_passed = ticks // (24 * 3600 * 1000)
day = 15 + (days_passed % 28) # Simple month simulation
month = 1 + (days_passed // 28) % 12
year = 2024 + (days_passed // (28 * 12))
months = [
"JAN",
"FEB",
"MAR",
"APR",
"MAI",
"JUN",
"JUL",
"AUG",
"SEP",
"OKT",
"NOV",
"DEZ",
]
month_name = months[month - 1]
return f"{day:02d}.{month_name}.{str(year)[-2:]}"
def sync_ntp_time() -> bool:
try:
print("Syncing time via NTP...")
ntptime.settime() # Default uses pool.ntp.org
print("Time synchronized successfully!")
return True
except Exception as e:
print("NTP sync failed:", e)
return False

View File

@@ -0,0 +1,68 @@
class URLEncoder:
@staticmethod
def encode(string):
"""URL encode a string without using string methods"""
result = []
safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
for char in string:
if char in safe_chars:
result.append(char)
else:
# result.append('%' + format(ord(char), '02X'))
# Converted to % formatting:
special_char = "%%%02X" % ord(char)
print( f"{char} --> {special_char}")
result.append(special_char)
return ''.join(result)
@staticmethod
def encode_utf8(string):
"""URL encode a string with proper UTF-8 handling"""
result = []
safe_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"
for char in string:
if char in safe_chars:
result.append(char)
else:
# UTF-8 encoding for proper URL encoding
utf8_bytes = char.encode('utf-8')
for byte in utf8_bytes:
result.append(f"%{byte:02X}")
return ''.join(result)
@staticmethod
def encode_plus(string):
encoded = URLEncoder.encode(string)
return encoded.replace('%20', '+')
@staticmethod
def decode(encoded_string):
result = []
i = 0
while i < len(encoded_string):
if encoded_string[i] == '%' and i + 2 < len(encoded_string):
hex_code = encoded_string[i+1:i+3]
result.append(chr(int(hex_code, 16)))
i += 3
elif encoded_string[i] == '+':
result.append(' ')
i += 1
else:
result.append(encoded_string[i])
i += 1
return ''.join(result)
@staticmethod
def encode_params(params):
pairs = []
for key, value in params.items():
pairs.append(f"{URLEncoder.encode(str(key))}={URLEncoder.encode(str(value))}")
return '&'.join(pairs)