v.0.10.0 redesign prj structure, multi dev containers
This commit is contained in:
8
pico-client/utils/__init__.py
Normal file
8
pico-client/utils/__init__.py
Normal 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
|
||||
98
pico-client/utils/colors.py
Normal file
98
pico-client/utils/colors.py
Normal 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)
|
||||
58
pico-client/utils/digital_clock.py
Normal file
58
pico-client/utils/digital_clock.py
Normal 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
|
||||
108
pico-client/utils/font_utils.py
Normal file
108
pico-client/utils/font_utils.py
Normal 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
|
||||
6
pico-client/utils/http_utils.py
Normal file
6
pico-client/utils/http_utils.py
Normal file
@@ -0,0 +1,6 @@
|
||||
http_message = {
|
||||
# Jedes Zeichen ist genau 3 Pixel breit, 5 Pixel hoch
|
||||
200: "OK",
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
}
|
||||
9
pico-client/utils/math_utils.py
Normal file
9
pico-client/utils/math_utils.py
Normal 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)]
|
||||
39
pico-client/utils/simple_counter.py
Normal file
39
pico-client/utils/simple_counter.py
Normal 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})"
|
||||
42
pico-client/utils/system_load.py
Normal file
42
pico-client/utils/system_load.py
Normal 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()
|
||||
136
pico-client/utils/time_utils.py
Normal file
136
pico-client/utils/time_utils.py
Normal 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
|
||||
68
pico-client/utils/url_encode.py
Normal file
68
pico-client/utils/url_encode.py
Normal 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)
|
||||
|
||||
Reference in New Issue
Block a user