2020-05-29 04:00:39 +02:00
|
|
|
from inkycal import Settings, Layout
|
2020-05-26 19:20:18 +02:00
|
|
|
from inkycal.custom import *
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
from os.path import exists
|
2020-05-26 19:20:18 +02:00
|
|
|
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')
|
2020-05-29 04:00:39 +02:00
|
|
|
logger.setLevel(level=logging.ERROR)
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
class Inkycal:
|
2020-05-26 19:20:18 +02:00
|
|
|
"""Main class"""
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
def __init__(self, settings_path, render=True):
|
2020-05-26 19:20:18 +02:00
|
|
|
"""initialise class
|
|
|
|
settings_path = str -> location/folder of settings file
|
|
|
|
render = bool -> show something on the ePaper?
|
|
|
|
"""
|
2020-05-29 04:00:39 +02:00
|
|
|
self._release = '2.0.0beta'
|
|
|
|
|
2020-05-26 19:20:18 +02:00
|
|
|
# Check if render is boolean
|
|
|
|
if not isinstance(render, bool):
|
|
|
|
raise Exception('render must be True or False, not "{}"'.format(render))
|
|
|
|
self.render = render
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
# Init settings class
|
2020-05-26 19:20:18 +02:00
|
|
|
self.Settings = Settings(settings_path)
|
2020-05-29 04:00:39 +02:00
|
|
|
|
|
|
|
# Check if display support colour
|
|
|
|
self.supports_colour = self.Settings.Layout.supports_colour
|
|
|
|
|
|
|
|
# Option to flip image upside down
|
|
|
|
self.upside_down = False
|
|
|
|
|
|
|
|
# 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
|
2020-05-26 19:20:18 +02:00
|
|
|
self.active_modules = self.Settings.active_modules()
|
|
|
|
for module in self.active_modules:
|
|
|
|
try:
|
2020-05-29 04:00:39 +02:00
|
|
|
loader = 'from inkycal.modules import {0}'.format(module)
|
2020-05-26 19:20:18 +02:00
|
|
|
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):
|
2020-05-29 04:00:39 +02:00
|
|
|
"""Inkycal test run"""
|
|
|
|
print('You are running inkycal v{}'.format(self._release))
|
|
|
|
|
|
|
|
|
|
|
|
print('Running inkyal test-run for {} ePaper'.format(
|
|
|
|
self.Settings.model))
|
2020-05-26 19:20:18 +02:00
|
|
|
|
|
|
|
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())
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
def run(self):
|
2020-05-26 19:20:18 +02:00
|
|
|
"""Runs the main inykcal program nonstop (cannot be stopped anymore!)
|
2020-05-29 04:00:39 +02:00
|
|
|
Will show something on the display if render was set to True"""
|
|
|
|
|
2020-05-26 19:20:18 +02:00
|
|
|
# TODO: printing traceback on display (or at least a smaller message?)
|
|
|
|
# Calibration
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
# 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
|
2020-05-26 19:20:18 +02:00
|
|
|
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')
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
# 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:
|
|
|
|
upside_down(im_black)
|
|
|
|
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:
|
|
|
|
upside_down(im_black)
|
|
|
|
|
|
|
|
Display.render(im_black)
|
2020-05-26 19:20:18 +02:00
|
|
|
|
|
|
|
print('\ninkycal has been running without any errors for', end = ' ')
|
2020-05-29 04:00:39 +02:00
|
|
|
print('{} display updates'.format(counter))
|
|
|
|
print('That was {}'.format(runtime.humanize()))
|
|
|
|
|
2020-05-26 19:20:18 +02:00
|
|
|
counter += 1
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
sleep_time = self.countdown()
|
2020-05-26 19:20:18 +02:00
|
|
|
time.sleep(sleep_time)
|
|
|
|
|
2020-06-07 15:57:01 +02:00
|
|
|
def _merge_bands(self):
|
2020-05-29 04:00:39 +02:00
|
|
|
"""Merges black and coloured bands for black-white ePapers
|
|
|
|
returns the merged image
|
|
|
|
"""
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
im_path = images
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png'
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
# If there is an image for black and colour, merge them
|
|
|
|
if exists(im1_path) and exists(im2_path):
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-06-07 15:57:01 +02:00
|
|
|
im1 = Image.open(im1_path).convert('RGBA')
|
|
|
|
im2 = Image.open(im2_path).convert('RGBA')
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
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)
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
im2 = clear_white(im2)
|
|
|
|
im1.paste(im2, (0,0), im2)
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
# 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')
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
return im1
|
2020-05-26 19:20:18 +02:00
|
|
|
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
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
|
|
|
|
|
|
|
|
# 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] - y
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# 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] - y
|
|
|
|
|
|
|
|
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]
|
2020-06-07 15:57:01 +02:00
|
|
|
# grey->black
|
|
|
|
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0]
|
2020-05-29 04:00:39 +02:00
|
|
|
image = Image.fromarray(buffer)
|
|
|
|
return image
|
|
|
|
|
|
|
|
def calibrate(self):
|
|
|
|
"""Calibrate the ePaper display to prevent burn-ins (ghosting)
|
|
|
|
Currently has to be run manually"""
|
2020-06-07 15:57:01 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
self.Display.calibrate()
|
|
|
|
|
|
|
|
|
|
|
|
def _check_for_updates(self):
|
|
|
|
"""Check if a new update is available for inkycal"""
|
2020-06-07 15:57:01 +02:00
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
raise NotImplementedError('Tha developer were too lazy to implement this..')
|
2020-05-26 19:20:18 +02:00
|
|
|
|