2022-03-31 18:36:50 +02:00
|
|
|
#!python3
|
2020-11-09 17:51:15 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
"""
|
|
|
|
Main class for inkycal Project
|
|
|
|
Copyright by aceisace
|
|
|
|
"""
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-06-22 15:52:42 +02:00
|
|
|
import os
|
2020-05-26 19:20:18 +02:00
|
|
|
import traceback
|
|
|
|
import arrow
|
|
|
|
import time
|
2020-11-09 17:51:15 +01:00
|
|
|
import json
|
2020-12-05 00:24:26 +01:00
|
|
|
import logging
|
|
|
|
from logging.handlers import RotatingFileHandler
|
|
|
|
|
|
|
|
from inkycal.display import Display
|
|
|
|
from inkycal.custom import *
|
|
|
|
from inkycal.modules.inky_image import Inkyimage as Images
|
2020-05-26 19:20:18 +02:00
|
|
|
|
|
|
|
try:
|
2022-03-31 18:36:50 +02:00
|
|
|
from PIL import Image
|
2020-05-26 19:20:18 +02:00
|
|
|
except ImportError:
|
2022-03-31 18:36:50 +02:00
|
|
|
print('Pillow is not installed! Please install with:')
|
|
|
|
print('pip3 install Pillow')
|
2020-05-26 19:20:18 +02:00
|
|
|
|
|
|
|
try:
|
2022-03-31 18:36:50 +02:00
|
|
|
import numpy
|
2020-05-26 19:20:18 +02:00
|
|
|
except ImportError:
|
2022-03-31 18:36:50 +02:00
|
|
|
print('numpy is not installed!. \nIf you are on Windows '
|
|
|
|
'run: pip3 install numpy \nIf you are on Raspberry Pi '
|
|
|
|
'remove numpy: pip3 uninstall numpy \nThen try again.')
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-11-29 14:56:44 +01:00
|
|
|
# (i): Logging shows logs above a threshold level.
|
|
|
|
# e.g. logging.DEBUG will show all from DEBUG until CRITICAL
|
|
|
|
# e.g. logging.ERROR will show from ERROR until CRITICAL
|
|
|
|
# #DEBUG > #INFO > #ERROR > #WARNING > #CRITICAL
|
|
|
|
|
|
|
|
# On the console, set a logger to show only important logs
|
|
|
|
# (level ERROR or higher)
|
|
|
|
stream_handler = logging.StreamHandler()
|
|
|
|
stream_handler.setLevel(logging.ERROR)
|
|
|
|
|
2020-12-07 00:23:42 +01:00
|
|
|
on_rtd = os.environ.get('READTHEDOCS') == 'True'
|
|
|
|
if on_rtd:
|
2022-03-31 18:36:50 +02:00
|
|
|
logging.basicConfig(
|
|
|
|
level=logging.INFO,
|
|
|
|
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
|
|
|
datefmt='%d-%m-%Y %H:%M:%S',
|
|
|
|
handlers=[stream_handler])
|
|
|
|
|
2020-12-07 00:23:42 +01:00
|
|
|
else:
|
2022-03-31 18:36:50 +02:00
|
|
|
# Save all logs to a file, which contains more detailed output
|
|
|
|
logging.basicConfig(
|
|
|
|
level=logging.INFO,
|
|
|
|
format='%(asctime)s | %(name)s | %(levelname)s: %(message)s',
|
|
|
|
datefmt='%d-%m-%Y %H:%M:%S',
|
|
|
|
handlers=[
|
|
|
|
|
|
|
|
stream_handler, # add stream handler from above
|
|
|
|
|
|
|
|
RotatingFileHandler( # log to a file too
|
|
|
|
f'{top_level}/logs/inkycal.log', # file to log
|
|
|
|
maxBytes=2097152, # 2MB max filesize
|
|
|
|
backupCount=5 # create max 5 log files
|
2020-12-07 00:23:42 +01:00
|
|
|
)
|
2022-03-31 18:36:50 +02:00
|
|
|
]
|
2020-12-07 00:23:42 +01:00
|
|
|
)
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2020-11-30 12:08:29 +01:00
|
|
|
# Show less logging for PIL module
|
|
|
|
logging.getLogger("PIL").setLevel(logging.WARNING)
|
|
|
|
|
2020-11-30 08:59:21 +01:00
|
|
|
filename = os.path.basename(__file__).split('.py')[0]
|
|
|
|
logger = logging.getLogger(filename)
|
2020-11-09 17:51:15 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
|
2020-11-24 00:40:49 +01:00
|
|
|
# TODO: autostart -> supervisor?
|
|
|
|
|
2020-05-29 04:00:39 +02:00
|
|
|
class Inkycal:
|
2022-03-31 18:36:50 +02:00
|
|
|
"""Inkycal main class
|
2020-05-29 04:00:39 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Main class of Inkycal, test and run the main Inkycal program.
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Args:
|
|
|
|
- settings_path = str -> the full path to your settings.json file
|
|
|
|
if no path is given, tries looking for settings file in /boot folder.
|
|
|
|
- render = bool (True/False) -> show the image on the epaper display?
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Attributes:
|
|
|
|
- optimize = True/False. Reduce number of colours on the generated image
|
|
|
|
to improve rendering on E-Papers. Set this to False for 9.7" E-Paper.
|
|
|
|
"""
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
def __init__(self, settings_path=None, render=True):
|
|
|
|
"""Initialise Inkycal"""
|
2020-11-09 17:51:15 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
self._release = '2.0.0'
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Check if render was set correctly
|
|
|
|
if render not in [True, False]:
|
|
|
|
raise Exception(f'render must be True or False, not "{render}"')
|
|
|
|
self.render = render
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# load settings file - throw an error if file could not be found
|
|
|
|
if settings_path:
|
|
|
|
try:
|
|
|
|
with open(settings_path) as settings_file:
|
|
|
|
settings = json.load(settings_file)
|
|
|
|
self.settings = settings
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
except FileNotFoundError:
|
2022-04-10 06:35:08 +02:00
|
|
|
raise SettingsFileNotFoundError
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
with open('/boot/settings.json') as settings_file:
|
|
|
|
settings = json.load(settings_file)
|
|
|
|
self.settings = settings
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
except FileNotFoundError:
|
2022-04-10 06:35:08 +02:00
|
|
|
raise SettingsFileNotFoundError
|
2022-03-31 18:36:50 +02:00
|
|
|
|
|
|
|
# Option to use epaper image optimisation, reduces colours
|
|
|
|
self.optimize = True
|
|
|
|
|
|
|
|
# Load drivers if image should be rendered
|
2022-03-31 19:04:42 +02:00
|
|
|
if self.render:
|
2022-03-31 18:36:50 +02:00
|
|
|
# Init Display class with model in settings file
|
|
|
|
# from inkycal.display import Display
|
|
|
|
self.Display = Display(settings["model"])
|
|
|
|
|
|
|
|
# check if colours can be rendered
|
|
|
|
self.supports_colour = True if 'colour' in settings['model'] else False
|
|
|
|
|
|
|
|
# get calibration hours
|
|
|
|
self._calibration_hours = self.settings['calibration_hours']
|
|
|
|
|
|
|
|
# init calibration state
|
|
|
|
self._calibration_state = False
|
|
|
|
|
|
|
|
# Load and intialize modules specified in the settings file
|
|
|
|
self._module_number = 1
|
|
|
|
for module in settings['modules']:
|
|
|
|
module_name = module['name']
|
|
|
|
try:
|
|
|
|
loader = f'from inkycal.modules import {module_name}'
|
|
|
|
# print(loader)
|
|
|
|
exec(loader)
|
|
|
|
setup = f'self.module_{self._module_number} = {module_name}({module})'
|
|
|
|
# print(setup)
|
|
|
|
exec(setup)
|
|
|
|
logger.info(('name : {name} size : {width}x{height} px'.format(
|
|
|
|
name=module_name,
|
|
|
|
width=module['config']['size'][0],
|
|
|
|
height=module['config']['size'][1])))
|
|
|
|
|
|
|
|
self._module_number += 1
|
|
|
|
|
|
|
|
# If a module was not found, print an error message
|
|
|
|
except ImportError:
|
|
|
|
print(f'Could not find module: "{module}". Please try to import manually')
|
|
|
|
|
|
|
|
# If something unexpected happened, show the error message
|
|
|
|
except Exception as e:
|
|
|
|
print(str(e))
|
|
|
|
|
|
|
|
# Path to store images
|
|
|
|
self.image_folder = top_level + '/images'
|
|
|
|
|
|
|
|
# Give an OK message
|
|
|
|
print('loaded inkycal')
|
|
|
|
|
|
|
|
def countdown(self, interval_mins=None):
|
|
|
|
"""Returns the remaining time in seconds until next display update"""
|
|
|
|
|
|
|
|
# Check if empty, if empty, use value from settings file
|
2022-03-31 19:04:42 +02:00
|
|
|
if interval_mins is None:
|
2022-03-31 18:36:50 +02:00
|
|
|
interval_mins = self.settings["update_interval"]
|
|
|
|
|
|
|
|
# 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(f'{minutes} minutes left until next refresh')
|
|
|
|
|
|
|
|
# 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):
|
|
|
|
"""Tests if Inkycal can run without issues.
|
|
|
|
|
|
|
|
Attempts to import module names from settings file. Loads the config
|
|
|
|
for each module and initializes the module. Tries to run the module and
|
|
|
|
checks if the images could be generated correctly.
|
|
|
|
|
|
|
|
Generated images can be found in the /images folder of Inkycal.
|
|
|
|
"""
|
|
|
|
|
|
|
|
print(f'Inkycal version: v{self._release}')
|
|
|
|
print(f'Selected E-paper display: {self.settings["model"]}')
|
|
|
|
|
|
|
|
# store module numbers in here
|
|
|
|
errors = []
|
|
|
|
|
|
|
|
# short info for info-section
|
|
|
|
self.info = f"{arrow.now().format('D MMM @ HH:mm')} "
|
|
|
|
|
|
|
|
for number in range(1, self._module_number):
|
|
|
|
name = eval(f"self.module_{number}.name")
|
|
|
|
module = eval(f'self.module_{number}')
|
|
|
|
print(f'generating image(s) for {name}...', end="")
|
|
|
|
try:
|
|
|
|
black, colour = module.generate_image()
|
|
|
|
black.save(f"{self.image_folder}/module{number}_black.png", "PNG")
|
|
|
|
colour.save(f"{self.image_folder}/module{number}_colour.png", "PNG")
|
|
|
|
print('OK!')
|
2022-03-31 19:04:42 +02:00
|
|
|
except:
|
2022-03-31 18:36:50 +02:00
|
|
|
errors.append(number)
|
|
|
|
self.info += f"module {number}: Error! "
|
|
|
|
print('Error!')
|
|
|
|
print(traceback.format_exc())
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
if errors:
|
|
|
|
print('Error/s in modules:', *errors)
|
|
|
|
del errors
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
self._assemble()
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
"""Runs main program in nonstop mode.
|
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
Uses an infinity loop to run Inkycal nonstop. Inkycal generates the image
|
2022-03-31 18:36:50 +02:00
|
|
|
from all modules, assembles them in one image, refreshed the E-Paper and
|
|
|
|
then sleeps until the next sheduled update.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Get the time of initial run
|
|
|
|
runtime = arrow.now()
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Function to flip images upside down
|
|
|
|
upside_down = lambda image: image.rotate(180, expand=True)
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Count the number of times without any errors
|
|
|
|
counter = 0
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
print(f'Inkycal version: v{self._release}')
|
|
|
|
print(f'Selected E-paper display: {self.settings["model"]}')
|
2020-11-24 00:40:49 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
while True:
|
|
|
|
current_time = arrow.now(tz=get_system_tz())
|
|
|
|
print(f"Date: {current_time.format('D MMM YY')} | "
|
|
|
|
f"Time: {current_time.format('HH:mm')}")
|
|
|
|
print('Generating images for all modules...', end='')
|
2020-11-24 00:40:49 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
errors = [] # store module numbers in here
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# short info for info-section
|
|
|
|
self.info = f"{current_time.format('D MMM @ HH:mm')} "
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
for number in range(1, self._module_number):
|
2020-05-26 19:20:18 +02:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
# name = eval(f"self.module_{number}.name")
|
2022-03-31 18:36:50 +02:00
|
|
|
module = eval(f'self.module_{number}')
|
2020-11-25 14:24:29 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
try:
|
|
|
|
black, colour = module.generate_image()
|
|
|
|
black.save(f"{self.image_folder}/module{number}_black.png", "PNG")
|
|
|
|
colour.save(f"{self.image_folder}/module{number}_colour.png", "PNG")
|
|
|
|
self.info += f"module {number}: OK "
|
2022-03-31 19:04:42 +02:00
|
|
|
except:
|
2022-03-31 18:36:50 +02:00
|
|
|
errors.append(number)
|
|
|
|
print('error!')
|
|
|
|
print(traceback.format_exc())
|
|
|
|
self.info += f"module {number}: error! "
|
|
|
|
logger.exception(f'Exception in module {number}')
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
if errors:
|
|
|
|
print('error/s in modules:', *errors)
|
|
|
|
counter = 0
|
|
|
|
else:
|
|
|
|
counter += 1
|
|
|
|
print('successful')
|
|
|
|
del errors
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Assemble image from each module - add info section if specified
|
|
|
|
self._assemble()
|
2020-11-25 14:24:29 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Check if image should be rendered
|
2022-03-31 19:04:42 +02:00
|
|
|
if self.render:
|
|
|
|
display = self.Display
|
2020-11-24 00:40:49 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
self._calibration_check()
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
if self.supports_colour:
|
2022-03-31 18:36:50 +02:00
|
|
|
im_black = Image.open(f"{self.image_folder}/canvas.png")
|
|
|
|
im_colour = Image.open(f"{self.image_folder}/canvas_colour.png")
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Flip the image by 180° if required
|
|
|
|
if self.settings['orientation'] == 180:
|
|
|
|
im_black = upside_down(im_black)
|
|
|
|
im_colour = upside_down(im_colour)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# render the image on the display
|
2022-03-31 19:04:42 +02:00
|
|
|
display.render(im_black, im_colour)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Part for black-white ePapers
|
2022-03-31 19:04:42 +02:00
|
|
|
elif not self.supports_colour:
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
im_black = self._merge_bands()
|
2020-11-30 12:08:29 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Flip the image by 180° if required
|
|
|
|
if self.settings['orientation'] == 180:
|
|
|
|
im_black = upside_down(im_black)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
display.render(im_black)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
print(f'\nNo errors since {counter} display updates \n'
|
|
|
|
f'program started {runtime.humanize()}')
|
2020-11-23 22:36:04 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
sleep_time = self.countdown()
|
|
|
|
time.sleep(sleep_time)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
@staticmethod
|
|
|
|
def _merge_bands():
|
2022-03-31 18:36:50 +02:00
|
|
|
"""Merges black and coloured bands for black-white ePapers
|
|
|
|
returns the merged image
|
|
|
|
"""
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
im1_path, im2_path = images + 'canvas.png', images + 'canvas_colour.png'
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# If there is an image for black and colour, merge them
|
|
|
|
if os.path.exists(im1_path) and os.path.exists(im2_path):
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
im1 = Image.open(im1_path).convert('RGBA')
|
|
|
|
im2 = Image.open(im2_path).convert('RGBA')
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
im1 = Images.merge(im1, im2)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# If there is no image for the coloured-band, return the bw-image
|
|
|
|
elif os.path.exists(im1_path) and not os.path.exists(im2_path):
|
2022-03-31 19:04:42 +02:00
|
|
|
im1 = Image.open(im1_path).convert('RGBA')
|
|
|
|
|
|
|
|
else:
|
|
|
|
raise FileNotFoundError("Inkycal cannot find images to merge")
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
return im1
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
def _assemble(self):
|
|
|
|
"""Assembles all sub-images to a single image"""
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Create 2 blank images with the same resolution as the display
|
|
|
|
width, height = Display.get_display_size(self.settings["model"])
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Since Inkycal runs in vertical mode, switch the height and width
|
|
|
|
width, height = height, width
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
im_black = Image.new('RGB', (width, height), color='white')
|
|
|
|
im_colour = Image.new('RGB', (width, height), color='white')
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Set cursor for y-axis
|
|
|
|
im1_cursor = 0
|
|
|
|
im2_cursor = 0
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
for number in range(1, self._module_number):
|
2020-05-29 04:00:39 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# get the path of the current module's generated images
|
|
|
|
im1_path = f"{self.image_folder}/module{number}_black.png"
|
|
|
|
im2_path = f"{self.image_folder}/module{number}_colour.png"
|
2020-06-22 15:52:42 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Check if there is an image for the black band
|
|
|
|
if os.path.exists(im1_path):
|
2020-06-22 15:52:42 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Get actual size of image
|
|
|
|
im1 = Image.open(im1_path).convert('RGBA')
|
|
|
|
im1_size = im1.size
|
2020-06-22 15:52:42 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Get the size of the section
|
2022-03-31 19:04:42 +02:00
|
|
|
section_size = [i for i in self.settings['modules'] if i['position'] == number][0]['config']['size']
|
2020-06-22 15:52:42 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Calculate coordinates to center the image
|
|
|
|
x = int((section_size[0] - im1_size[0]) / 2)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# 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)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# center the image in the section space
|
|
|
|
im_black.paste(im1, (x, y), im1)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Shift the y-axis cursor at the beginning of next section
|
|
|
|
im1_cursor += section_size[1]
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Check if there is an image for the coloured band
|
|
|
|
if os.path.exists(im2_path):
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Get actual size of image
|
|
|
|
im2 = Image.open(im2_path).convert('RGBA')
|
|
|
|
im2_size = im2.size
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Get the size of the section
|
2022-03-31 19:04:42 +02:00
|
|
|
section_size = [i for i in self.settings['modules'] if i['position'] == number][0]['config']['size']
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Calculate coordinates to center the image
|
|
|
|
x = int((section_size[0] - im2_size[0]) / 2)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# 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)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# center the image in the section space
|
|
|
|
im_colour.paste(im2, (x, y), im2)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Shift the y-axis cursor at the beginning of next section
|
|
|
|
im2_cursor += section_size[1]
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Add info-section if specified --
|
2020-06-22 15:52:42 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Calculate the max. fontsize for info-section
|
2022-03-31 19:04:42 +02:00
|
|
|
if self.settings['info_section']:
|
2022-03-31 18:36:50 +02:00
|
|
|
info_height = self.settings["info_section_height"]
|
|
|
|
info_width = width
|
|
|
|
font = self.font = ImageFont.truetype(
|
|
|
|
fonts['NotoSansUI-Regular'], size=14)
|
2020-06-22 15:52:42 +02:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
info_x = im_black.size[1] - info_height
|
|
|
|
write(im_black, (0, info_x), (info_width, info_height),
|
|
|
|
self.info, font=font)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# optimize the image by mapping colours to pure black and white
|
2022-03-31 19:04:42 +02:00
|
|
|
if self.optimize:
|
2022-03-31 18:36:50 +02:00
|
|
|
im_black = self._optimize_im(im_black)
|
|
|
|
im_colour = self._optimize_im(im_colour)
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
im_black.save(self.image_folder + '/canvas.png', 'PNG')
|
|
|
|
im_colour.save(self.image_folder + '/canvas_colour.png', 'PNG')
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
# Additionally combine the two images with color
|
|
|
|
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)
|
|
|
|
|
|
|
|
# Additionally combine the two images with color
|
|
|
|
def black_to_colour(img):
|
|
|
|
"""Replace all black pixels from image with red pixels
|
|
|
|
"""
|
|
|
|
buffer = numpy.array(img.convert('RGB'))
|
|
|
|
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
|
|
|
|
|
|
|
threshold = 220
|
|
|
|
|
|
|
|
# non-white -> red
|
|
|
|
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [255, 0, 0]
|
|
|
|
|
|
|
|
return Image.fromarray(buffer)
|
|
|
|
|
|
|
|
# Save full-screen images as well
|
|
|
|
im_black = clear_white(im_black)
|
|
|
|
im_colour = black_to_colour(im_colour)
|
|
|
|
|
|
|
|
im_colour.paste(im_black, (0, 0), im_black)
|
|
|
|
im_colour.save(images + 'full-screen.png', 'PNG')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _optimize_im(image, threshold=220):
|
2022-03-31 18:36:50 +02:00
|
|
|
"""Optimize the image for rendering on ePaper displays"""
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
buffer = numpy.array(image.convert('RGB'))
|
|
|
|
red, green = buffer[:, :, 0], buffer[:, :, 1]
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# grey->black
|
|
|
|
buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0, 0, 0]
|
|
|
|
image = Image.fromarray(buffer)
|
|
|
|
return image
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
def calibrate(self):
|
|
|
|
"""Calibrate the E-Paper display
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Uses the Display class to calibrate the display with the default of 3
|
|
|
|
cycles. After a refresh cycle, a new image is generated and shown.
|
|
|
|
"""
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
self.Display.calibrate()
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
def _calibration_check(self):
|
|
|
|
"""Calibration sheduler
|
|
|
|
uses calibration hours from settings file to check if calibration is due"""
|
|
|
|
now = arrow.now()
|
|
|
|
# print('hour:', now.hour, 'hours:', self._calibration_hours)
|
|
|
|
# print('state:', self._calibration_state)
|
|
|
|
if now.hour in self._calibration_hours and self._calibration_state == False:
|
|
|
|
self.calibrate()
|
|
|
|
self._calibration_state = True
|
2020-11-21 16:31:00 +01:00
|
|
|
else:
|
2022-03-31 18:36:50 +02:00
|
|
|
self._calibration_state = False
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
@classmethod
|
|
|
|
def add_module(cls, filepath):
|
|
|
|
"""registers a third party module for inkycal.
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Uses the full filepath of the third party module to check if it is inside
|
|
|
|
the correct folder, then checks if it's an inkycal module. Lastly, the
|
|
|
|
init files in /inkycal and /inkycal/modules are updated to allow using
|
|
|
|
the new module.
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Args:
|
|
|
|
- filepath: The full filepath of the third party module. Modules should be
|
|
|
|
in Inkycal/inkycal/modules.
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Usage:
|
|
|
|
- download a third-party module. The exact link is provided by the
|
|
|
|
developer of that module and starts with
|
|
|
|
`https://raw.githubusercontent.com/...`
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
enter the following in bash to download a module::
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
$ cd Inkycal/inkycal/modules #navigate to modules folder in inkycal
|
|
|
|
$ wget https://raw.githubusercontent.com/... #download the module
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
then register it with this function::
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
>>> from inkycal import Inkycal
|
2022-03-31 18:36:50 +02:00
|
|
|
>>> Inkycal.add_module('/full/path/to/the/module/in/inkycal/modules.py')
|
|
|
|
"""
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
module_folder = top_level + '/inkycal/modules'
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
if module_folder in filepath:
|
|
|
|
filename = filepath.split('.py')[0].split('/')[-1]
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
# Extract name of class from given module and validate if it's an inkycal
|
|
|
|
# module
|
|
|
|
with open(filepath, mode='r') as module:
|
|
|
|
module_content = module.read().splitlines()
|
2020-11-24 00:40:49 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
for line in module_content:
|
|
|
|
if '(inkycal_module):' in line:
|
|
|
|
classname = line.split(' ')[-1].split('(')[0]
|
|
|
|
break
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
if not classname:
|
|
|
|
raise TypeError("your module doesn't seem to be a correct inkycal module.."
|
|
|
|
"Please check your module again.")
|
2020-11-21 16:31:00 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
# Check if filename or classname exists in init of module folder
|
|
|
|
with open(module_folder + '/__init__.py', mode='r') as file:
|
|
|
|
module_init = file.read().splitlines()
|
2022-03-31 18:36:50 +02:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
print('checking module init file..')
|
|
|
|
for line in module_init:
|
|
|
|
if filename in line:
|
|
|
|
raise Exception(
|
|
|
|
"A module with this filename already exists! \n"
|
|
|
|
"Please consider renaming your module and try again."
|
|
|
|
)
|
|
|
|
if classname in line:
|
|
|
|
raise Exception(
|
|
|
|
"A module with this classname already exists! \n"
|
|
|
|
"Please consider renaming your class and try again."
|
|
|
|
)
|
|
|
|
print('OK!')
|
|
|
|
|
|
|
|
# Check if filename or classname exists in init of inkycal folder
|
|
|
|
with open(top_level + '/inkycal/__init__.py', mode='r') as file:
|
|
|
|
inkycal_init = file.read().splitlines()
|
|
|
|
|
|
|
|
print('checking inkycal init file..')
|
|
|
|
for line in inkycal_init:
|
|
|
|
if filename in line:
|
|
|
|
raise Exception(
|
|
|
|
"A module with this filename already exists! \n"
|
|
|
|
"Please consider renaming your module and try again."
|
|
|
|
)
|
|
|
|
if classname in line:
|
|
|
|
raise Exception(
|
|
|
|
"A module with this classname already exists! \n"
|
|
|
|
"Please consider renaming your class and try again."
|
|
|
|
)
|
|
|
|
print('OK')
|
|
|
|
|
|
|
|
# If all checks have passed, add the module in the module init file
|
|
|
|
with open(module_folder + '/__init__.py', mode='a') as file:
|
|
|
|
file.write(f'from .{filename} import {classname} # Added by module adder')
|
|
|
|
|
|
|
|
# If all checks have passed, add the module in the inkycal init file
|
|
|
|
with open(top_level + '/inkycal/__init__.py', mode='a') as file:
|
|
|
|
file.write(f'import inkycal.modules.{filename} # Added by module adder')
|
|
|
|
|
|
|
|
print(f"Your module '{filename}' with class '{classname}' has been added "
|
|
|
|
"successfully! Hooray!")
|
|
|
|
return
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
# Check if module is inside the modules folder
|
|
|
|
raise Exception(f"Your module should be in {module_folder} "
|
|
|
|
f"but is currently in {filepath}")
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
@classmethod
|
|
|
|
def remove_module(cls, filename, remove_file=True):
|
2022-03-31 19:04:42 +02:00
|
|
|
"""unregisters an inkycal module.
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Looks for given filename.py in /modules folder, removes entries of that
|
|
|
|
module in init files inside /inkycal and /inkycal/modules
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Args:
|
|
|
|
- filename: The filename (with .py ending) of the module which should be
|
|
|
|
unregistered. e.g. `'mymodule.py'`
|
|
|
|
- remove_file: ->bool (True/False). If set to True, the module is deleted
|
|
|
|
after unregistering it, else it remains in the /modules folder
|
2020-11-24 15:32:11 +01:00
|
|
|
|
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Usage:
|
|
|
|
- Look for the module in Inkycal/inkycal/modules which should be removed.
|
|
|
|
Only the filename (with .py) is required, not the full path.
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
Use this function to unregister the module from inkycal::
|
2020-11-30 12:08:29 +01:00
|
|
|
|
2022-03-31 19:04:42 +02:00
|
|
|
>>> from inkycal import Inkycal
|
2022-03-31 18:36:50 +02:00
|
|
|
>>> Inkycal.remove_module('mymodule.py')
|
|
|
|
"""
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
module_folder = top_level + '/inkycal/modules'
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Check if module is inside the modules folder and extract classname
|
|
|
|
try:
|
|
|
|
with open(f"{module_folder}/{filename}", mode='r') as file:
|
|
|
|
module_content = file.read().splitlines()
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
for line in module_content:
|
|
|
|
if '(inkycal_module):' in line:
|
|
|
|
classname = line.split(' ')[-1].split('(')[0]
|
|
|
|
break
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
if not classname:
|
|
|
|
print('The module you are trying to remove is not an inkycal module.. '
|
|
|
|
'Not removing it.')
|
|
|
|
return
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
except FileNotFoundError:
|
|
|
|
print(f"No module named {filename} found in {module_folder}")
|
|
|
|
return
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
filename = filename.split('.py')[0]
|
2020-11-30 12:08:29 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Create a memory backup of /modules init file
|
|
|
|
with open(module_folder + '/__init__.py', mode='r') as file:
|
|
|
|
module_init = file.read().splitlines()
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
print('removing line from module_init')
|
|
|
|
# Remove lines that contain classname
|
|
|
|
with open(module_folder + '/__init__.py', mode='w') as file:
|
|
|
|
for line in module_init:
|
|
|
|
if not classname in line:
|
|
|
|
file.write(line + '\n')
|
|
|
|
else:
|
|
|
|
print('found, removing')
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# Create a memory backup of inkycal init file
|
|
|
|
with open(f"{top_level}/inkycal/__init__.py", mode='r') as file:
|
|
|
|
inkycal_init = file.read().splitlines()
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
print('removing line from inkycal init')
|
|
|
|
# Remove lines that contain classname
|
|
|
|
with open(f"{top_level}/inkycal/__init__.py", mode='w') as file:
|
|
|
|
for line in inkycal_init:
|
2022-03-31 19:04:42 +02:00
|
|
|
if filename in line:
|
2022-03-31 18:36:50 +02:00
|
|
|
print('found, removing')
|
2022-03-31 19:04:42 +02:00
|
|
|
else:
|
|
|
|
file.write(line + '\n')
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
# remove the file of the third party module if it exists and remove_file
|
|
|
|
# was set to True (default)
|
2022-03-31 19:04:42 +02:00
|
|
|
if os.path.exists(f"{module_folder}/{filename}.py") and remove_file is True:
|
2022-03-31 18:36:50 +02:00
|
|
|
print('deleting module file')
|
|
|
|
os.remove(f"{module_folder}/{filename}.py")
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2022-03-31 18:36:50 +02:00
|
|
|
print(f"Your module '{filename}' with class '{classname}' was removed.")
|
2020-11-24 15:32:11 +01:00
|
|
|
|
2020-11-21 16:31:00 +01:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2022-03-31 18:36:50 +02:00
|
|
|
print(f'running inkycal main in standalone/debug mode')
|