Files
fbclock/faster.py
T
2026-07-28 00:37:31 +03:00

225 lines
7.1 KiB
Python

import numpy as np
from PIL import Image, ImageDraw, ImageFont
from time import sleep
from datetime import datetime, time
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 draw_countdown(x, y, target_dt, size=STATUS_SIZE_MULTIPLIER):
"""
Draws a countdown/countup to target_dt: green if more than 15 minutes away,
yellow if less than 15 minutes away, red if target_dt is in the past.
"""
now = datetime.now()
delta_seconds = (target_dt - now).total_seconds()
if delta_seconds >= 0:
color = (255, 255, 0) if delta_seconds < 15 * 60 else (0, 255, 0)
else:
color = (255, 0, 0)
total = abs(int(delta_seconds))
hours, remainder = divmod(total, 3600)
minutes = remainder // 60
draw_clock_digits(x, y, size, f'{hours:02}', f'{minutes:02}', 1, color)
WORKDAY_EVENTS = [
(7, 0, 'Wake up'),
(8, 0, 'Start working'),
(11, 0, 'Scrum meeting'),
(14, 0, 'Physiotherapy'),
(15, 0, 'Lunch'),
(18, 0, 'Close work day'),
(18, 30, 'Evening walk'),
(22, 0, 'Close the day'),
(23, 0, 'Go to sleep'),
]
def next_workday_event(now=None):
"""
Returns (target_dt, label) for the next event in WORKDAY_EVENTS, or None on weekends.
Once past the last event of the day, keeps returning it (now overdue) until midnight.
"""
now = now or datetime.now()
if now.weekday() >= 5:
return None
today = now.date()
for hour, minute, label in WORKDAY_EVENTS:
dt = datetime.combine(today, time(hour, minute))
if dt > now:
return dt, label
last_hour, last_minute, last_label = WORKDAY_EVENTS[-1]
return datetime.combine(today, time(last_hour, last_minute)), last_label
def draw_next_event(order):
result = next_workday_event()
if result is None:
return
target, label = result
x, y = status_clock_position(order)
draw_countdown(x, y, target, STATUS_SIZE_MULTIPLIER)
draw_label(f'{label}:', x - MARGIN_PIXELS, y + CLOCK_HEIGHT_PIXELS * STATUS_SIZE_MULTIPLIER)
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'
target = datetime.strptime(status['fast']['projected_end'], "%Y-%m-%d %H:%M:%S")
else:
label = 'Fasting'
target = datetime.now().replace(hour=16, minute=0, second=0, microsecond=0)
draw_countdown(x, y, target, STATUS_SIZE_MULTIPLIER)
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_next_event(order=2.5)
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)