Add fbdev based display

This commit is contained in:
Asaril 2021-03-20 21:21:43 +01:00
parent f5eaa0603b
commit 7014eaadfa
2 changed files with 263 additions and 0 deletions

138
fb_gui.py Executable file
View file

@ -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)

125
framebuffer.py Executable file
View file

@ -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)