391 lines
12 KiB
Python
Executable file
391 lines
12 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
import sys, os, random, time, signal
|
|
import numpy as np
|
|
import RPi.GPIO as GPIO
|
|
from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageOps, ImagePalette
|
|
from inky.inky_uc8159 import Inky
|
|
|
|
|
|
class Inkybot:
|
|
|
|
# FIXME: a bunch of this needs to be parameterized
|
|
|
|
palette = [
|
|
(0, 0, 0), # black
|
|
(255, 255, 255), # white
|
|
(0, 255, 0), # green
|
|
(0, 0, 255), # blue
|
|
(255, 0, 0), # red
|
|
(255, 255, 0), # yellow
|
|
(255, 140, 0), # orange
|
|
(255, 255, 255) # white again???
|
|
]
|
|
|
|
font_size = 60
|
|
|
|
BUTTONS = [5,6,16,24]
|
|
BUTTON_LABELS = ['A', 'B', 'C', 'D']
|
|
|
|
inky = Inky()
|
|
state = None
|
|
states = {}
|
|
|
|
def __init__(self):
|
|
self.font = ImageFont.truetype("fonts/3270NerdFontMono-Regular.ttf", size=self.font_size)
|
|
GPIO.setmode(GPIO.BCM)
|
|
GPIO.setup(self.BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
|
|
|
for pin in self.BUTTONS:
|
|
GPIO.add_event_detect(pin, GPIO.FALLING, self.handle_button, bouncetime=250)
|
|
|
|
def color_similarity(self, color1, color2):
|
|
return np.sqrt(np.sum((np.array(color1) - np.array(color2)) ** 2))
|
|
|
|
def least_similar_color(self, color):
|
|
return max(self.palette, key=lambda ref_color: self.color_similarity(ref_color, color))
|
|
|
|
def average_outer_perimeter_color(self,image):
|
|
# Get image dimensions
|
|
width, height = image.size
|
|
|
|
# Extract the outer 1-pixel perimeter
|
|
outer_perimeter_pixels = []
|
|
for x in range(width):
|
|
outer_perimeter_pixels.append(image.getpixel((x, 0))) # Top row
|
|
outer_perimeter_pixels.append(image.getpixel((x, height - 1))) # Bottom row
|
|
for y in range(1, height - 1):
|
|
outer_perimeter_pixels.append(image.getpixel((0, y))) # Left column
|
|
outer_perimeter_pixels.append(image.getpixel((width - 1, y))) # Right column
|
|
|
|
# Calculate average color
|
|
total_pixels = len(outer_perimeter_pixels)
|
|
total_red = sum(pixel[0] for pixel in outer_perimeter_pixels)
|
|
total_green = sum(pixel[1] for pixel in outer_perimeter_pixels)
|
|
total_blue = sum(pixel[2] for pixel in outer_perimeter_pixels)
|
|
|
|
average_red = total_red // total_pixels
|
|
average_green = total_green // total_pixels
|
|
average_blue = total_blue // total_pixels
|
|
|
|
return (average_red, average_green, average_blue)
|
|
|
|
def resize_with_letterbox(self, image, resolution, letterbox_color=(0, 0, 0)):
|
|
target_width = resolution[0]
|
|
target_height = resolution[1]
|
|
|
|
# Get original width and height
|
|
original_width, original_height = image.size
|
|
|
|
# Calculate the aspect ratios
|
|
original_aspect_ratio = original_width / original_height
|
|
target_aspect_ratio = target_width / target_height
|
|
|
|
# Calculate resizing factors
|
|
if original_aspect_ratio < target_aspect_ratio:
|
|
# Image is narrower than target, resize based on height
|
|
new_width = int(target_height * original_aspect_ratio)
|
|
new_height = target_height
|
|
else:
|
|
# Image is taller than target, resize based on width
|
|
new_width = target_width
|
|
new_height = int(target_width / original_aspect_ratio)
|
|
|
|
# Resize the image
|
|
resized_image = image.resize((new_width, new_height), Image.ANTIALIAS)
|
|
|
|
# Create a new image with letterbox bars
|
|
x_max = target_width - new_width
|
|
if x_max // 2 > self.font_size:
|
|
x = x_max // 2
|
|
else:
|
|
x = min(x_max, x_max // 2 + self.font_size)
|
|
letterbox_image = Image.new(image.mode, (target_width, target_height), letterbox_color)
|
|
letterbox_image.paste(resized_image, (x, (target_height - new_height) // 2))
|
|
|
|
return letterbox_image
|
|
|
|
|
|
def handle_button(self,pin):
|
|
label = self.BUTTON_LABELS[self.BUTTONS.index(pin)]
|
|
|
|
if label == 'A':
|
|
self.state.button_a()
|
|
elif label == 'B':
|
|
self.state.button_b()
|
|
elif label == 'C':
|
|
self.state.button_c()
|
|
elif label == 'D':
|
|
self.state.button_d()
|
|
else:
|
|
raise Exception("Unhandled button press!")
|
|
|
|
class StateClass:
|
|
button_text = [ "?", "?", "?", "?" ]
|
|
button_colors = [ None, None, None, None ]
|
|
button_positions = [
|
|
(5,49),
|
|
(5,161),
|
|
(5,273),
|
|
(5,385)
|
|
]
|
|
font_size = 60
|
|
saturation = 0.7
|
|
|
|
def __init__(self, parent):
|
|
self.parent = parent
|
|
|
|
# enter and exit functions can be overridden by the child class
|
|
def enter(self):
|
|
pass
|
|
def exit(self):
|
|
pass
|
|
|
|
def change_state(self, state):
|
|
self.parent.change_state(state)
|
|
|
|
# button_x functions should be overridden by the child class too
|
|
def button_a(self):
|
|
print("Button A")
|
|
def button_b(self):
|
|
print("Button B")
|
|
def button_c(self):
|
|
print("Button C")
|
|
def button_d(self):
|
|
print("Button D")
|
|
|
|
# same applies to the loop
|
|
def loop(self):
|
|
pass
|
|
|
|
def set_image(self, image):
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
# text colors use the nearest undithered color to what's in the letterbox
|
|
color_border = self.parent.least_similar_color(self.parent.average_outer_perimeter_color(image))
|
|
color = self.parent.least_similar_color(color_border)
|
|
|
|
for i in range(4):
|
|
if self.button_colors[i] is not None:
|
|
c = self.button_colors[i]
|
|
cb = self.parent.least_similar_color(c)
|
|
else:
|
|
c = color
|
|
cb = color_border
|
|
txt = self.button_text[i]
|
|
x,y = self.button_positions[i]
|
|
text_width, text_height = draw.textsize(txt, font=self.parent.font)
|
|
dy = int(text_height / 2)
|
|
|
|
for xx in range(x - 1, x + 2, 1):
|
|
for yy in range(y - 1 - dy, y + 2 - dy, 1):
|
|
draw.text((xx,yy), txt, fill=cb, font=self.parent.font)
|
|
|
|
draw.text((x,y - dy), txt, fill=c, font=self.parent.font)
|
|
|
|
self.parent.inky.set_image(image, saturation=self.saturation)
|
|
self.parent.inky.show()
|
|
|
|
def clear(self, color="white"):
|
|
w,h = self.parent.inky.resolution
|
|
|
|
clear_image = Image.new('RGB', (w, h), color)
|
|
self.parent.inky.set_image(clear_image, saturation=1.0)
|
|
self.parent.inky.show()
|
|
|
|
def State(self, name):
|
|
def decorator(c):
|
|
self.states[name] = c(parent = self)
|
|
return c
|
|
return decorator
|
|
|
|
def change_state(self, state):
|
|
print(f"Changing state to: {state}")
|
|
if self.state:
|
|
self.state.exit()
|
|
self.state = self.states[state]
|
|
self.state.enter()
|
|
|
|
def start(self, state):
|
|
self.state = self.states[state]
|
|
self.state.enter()
|
|
|
|
while True:
|
|
self.state.loop()
|
|
time.sleep(0.1)
|
|
|
|
|
|
|
|
inkybot = Inkybot()
|
|
|
|
@inkybot.State('picture')
|
|
class PictureMode(inkybot.StateClass):
|
|
button_text = [
|
|
"",
|
|
"",
|
|
"",
|
|
""
|
|
]
|
|
font_size = 60
|
|
picpath = '/srv/inkybot/pictures'
|
|
saturation = 0.7
|
|
pic_time = 60.0
|
|
|
|
def enter(self):
|
|
self.imagelist = []
|
|
self.next_img = True
|
|
self.time_target = 0.0
|
|
|
|
def button_b(self):
|
|
self.change_state('hass')
|
|
|
|
def button_d(self):
|
|
print("changing image...")
|
|
self.next_img = True
|
|
self.time_target = time.time() + self.pic_time
|
|
|
|
def loop(self):
|
|
|
|
if self.time_target <= time.time():
|
|
self.next_img = True
|
|
|
|
if self.next_img:
|
|
self.next_img = False
|
|
self.time_target = time.time() + self.pic_time
|
|
|
|
if len(self.imagelist) == 0:
|
|
self.imagelist = os.listdir(self.picpath)
|
|
random.shuffle(self.imagelist)
|
|
|
|
fn = self.imagelist.pop()
|
|
print(f"Displaying {fn}")
|
|
|
|
image = Image.open(f"{self.picpath}/{fn}") # XXX FIXME: os join function instead
|
|
|
|
enhancer = ImageEnhance.Color(image)
|
|
adjustedimage = enhancer.enhance(1.5)
|
|
|
|
resizedimage = self.parent.resize_with_letterbox(
|
|
adjustedimage,
|
|
self.parent.inky.resolution,
|
|
self.parent.average_outer_perimeter_color(image)
|
|
)
|
|
|
|
self.set_image(resizedimage)
|
|
|
|
|
|
|
|
from selenium import webdriver
|
|
from selenium.webdriver.chrome.service import Service
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
from selenium.common.exceptions import TimeoutException as selenium_TimeoutException
|
|
from io import BytesIO
|
|
|
|
@inkybot.State('hass')
|
|
class HassMode(inkybot.StateClass):
|
|
button_text = [
|
|
" ",
|
|
"",
|
|
" ",
|
|
""
|
|
]
|
|
|
|
button_colors = [
|
|
None,
|
|
(0,0,255),
|
|
None,
|
|
(0,255,0)
|
|
]
|
|
|
|
driver_path = "/usr/bin/chromedriver"
|
|
driver=None
|
|
refresh_target = 0.0
|
|
screen_scale = 1.0
|
|
|
|
def button_b(self):
|
|
self.change_state('picture')
|
|
|
|
def button_d(self):
|
|
self.driver.refresh()
|
|
self.update()
|
|
self.refresh_target = time.time() + 5
|
|
|
|
def enter(self):
|
|
#self.clear("blue")
|
|
url = "http://localhost:8123/lovelace/1?kiosk"
|
|
print(f"launching selenium and loading {url}")
|
|
|
|
service = Service(self.driver_path)
|
|
options = webdriver.ChromeOptions()
|
|
options.add_argument('--headless')
|
|
self.driver = webdriver.Chrome(service=service, options=options)
|
|
|
|
width,height = [self.screen_scale * x for x in self.parent.inky.resolution]
|
|
#width -= self.font_size
|
|
self.driver.set_window_size(width,height)
|
|
|
|
self.driver.get(url)
|
|
print("waiting for login button...")
|
|
login_button = WebDriverWait(self.driver, 5).until(EC.visibility_of_element_located((By.CSS_SELECTOR, "mwc-button")))
|
|
#self.driver.execute_script("document.body.style.fontSize = 'larger';") # font scaling
|
|
login_button.click()
|
|
|
|
self.update()
|
|
self.refresh_target = time.time() + 5
|
|
|
|
def update(self):
|
|
if self.driver is not None:
|
|
print("Updating display...")
|
|
screenshot_bytes = self.driver.get_screenshot_as_png()
|
|
screenshot_image = Image.open(BytesIO(screenshot_bytes)).convert('RGB')
|
|
width, height = screenshot_image.size
|
|
cropped_image = screenshot_image.crop((5,5, width - 5, height - 5))
|
|
|
|
resizedimage = self.parent.resize_with_letterbox(
|
|
cropped_image,
|
|
self.parent.inky.resolution,
|
|
self.parent.average_outer_perimeter_color(screenshot_image)
|
|
)
|
|
|
|
#enhancer = ImageEnhance.Contrast(resizedimage)
|
|
enhancer = ImageEnhance.Color(resizedimage)
|
|
adjustedimage = enhancer.enhance(4.0)
|
|
enhancer = ImageEnhance.Contrast(adjustedimage)
|
|
adjustedimage = enhancer.enhance(5.0)
|
|
#print(resizedimage.mode)
|
|
#adjustedimage = ImageOps.autocontrast(resizedimage)
|
|
|
|
self.set_image(adjustedimage)
|
|
|
|
self.driver.refresh() # reloading the page at every refresh will get us any UI updates in the background
|
|
self.refresh_target = time.time() + 60
|
|
|
|
def exit(self):
|
|
print("Quitting chrome driver...")
|
|
d = self.driver
|
|
self.driver = None
|
|
d.close()
|
|
d.quit()
|
|
|
|
def loop(self):
|
|
if self.driver is not None:
|
|
|
|
try:
|
|
refresh_button = WebDriverWait(self.driver, 0.5).until(
|
|
EC.visibility_of_element_located((By.XPATH, '//*[text()="Refresh"]'))
|
|
)
|
|
refresh_button.click()
|
|
self.refresh_target = time.time() + 2.5
|
|
except selenium_TimeoutException:
|
|
|
|
if self.refresh_target <= time.time():
|
|
self.update()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
#inkybot.start('picture')
|
|
inkybot.start('hass')
|
|
|
|
|