commit 8127789b0805208059b670f852a91af99b98d8cc Author: Sorin Savu Date: Thu Jul 9 14:15:38 2026 +0300 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5420c78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo + +# Editors +.vscode/ +.idea/ + +# Python virtual environments +venv/ +.venv/ + +# Generated files +investments.html* +master.zip + +# OS files +.DS_Store +Thumbs.db diff --git a/faster.py b/faster.py new file mode 100644 index 0000000..8d6da1d --- /dev/null +++ b/faster.py @@ -0,0 +1,167 @@ +import numpy as np +from PIL import Image, ImageDraw, ImageFont +from time import sleep +from datetime import datetime +import logging +import signal +import sys + +from requests import RequestException + + +from lib.image.rgb565 import convert_rgb_to_rgb565_numpy +from lib.terminal import disable_cursor, enable_cursor, restore_screen, was_key_pressed +from lib.lifedata import get_status_data + +SCREEN_WIDTH_PIXELS = 900 +SCREEN_HEIGHT_PIXELS = 1440 +MARGIN_PIXELS = 15 + +CLOCK_SIZE_MULTIPLIER = (900 - 2 * MARGIN_PIXELS) / 243 +STATUS_SIZE_MULTIPLIER = 2 + +CLOCK_WIDTH_PIXELS = 243 +CLOCK_HEIGHT_PIXELS = 70 + + +def signal_handler(signal, frame): + logging.info('You pressed Ctrl+C!') + restore_screen('/dev/tty1') + enable_cursor('/dev/tty1') + sys.exit(0) + +signal.signal(signal.SIGINT, signal_handler) + +#capture also ctrl+c +signal.signal(signal.SIGTERM, signal_handler) + +disable_cursor('/dev/tty1') + +img = Image.new('RGB', (SCREEN_WIDTH_PIXELS,SCREEN_HEIGHT_PIXELS)) +draw = ImageDraw.Draw(img) + + +def draw_label(label, x, y): + label_font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', CLOCK_HEIGHT_PIXELS // 2) + label_text_color = (0, 255//2, 0) + draw.text((x,y), label, font=label_font, fill=label_text_color, anchor='rb') + + +def draw_clock_digits(x, y, size ,hours, minutes, blink_second, color=(0, 255, 0)): + """ + Draws clock digits at the specified position. + """ + current_time = f'{hours}:{minutes}' + # Blink logic + if ':' in current_time and datetime.now().second % 2 == blink_second: + current_time = current_time.replace(":", " ") + + draw_digits(x, y, size, current_time, color) + +def draw_digits(x, y, size, digits, color=(0, 255, 0)): + """ + Draws clock digits at the specified position. + """ + font_path = "/usr/share/fonts/truetype/dseg/DSEG14Classic-BoldItalic.ttf" + font = ImageFont.truetype(font_path, int(CLOCK_HEIGHT_PIXELS * size)) + + draw.text((x, y), digits, font=font, fill=color) + +def draw_clock(): + x = SCREEN_WIDTH_PIXELS - CLOCK_WIDTH_PIXELS * CLOCK_SIZE_MULTIPLIER - MARGIN_PIXELS + y = MARGIN_PIXELS + now = datetime.now() + hours = now.strftime("%H") + minutes = now.strftime("%M") + + draw_clock_digits(x, y, CLOCK_SIZE_MULTIPLIER, hours, minutes, 0) + #draw_label('Time:', x - MARGIN_PIXELS, y + CLOCK_HEIGHT_PIXELS) + + +def draw_death_clock(): + x = SCREEN_WIDTH_PIXELS - CLOCK_WIDTH_PIXELS * STATUS_SIZE_MULTIPLIER - MARGIN_PIXELS + y = MARGIN_PIXELS + CLOCK_HEIGHT_PIXELS * CLOCK_SIZE_MULTIPLIER + 2 * MARGIN_PIXELS + + death_date = datetime.strptime("08/23/2026", "%m/%d/%Y").date() + today = datetime.today().date() + days_until_death = (death_date - today).days + + color = (0, 255, 0) + + draw_digits(x, y, STATUS_SIZE_MULTIPLIER, f'{days_until_death:4}', color) + draw_label('death:', x - MARGIN_PIXELS, y + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER) + +def draw_journal_minutes(status): + x = SCREEN_WIDTH_PIXELS - CLOCK_WIDTH_PIXELS * STATUS_SIZE_MULTIPLIER - MARGIN_PIXELS + y = MARGIN_PIXELS + CLOCK_HEIGHT_PIXELS * CLOCK_SIZE_MULTIPLIER + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER + 4 * MARGIN_PIXELS + + elapsed_minutes = status['journal_updated_minutes'] + + hours = elapsed_minutes // 60 + minutes = elapsed_minutes % 60 + + if elapsed_minutes <=15: + color = (0, 255, 0) + elif elapsed_minutes <= 20: + color = (255, 255, 0) + else: + color = (255, 0, 0) + + draw_clock_digits(x, y, STATUS_SIZE_MULTIPLIER, f'{hours:02}', f'{minutes:02}', 1, color) + draw_label('Journal:', x - MARGIN_PIXELS, y + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER) + + +def draw_fasting(status): + x = SCREEN_WIDTH_PIXELS - CLOCK_WIDTH_PIXELS * STATUS_SIZE_MULTIPLIER - MARGIN_PIXELS + y = MARGIN_PIXELS + CLOCK_HEIGHT_PIXELS * CLOCK_SIZE_MULTIPLIER + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER * 2 + 6 * MARGIN_PIXELS + + if 'fasting' in status and status['fasting']: + label = 'Breakfast' + date = datetime.strptime(status['fast']['projected_end'], "%Y-%m-%d %H:%M:%S") + delta = date - datetime.now() + hours = delta.seconds // 3600 + minutes = (delta.seconds // 60) % 60 + color = (0, 255, 0) + else: + label = 'Fasting' + date = datetime.now().replace(hour=16, minute=0, second=0, microsecond=0) + if date > datetime.now(): + delta = date - datetime.now() + color = (0, 255, 0) + else: + delta = datetime.now() - date + color = (255, 0, 0) + + hours = delta.seconds // 3600 + minutes = (delta.seconds // 60) % 60 + + draw_clock_digits(x, y, STATUS_SIZE_MULTIPLIER, f'{hours:02}', f'{minutes:02}', 1, color) + draw_label(f'{label}:', x - MARGIN_PIXELS, y + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER) + +def clear_status_area(): + x = 0 + y = MARGIN_PIXELS + CLOCK_HEIGHT_PIXELS * CLOCK_SIZE_MULTIPLIER + 2 * MARGIN_PIXELS + draw.rectangle([x, y, SCREEN_WIDTH_PIXELS, SCREEN_HEIGHT_PIXELS], fill=(0, 0, 0)) + + +while True: + draw_clock() + draw_death_clock() + try: + status = get_status_data() + + draw_journal_minutes(status) + # draw_fasting(status) + + except RequestException as e: + logging.error(f'Error fetching status data: {e}') + clear_status_area() + + + rotated_img = img.rotate(90, expand=True) + img_rgb565 = convert_rgb_to_rgb565_numpy(rotated_img) + with open('/dev/fb0', 'wb') as fb: + fb.write(img_rgb565) + img = Image.new('RGB', (SCREEN_WIDTH_PIXELS,SCREEN_HEIGHT_PIXELS)) + draw = ImageDraw.Draw(img) + sleep(1) diff --git a/lib/image/rgb565.py b/lib/image/rgb565.py new file mode 100644 index 0000000..7a82580 --- /dev/null +++ b/lib/image/rgb565.py @@ -0,0 +1,24 @@ +import numpy as np +from PIL import Image, ImageDraw, ImageFont + +def convert_rgb_to_rgb565_numpy(image): + if image.mode != 'RGB': + raise ValueError('Image must be in RGB mode') + + # Convert image to NumPy array + img_array = np.array(image) + + # Extract the individual RGB components + r = img_array[:,:,0].astype(np.uint16) + g = img_array[:,:,1].astype(np.uint16) + b = img_array[:,:,2].astype(np.uint16) + + # Shift the bits into the correct positions for RGB565 +# rgb565_array = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + # Shift the bits into the correct positions for RGB565 using NumPy's bitwise functions + rgb565_array = np.bitwise_or(np.bitwise_or(np.left_shift(np.bitwise_and(r, 0xF8), 8), + np.left_shift(np.bitwise_and(g, 0xFC), 3)), + np.right_shift(b, 3)) + + # Flatten the array to a 1D array of bytes and return + return rgb565_array.flatten().tobytes() \ No newline at end of file diff --git a/lib/lifedata.py b/lib/lifedata.py new file mode 100644 index 0000000..18ebb77 --- /dev/null +++ b/lib/lifedata.py @@ -0,0 +1,9 @@ +import requests + + + +def get_status_data(): + url = 'https://life.itguy.ro/lifeapi/status.html' + data = requests.get(url).json() + return data + diff --git a/lib/terminal.py b/lib/terminal.py new file mode 100644 index 0000000..f249c91 --- /dev/null +++ b/lib/terminal.py @@ -0,0 +1,34 @@ +import select +import sys +import termios +import tty + +def disable_cursor(tty): + with open(tty, 'w') as t: + t.write("\033[?25l") + +def enable_cursor(tty): + with open(tty, 'w') as t: + t.write("\033[?25h") + +def restore_screen(tty): + with open(tty, 'w') as t: + t.write("\033[?5h\033[?5l") + + +def was_key_pressed(): + # Save the terminal settings + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + + try: + # Set the terminal to raw mode to read key press + tty.setraw(sys.stdin.fileno()) + + # Use select to check if there's any input + rlist, _, _ = select.select([sys.stdin], [], [], 0) + finally: + # Restore the terminal settings + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + + return bool(rlist) diff --git a/tools/clearfb.py b/tools/clearfb.py new file mode 100644 index 0000000..eedcbc1 --- /dev/null +++ b/tools/clearfb.py @@ -0,0 +1,72 @@ +import os +import fcntl +import struct +import mmap +from time import sleep + +with open('/dev/tty1', 'w') as tty: + tty.write("\033[?25l") + + +# Constants for the ioctl call +FBIOGET_VSCREENINFO = 0x4600 + +# Define a function to perform an ioctl call to get screen info +def get_screen_info(fb_device): + # We will get the variable screen info (resolution and bits per pixel) + # The struct for variable screen info is defined in the Linux kernel headers + # as fb_var_screeninfo, which is 56 bytes + screen_info_format = '8I12I16I2I' + screen_info = struct.pack(screen_info_format, *([0] * 38)) + screen_info = fcntl.ioctl(fb_device, FBIOGET_VSCREENINFO, screen_info) + return struct.unpack(screen_info_format, screen_info) + +# Open the framebuffer device in read-write mode +with open("/dev/fb0", "r+b") as fb: + # Get screen information using ioctl + info = get_screen_info(fb) + xres, yres, bits_per_pixel = info[0], info[1], info[6] + screensize = xres * yres * bits_per_pixel // 8 + + # Create a memory-mapped area to the framebuffer + fb_bytes = mmap.mmap(fb.fileno(), screensize, mmap.MAP_SHARED, mmap.PROT_WRITE | mmap.PROT_READ) + + # Now you can write pixel data to the framebuffer + # Here is an example that just sets all pixels to black + fb_bytes.seek(0) # Go to the beginning of the framebuffer + fb_bytes.write(b'\x00' * screensize) + + # Your framebuffer application code here + + # Once your program is finished, you can unmap and close the framebuffer device + fb_bytes.close() + +import sys +import termios +import atexit +from select import select + +# Save the terminal settings +fd = sys.stdin.fileno() +old_settings = termios.tcgetattr(fd) +atexit.register(lambda: termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)) + +def wait_for_key_press(): + # Disable canonical input and echo + new_settings = termios.tcgetattr(fd) + new_settings[3] = new_settings[3] & ~termios.ICANON & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSADRAIN, new_settings) + + # Wait for input + rlist, _, _ = select([sys.stdin], [], [], None) + if rlist: + sys.stdin.read(1) + +# Call the function to wait for any key press +wait_for_key_press() + +with open('/dev/tty1', 'w') as tty: + tty.write("\033[0;0H") + tty.write("\033[?5h\033[?5l") + tty.write("\033[?25h") + diff --git a/tools/fontsize.py b/tools/fontsize.py new file mode 100644 index 0000000..b9808b6 --- /dev/null +++ b/tools/fontsize.py @@ -0,0 +1,25 @@ +import math + +# Screen dimensions +screen_width_pixels = 1440 +screen_height_pixels = 900 +screen_diagonal_inches = 19 + +# Calculate the aspect ratio dimensions +aspect_ratio = 8 / 5 +height_inches = math.sqrt((screen_diagonal_inches**2) / (1 + aspect_ratio**2)) +width_inches = aspect_ratio * height_inches + +# Calculate PPI (Pixels Per Inch) +ppi_width = screen_width_pixels / width_inches +ppi_height = screen_height_pixels / height_inches +ppi = (ppi_width + ppi_height) / 2 # Average the PPI + +# Convert PPI to PPCM (Pixels Per Centimeter) +ppcm = ppi / 2.54 + +# Determine the pixel size for 1 cm tall characters +font_pixel_height = int(ppcm * 1) # 1 cm + +# Output the calculated font size in pixels +print(f"The font size for 1 cm tall characters should be approximately {font_pixel_height} pixels.") diff --git a/tools/resolution.py b/tools/resolution.py new file mode 100644 index 0000000..f5d4708 --- /dev/null +++ b/tools/resolution.py @@ -0,0 +1,17 @@ +import fcntl +import struct + + +FBIOGET_VSCREENINFO = 0x4600 + + +with open('/dev/fb0', 'r+b') as fb: + fb_var_screeninfo = '12sL9I' + var_info = fcntl.ioctl(fb, FBIOGET_VSCREENINFO, struct.pack(fb_var_screeninfo, b'', 0, *[0]*9)) + + xres, yres, xres_virtual, yres_virtual, xoffset, yoffset, bits_per_pixel = struct.unpack('7I', var_info[:28]) + print(xres) + print(yres) + print(bits_per_pixel) + +