292 lines
8.6 KiB
Python
Executable file
292 lines
8.6 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
|
|
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
|
|
picpath = '/srv/inkybot/pictures'
|
|
saturation = 0.7
|
|
|
|
exiting = False
|
|
skip_img = False
|
|
|
|
|
|
BUTTONS = [5,6,16,24]
|
|
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, palette):
|
|
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.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_positions = [
|
|
(10,49),
|
|
(10,161),
|
|
(10,273),
|
|
(10,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),
|
|
self.parent.palette)
|
|
color = self.parent.least_similar_color(
|
|
color_border,
|
|
self.parent.palette)
|
|
|
|
for i in range(4):
|
|
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=color_border, font=self.parent.font)
|
|
|
|
draw.text((x,y - dy), txt, fill=color, font=self.parent.font)
|
|
|
|
self.parent.inky.set_image(image, saturation=self.saturation)
|
|
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 self.exiting is not 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
|
|
|
|
resizedimage = self.parent.resize_with_letterbox(
|
|
image,
|
|
self.parent.inky.resolution,
|
|
self.parent.average_outer_perimeter_color(image)
|
|
)
|
|
|
|
self.set_image(resizedimage)
|
|
|
|
|
|
@inkybot.State('hass')
|
|
class HassMode(inkybot.StateClass):
|
|
button_text = [
|
|
" ",
|
|
"",
|
|
" ",
|
|
" "
|
|
]
|
|
|
|
def button_b(self):
|
|
self.change_state('picture')
|
|
|
|
def enter(self):
|
|
image = Image.new("RGB", self.parent.inky.resolution, (255,0,0))
|
|
self.set_image(image)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
inkybot.start('picture')
|
|
|
|
|