diff --git a/fb_gui.py b/fb_gui.py new file mode 100755 index 0000000..992b6d7 --- /dev/null +++ b/fb_gui.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +import random +import time +import sys +import threading + +from PIL import Image, ImageDraw, ImageFont, ImageColor + +import requests + +from framebuffer import Framebuffer + +import subprocess + +class TextBox: + def __init__(self, pos, size, max_jitter, color, background, fitting_text): + self.color = color + self.background = background + self.pos = pos + self.bounds = (size[0]+1, size[1]+1) + font_size = 100 + while (self.bounds[0] > size[0]-max_jitter) or (self.bounds[1] > size[1]-max_jitter): + font_size-=1 + self.font = ImageFont.truetype("/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf", font_size) + self.bounds = self.font.getsize(fitting_text) + self.size = size + self.img = Image.new("RGB", self.bounds) + self.draw = ImageDraw.Draw(self.img) + self.offset = (int((self.size[0]-self.bounds[0])/2), int((self.size[1]-self.bounds[1])/2)) + + def draw_text(self, target_img, text, jitter_pos): + self.draw.rectangle((0, 0, self.img.width-1, self.img.height-1), self.background, self.background, 1) + self.draw.text((jitter_pos[0],0), text, font=self.font, fill=self.color) + x=self.pos[0]+self.offset[0] + y=self.pos[1]+self.offset[1] + target_img.paste(self.img, (x,y)) + +### +# CONFIGURATION +### + +DISPLAY_SIZE=(320,240) + +CLOCK_FORMAT = "%H:%M" +TEMP_FORMAT = "% 3.1f°C" + +BKGND_COLOR=(0,0,0) +LINE_COLOR=(255,255,255) + +JITTER=5 +DELAY=0.3 +TEMP_DELAY=10 + +GPIO_TOOL=['gpio','-g'] +PWM_MODE=['mode','18','pwm'] +DAY_BRIGHT=['pwm','18','800'] +NIGHT_BRIGHT=['pwm','18','200'] + +now_parts=time.localtime() +time_str = time.strftime(CLOCK_FORMAT, now_parts) +temp_str = TEMP_FORMAT % -10.0 + +clock_box=TextBox( (10,10), (300,120), JITTER, (255,255,255), BKGND_COLOR, time_str) +temp_box =TextBox( (120,180), (190,50), JITTER, (255,0,0), BKGND_COLOR, temp_str) + +display = Framebuffer(1) +fb_img = Image.new(mode="RGB", size=DISPLAY_SIZE) +fb_draw = ImageDraw.Draw(fb_img) + +mean_temp = 0 + +def fetch_temp(bridge, user, sensor): + r=requests.get(f"http://{bridge}/api/{user}/sensors/{str(sensor)}") + if r: + return r.json()["state"]["temperature"]/100 + return None + +def jitter(): + return (random.randint(0,JITTER), random.randint(0,JITTER)) + +# Main loop: +jitter_pos = jitter() + +old_time="" +old_temp="" + +subprocess.run(GPIO_TOOL+PWM_MODE) +subprocess.run(GPIO_TOOL+NIGHT_BRIGHT) + +fb_draw.rectangle(((0,0),DISPLAY_SIZE),BKGND_COLOR, BKGND_COLOR,1) +temp_time=time.time() +try: + while True: + changed = False + + now = time.time() + now_parts = time.localtime(now) + + if now-temp_time > TEMP_DELAY: + cur_temp = fetch_temp("philips-hue", "GQ03rw1saUS0n88G5yj9j7-TsteFIE1yxtlBOgzD", 71) + if cur_temp: + mean_temp = cur_temp + temp_time = now + + if now_parts.tm_sec == 0: + jitter_pos = jitter() + + if (now_parts.tm_min == 0) and (now_parts.tm_hour > 7 and now_parts.tm_hour < 19): + subprocess.run(GPIO_TOOL+DAY_BRIGHT) + else: + subprocess.run(GPIO_TOOL+NIGHT_BRIGHT) + + time_str = time.strftime(CLOCK_FORMAT, now_parts) + temp_str = TEMP_FORMAT % mean_temp + + if time_str != old_time: + clock_box.draw_text(fb_img, time_str, jitter_pos) + old_time = time_str + changed = True + + if temp_str != old_temp: + temp_box.draw_text(fb_img, temp_str, jitter_pos) + old_temp = temp_str + changed=True + + if changed: + display.show(fb_img) + + while time.time() - now < DELAY: + time.sleep(DELAY) + +except KeyboardInterrupt: + clock_box.draw_text(fb_img, "off" ,(0,0)) + temp_box.draw_text(fb_img,"",(0,0)) + display.show(fb_img) + subprocess.run(GPIO_TOOL+NIGHT_BRIGHT) + diff --git a/framebuffer.py b/framebuffer.py new file mode 100755 index 0000000..e05a7b5 --- /dev/null +++ b/framebuffer.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Framebuffer helper that makes lots of simpifying assumptions + +bits_per_pixel assumed memory layout +16 rgb565 +24 rgb +32 argb + +""" + +from PIL import Image +import numpy + + +def _read_and_convert_to_ints(filename): + with open(filename, "r") as fp: + content = fp.read() + tokens = content.strip().split(",") + return [int(t) for t in tokens if t] + + +def _converter_argb(image: Image): + return bytes([x for r, g, b in image.getdata() for x in (255, r, g, b)]) + + +def _converter_rgb565(image: Image): + return bytes([x for r, g, b in image.getdata() + for x in ((g & 0x1c) << 3 | (b >> 3), r & 0xf8 | (g >> 3))]) + + +def _converter_1_argb(image: Image): + return bytes([x for p in image.getdata() + for x in (255, p, p, p)]) + + +def _converter_1_rgb(image: Image): + return bytes([x for p in image.getdata() + for x in (p, p, p)]) + + +def _converter_1_rgb565(image: Image): + return bytes([(255 if x else 0) for p in image.getdata() + for x in (p, p)]) + + +def _converter_rgba_rgb565_numpy(image: Image): + flat = numpy.frombuffer(image.tobytes(), dtype=numpy.uint32) + # note, this is assumes little endian byteorder and results in + # the following packing of an integer: + # bits 0-7: red, 8-15: green, 16-23: blue, 24-31: alpha + flat = ((flat & 0xf8) << 8) | ((flat & 0xfc00) >> 5) | ((flat & 0xf80000) >> 19) + return flat.astype(numpy.uint16).tobytes() + + +def _converter_no_change(image: Image): + return image.tobytes() + +# anything that does not use numpy is hopelessly slow +_CONVERTER = { + ("RGBA", 16): _converter_rgba_rgb565_numpy, + ("RGB", 16): _converter_rgb565, + ("RGB", 24): _converter_no_change, + ("RGB", 32): _converter_argb, + # note numpy does not work well with mode="1" images as + # image.tobytes() loses pixel color info + ("1", 16): _converter_1_rgb565, + ("1", 24): _converter_1_rgb, + ("1", 32): _converter_1_argb, +} + + +class Framebuffer(object): + + def __init__(self, device_no: int): + self.path = "/dev/fb%d" % device_no + config_dir = "/sys/class/graphics/fb%d/" % device_no + self.size = tuple(_read_and_convert_to_ints( + config_dir + "/virtual_size")) + self.stride = _read_and_convert_to_ints(config_dir + "/stride")[0] + self.bits_per_pixel = _read_and_convert_to_ints( + config_dir + "/bits_per_pixel")[0] + assert self.stride == self.bits_per_pixel // 8 * self.size[0] + + def __str__(self): + args = (self.path, self.size, self.stride, self.bits_per_pixel) + return "%s size:%s stride:%s bits_per_pixel:%s" % args + + # Note: performance is terrible even for medium resolutions + def show(self, image: Image): + converter = _CONVERTER[(image.mode, self.bits_per_pixel)] + assert image.size == self.size + out = converter(image) + with open(self.path, "wb") as fp: + fp.write(out) + + def on(self): + pass + + def off(self): + pass + +if __name__ == "__main__": + import time + from PIL import ImageDraw + + + def TestFrameBuffer(i): + fb = Framebuffer(i) + print(fb) + image = Image.new("RGBA", fb.size) + draw = ImageDraw.Draw(image) + draw.rectangle(((0, 0), fb.size), fill="green") + draw.ellipse(((0, 0), fb.size), fill="blue", outline="red") + draw.line(((0, 0), fb.size), fill="green", width=2) + start = time.time() + for i in range(5): + fb.show(image) + stop = time.time() + print("fps: %.2f" % (10 / (stop - start))) + + + for i in [1]: + TestFrameBuffer(i)