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), anchor='la'): """ 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, anchor=anchor) def status_clock_position(order): """ Computes the x, y position of the status clock at the given stacking order (0-based). """ x = SCREEN_WIDTH_PIXELS - CLOCK_WIDTH_PIXELS * STATUS_SIZE_MULTIPLIER - MARGIN_PIXELS slot_height = CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER + 2 * MARGIN_PIXELS y = (MARGIN_PIXELS + CLOCK_HEIGHT_PIXELS * CLOCK_SIZE_MULTIPLIER + 2 * MARGIN_PIXELS + order * slot_height) return x, y 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 milestone_countdown(order, target_date, label): x, y = status_clock_position(order) right_x = x + CLOCK_WIDTH_PIXELS * STATUS_SIZE_MULTIPLIER today = datetime.today().date() days_until = (target_date - today).days color = (0, 255, 0) draw_digits(right_x, y, STATUS_SIZE_MULTIPLIER, f'{days_until}', color, anchor='ra') draw_label(label, x - MARGIN_PIXELS, y + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER) def draw_journal_minutes(status, order): x, y = status_clock_position(order) 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, order): x, y = status_clock_position(order) 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() try: status = get_status_data() draw_journal_minutes(status, order=0) draw_fasting(status, order=1) milestone_countdown(order=5, target_date=datetime.strptime("09/01/2026", "%m/%d/%Y").date(), label='below 108kg:') milestone_countdown(order=4, target_date=datetime.strptime("12/25/2026", "%m/%d/%Y").date(), label='below 95kg:') 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)