The image will only be rotated while rendering on the Display, i.e. when render = True. Images saved in /Inkycal/images will not be affected by this change to allow developers to see the image wihtout having to flip the images manually
342 lines
10 KiB
Python
342 lines
10 KiB
Python
from inkycal import Settings, Layout
|
|
from inkycal.custom import *
|
|
|
|
from os.path import exists
|
|
import traceback
|
|
import logging
|
|
import arrow
|
|
import time
|
|
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
print('Pillow is not installed! Please install with:')
|
|
print('pip3 install Pillow')
|
|
|
|
try:
|
|
import numpy
|
|
except ImportError:
|
|
print('numpy is not installed! Please install with:')
|
|
print('pip3 install numpy')
|
|
|
|
logger = logging.getLogger('inkycal')
|
|
logger.setLevel(level=logging.ERROR)
|
|
|
|
class Inkycal:
|
|
"""Main class"""
|
|
|
|
def __init__(self, settings_path, render=True):
|
|
"""initialise class
|
|
settings_path = str -> location/folder of settings file
|
|
render = bool -> show something on the ePaper?
|
|
"""
|
|
self._release = '2.0.0beta'
|
|
|
|
# Check if render is boolean
|
|
if not isinstance(render, bool):
|
|
raise Exception('render must be True or False, not "{}"'.format(render))
|
|
self.render = render
|
|
|
|
# Init settings class
|
|
self.Settings = Settings(settings_path)
|
|
|
|
# Check if display support colour
|
|
self.supports_colour = self.Settings.Layout.supports_colour
|
|
|
|
# Option to flip image upside down
|
|
if self.Settings.display_orientation == 'normal':
|
|
self.upside_down = False
|
|
|
|
elif self.Settings.display_orientation == 'upside_down':
|
|
self.upside_down = True
|
|
|
|
# Option to use epaper image optimisation
|
|
self.optimize = True
|
|
|
|
# Load drivers if image should be rendered
|
|
if self.render == True:
|
|
|
|
# Get model and check if colour can be rendered
|
|
model= self.Settings.model
|
|
|
|
# Init Display class
|
|
from inkycal.display import Display
|
|
self.Display = Display(model)
|
|
|
|
# load+validate settings file. Import and setup specified modules
|
|
self.active_modules = self.Settings.active_modules()
|
|
for module in self.active_modules:
|
|
try:
|
|
loader = 'from inkycal.modules import {0}'.format(module)
|
|
module_data = self.Settings.get_config(module)
|
|
size, conf = module_data['size'], module_data['config']
|
|
setup = 'self.{} = {}(size, conf)'.format(module, module)
|
|
exec(loader)
|
|
exec(setup)
|
|
logger.debug(('{}: size: {}, config: {}'.format(module, size, conf)))
|
|
|
|
# If a module was not found, print an error message
|
|
except ImportError:
|
|
print(
|
|
'Could not find module: "{}". Please try to import manually.'.format(
|
|
module))
|
|
|
|
# Give an OK message
|
|
print('loaded inkycal')
|
|
|
|
|
|
def countdown(self, interval_mins=None ):
|
|
"""Returns the remaining time in seconds until next display update"""
|
|
|
|
# Validate update interval
|
|
allowed_intervals = [10, 15, 20, 30, 60]
|
|
|
|
# Check if empty, if empty, use value from settings file
|
|
if interval_mins == None:
|
|
interval_mins = self.Settings.update_interval
|
|
|
|
# Check if integer
|
|
if not isinstance(interval_mins, int):
|
|
raise Exception('Update interval must be an integer -> 60')
|
|
|
|
# Check if value is supported
|
|
if interval_mins not in allowed_intervals:
|
|
raise Exception('Update interval is {}, but should be one of: {}'.format(
|
|
interval_mins, allowed_intervals))
|
|
|
|
# Find out at which minutes the update should happen
|
|
now = arrow.now()
|
|
update_timings = [(60 - int(interval_mins)*updates) for updates in
|
|
range(60//int(interval_mins))][::-1]
|
|
|
|
# Calculate time in mins until next update
|
|
minutes = [_ for _ in update_timings if _>= now.minute][0] - now.minute
|
|
|
|
# Print the remaining time in mins until next update
|
|
print('{0} Minutes left until next refresh'.format(minutes))
|
|
|
|
# Calculate time in seconds until next update
|
|
remaining_time = minutes*60 + (60 - now.second)
|
|
|
|
# Return seconds until next update
|
|
return remaining_time
|
|
|
|
def test(self):
|
|
"""Inkycal test run"""
|
|
print('You are running inkycal v{}'.format(self._release))
|
|
|
|
|
|
print('Running inkycal test-run for {} ePaper'.format(
|
|
self.Settings.model))
|
|
|
|
if self.upside_down == True:
|
|
print('upside-down mode active')
|
|
|
|
for module in self.active_modules:
|
|
generate_im = 'self.{0}.generate_image()'.format(module)
|
|
print('generating image for {} module...'.format(module), end = '')
|
|
try:
|
|
exec(generate_im)
|
|
print('OK!')
|
|
except Exception as Error:
|
|
print('Error!')
|
|
print(traceback.format_exc())
|
|
|
|
def run(self):
|
|
"""Runs the main inykcal program nonstop (cannot be stopped anymore!)
|
|
Will show something on the display if render was set to True"""
|
|
|
|
# TODO: printing traceback on display (or at least a smaller message?)
|
|
# Calibration
|
|
|
|
# Get the time of initial run
|
|
runtime = arrow.now()
|
|
|
|
# Function to flip images upside down
|
|
upside_down = lambda image: image.rotate(180, expand=True)
|
|
|
|
# Count the number of times without any errors
|
|
counter = 1
|
|
|
|
while True:
|
|
print('Generating images for all modules...')
|
|
for module in self.active_modules:
|
|
generate_im = 'self.{0}.generate_image()'.format(module)
|
|
try:
|
|
exec(generate_im)
|
|
except Exception as Error:
|
|
print('Error!')
|
|
message = traceback.format_exc()
|
|
print(message)
|
|
counter = 0
|
|
print('OK')
|
|
|
|
# Assemble image from each module
|
|
self._assemble()
|
|
|
|
# Check if image should be rendered
|
|
if self.render == True:
|
|
Display = self.Display
|
|
|
|
if self.supports_colour == True:
|
|
im_black = Image.open(images+'canvas.png')
|
|
im_colour = Image.open(images+'canvas_colour.png')
|
|
|
|
# Flip the image by 180° if required
|
|
if self.upside_down == True:
|
|
im_black = upside_down(im_black)
|
|
im_colour = upside_down(im_colour)
|
|
|
|
# render the image on the display
|
|
Display.render(im_black, im_colour)
|
|
|
|
# Part for black-white ePapers
|
|
elif self.supports_colour == False:
|
|
|
|
im_black = self._merge_bands()
|
|
|
|
# Flip the image by 180° if required
|
|
if self.upside_down == True:
|
|
im_black = upside_down(im_black)
|
|
|
|
Display.render(im_black)
|
|
|
|
print('\ninkycal has been running without any errors for', end = ' ')
|
|
print('{} display updates'.format(counter))
|
|
print('That was {}'.format(runtime.humanize()))
|
|
|
|
counter += 1
|
|
|
|
sleep_time = self.countdown()
|
|
time.sleep(sleep_time)
|
|
|
|
def _merge_bands(self):
|
|
"""Merges black and coloured bands for black-white ePapers
|
|
returns the merged image
|
|
"""
|
|
|
|
im_path = images
|
|
|
|
im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png'
|
|
|
|
# If there is an image for black and colour, merge them
|
|
if exists(im1_path) and exists(im2_path):
|
|
|
|
im1 = Image.open(im1_path).convert('RGBA')
|
|
im2 = Image.open(im2_path).convert('RGBA')
|
|
|
|
def clear_white(img):
|
|
"""Replace all white pixels from image with transparent pixels
|
|
"""
|
|
x = numpy.asarray(img.convert('RGBA')).copy()
|
|
x[:, :, 3] = (255 * (x[:, :, :3] != 255).any(axis=2)).astype(numpy.uint8)
|
|
return Image.fromarray(x)
|
|
|
|
im2 = clear_white(im2)
|
|
im1.paste(im2, (0,0), im2)
|
|
|
|
# If there is no image for the coloured-band, return the bw-image
|
|
elif exists(im1_path) and not exists(im2_path):
|
|
im1 = Image.open(im1_name).convert('RGBA')
|
|
|
|
return im1
|
|
|
|
|
|
def _assemble(self):
|
|
"""Assmebles all sub-images to a single image"""
|
|
|
|
# Create an empty canvas with the size of the display
|
|
width, height = self.Settings.Layout.display_size
|
|
height, width = width, height
|
|
|
|
im_black = Image.new('RGB', (width, height), color = 'white')
|
|
im_colour = Image.new('RGB', (width ,height), color = 'white')
|
|
|
|
# Set cursor for y-axis
|
|
im1_cursor = 0
|
|
im2_cursor = 0
|
|
|
|
for module in self.active_modules:
|
|
|
|
im1_path = images+module+'.png'
|
|
im2_path = images+module+'_colour.png'
|
|
|
|
# Check if there is an image for the black band
|
|
if exists(im1_path):
|
|
|
|
# Get actual size of image
|
|
im1 = Image.open(im1_path).convert('RGBA')
|
|
im1_size = im1.size
|
|
|
|
# Get the size of the section
|
|
section_size = self.Settings.get_config(module)['size']
|
|
# Calculate coordinates to center the image
|
|
x = int( (section_size[0] - im1_size[0]) /2)
|
|
|
|
# If this is the first module, use the y-offset
|
|
if im1_cursor == 0:
|
|
y = int( (section_size[1]-im1_size[1]) /2)
|
|
else:
|
|
y = im1_cursor + int( (section_size[1]-im1_size[1]) /2)
|
|
|
|
# center the image in the section space
|
|
im_black.paste(im1, (x,y), im1)
|
|
|
|
# Shift the y-axis cursor at the beginning of next section
|
|
im1_cursor += section_size[1]
|
|
|
|
# Check if there is an image for the coloured band
|
|
if exists(im2_path):
|
|
|
|
# Get actual size of image
|
|
im2 = Image.open(im2_path).convert('RGBA')
|
|
im2_size = im2.size
|
|
|
|
# Get the size of the section
|
|
section_size = self.Settings.get_config(module)['size']
|
|
|
|
# Calculate coordinates to center the image
|
|
x = int( (section_size[0]-im2_size[0]) /2)
|
|
|
|
# If this is the first module, use the y-offset
|
|
if im2_cursor == 0:
|
|
y = int( (section_size[1]-im2_size[1]) /2)
|
|
else:
|
|
y = im2_cursor + int( (section_size[1]-im2_size[1]) /2)
|
|
|
|
# center the image in the section space
|
|
im_colour.paste(im2, (x,y), im2)
|
|
|
|
# Shift the y-axis cursor at the beginning of next section
|
|
im2_cursor += section_size[1]
|
|
|
|
if self.optimize == True:
|
|
self._optimize_im(im_black).save(images+'canvas.png', 'PNG')
|
|
self._optimize_im(im_colour).save(images+'canvas_colour.png', 'PNG')
|
|
else:
|
|
im_black.save(images+'canvas.png', 'PNG')
|
|
im_colour.save(images+'canvas_colour.png', 'PNG')
|
|
|
|
def _optimize_im(self, image, threshold=220):
|
|
"""Optimize the image for rendering on ePaper displays"""
|
|
|
|
buffer = numpy.array(image.convert('RGB'))
|
|
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
|
# grey->black
|
|
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0]
|
|
image = Image.fromarray(buffer)
|
|
return image
|
|
|
|
def calibrate(self):
|
|
"""Calibrate the ePaper display to prevent burn-ins (ghosting)
|
|
Currently has to be run manually"""
|
|
|
|
self.Display.calibrate()
|
|
|
|
|
|
def _check_for_updates(self):
|
|
"""Check if a new update is available for inkycal"""
|
|
|
|
raise NotImplementedError('Tha developer were too lazy to implement this..')
|
|
|