Adapted main file to support new web-ui
A lot of work behind the scenes. Logging is now handled by main file now. Improved logging, support for logging from modules, improved support for info section, added support for info section height, slight improvements in printing output
This commit is contained in:
parent
f1ce2911eb
commit
49b0d7cc65
372
inkycal/main.py
372
inkycal/main.py
@ -27,18 +27,22 @@ except ImportError:
|
||||
print('numpy is not installed! Please install with:')
|
||||
print('pip3 install numpy')
|
||||
|
||||
filename = os.path.basename(__file__).split('.py')[0]
|
||||
logger = logging.getLogger(filename)
|
||||
logger.setLevel(level=logging.ERROR)
|
||||
logging.basicConfig(
|
||||
level = logging.INFO, #DEBUG > #INFO > #ERROR > #WARNING > #CRITICAL
|
||||
format='%(name)s -> %(levelname)s -> %(asctime)s -> %(message)s',
|
||||
datefmt='%d-%m-%Y %H:%M')
|
||||
|
||||
logger = logging.getLogger('inykcal main')
|
||||
|
||||
class Inkycal:
|
||||
"""Inkycal main class"""
|
||||
|
||||
def __init__(self, settings_path, render=True):
|
||||
def __init__(self, settings_path=None, render=True):
|
||||
"""Initialise Inkycal
|
||||
settings_path = str -> location/folder of settings file
|
||||
render = bool -> show something on the ePaper?
|
||||
settings_path = str -> the full path to your settings.json file
|
||||
if no path is given, try looking for settings file in /boot folder
|
||||
|
||||
render = bool (True/False) -> show the image on the epaper display?
|
||||
"""
|
||||
self._release = '2.0.0'
|
||||
|
||||
@ -48,75 +52,82 @@ class Inkycal:
|
||||
self.render = render
|
||||
|
||||
# load settings file - throw an error if file could not be found
|
||||
try:
|
||||
with open(settings_path) as file:
|
||||
settings = json.load(file)
|
||||
self.settings = settings
|
||||
if settings_path:
|
||||
try:
|
||||
with open(settings_path) as settings_file:
|
||||
settings = json.load(settings_file)
|
||||
self.settings = settings
|
||||
|
||||
except FileNotFoundError:
|
||||
print('No settings file found in specified location')
|
||||
print('Please double check your path')
|
||||
except FileNotFoundError:
|
||||
print('No settings file found in given path\n'
|
||||
'Please double check your settings_path')
|
||||
return
|
||||
|
||||
# Option to use epaper image optimisation
|
||||
else:
|
||||
try:
|
||||
with open('/boot/settings.json') as settings_file:
|
||||
settings = json.load(settings_file)
|
||||
self.settings = settings
|
||||
|
||||
except FileNotFoundError:
|
||||
print('No settings file found in /boot')
|
||||
return
|
||||
|
||||
|
||||
# Option to use epaper image optimisation, reduces colours
|
||||
self.optimize = True
|
||||
|
||||
# Init Display class with model in settings file
|
||||
from inkycal.display import Display
|
||||
self.Display = Display(settings["model"])
|
||||
|
||||
# Load drivers if image should be rendered
|
||||
if self.render == True:
|
||||
|
||||
# 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
|
||||
|
||||
# init calibration state
|
||||
self._calibration_state = False
|
||||
|
||||
|
||||
|
||||
# WIP
|
||||
# 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)
|
||||
conf = module["config"]
|
||||
#size, conf = module_data['size'], module_data['config']
|
||||
setup = f'self.{module} = {module}(size, conf)'
|
||||
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.debug(('{}: size: {}, config: {}'.format(module, size, conf)))
|
||||
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(
|
||||
'Could not find module: "{}". Please try to import manually.'.format(
|
||||
module))
|
||||
print('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"""
|
||||
|
||||
# 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))
|
||||
interval_mins = self.settings["update_interval"]
|
||||
|
||||
# Find out at which minutes the update should happen
|
||||
now = arrow.now()
|
||||
@ -127,7 +138,7 @@ class Inkycal:
|
||||
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))
|
||||
print(f'{minutes} Minutes left until next refresh')
|
||||
|
||||
# Calculate time in seconds until next update
|
||||
remaining_time = minutes*60 + (60 - now.second)
|
||||
@ -136,17 +147,284 @@ class Inkycal:
|
||||
return remaining_time
|
||||
|
||||
|
||||
def test(self):
|
||||
"""Inkycal test run
|
||||
Generates images for each module, one by one and prints OK if no
|
||||
problems were found."""
|
||||
|
||||
print(f'Inkycal version: v{self._release}')
|
||||
print(f'Selected E-paper display: {self.settings["model"]}')
|
||||
|
||||
# store module numbers in here
|
||||
errors = []
|
||||
|
||||
for number in range(1, self._module_number):
|
||||
name = eval(f"self.module_{number}.name")
|
||||
generate_im = f'black,colour=self.module_{number}.generate_image()'
|
||||
save_black = f'black.save("{self.image_folder}/module{number}_black.png", "PNG")'
|
||||
save_colour = f'colour.save("{self.image_folder}/module{number}_colour.png", "PNG")'
|
||||
full_command = generate_im+'\n'+save_black+'\n'+save_colour
|
||||
#print(full_command)
|
||||
|
||||
print(f'generating image(s) for {name}...')
|
||||
try:
|
||||
exec(full_command)
|
||||
except Exception as Error:
|
||||
errors.append(number)
|
||||
print('Error!')
|
||||
print(traceback.format_exc())
|
||||
|
||||
if errors:
|
||||
print('Error/s in modules:',*errors)
|
||||
del errors
|
||||
|
||||
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"""
|
||||
|
||||
# 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 = 0
|
||||
|
||||
print(f'Inkycal version: v{self._release}')
|
||||
print(f'Selected E-paper display: {self.settings["model"]}')
|
||||
|
||||
while True:
|
||||
print(f"Date: {runtime.format('D MMM YY')} | Time: {runtime.format('HH:mm')}")
|
||||
print('Generating images for all modules...')
|
||||
|
||||
errors = [] # store module numbers in here
|
||||
|
||||
# short info for info-section
|
||||
self.info = f"{runtime.format('D MMM @ HH:mm')} "
|
||||
|
||||
for number in range(1, self._module_number):
|
||||
name = eval(f"self.module_{number}.name")
|
||||
generate_im = f'black,colour=self.module_{number}.generate_image()'
|
||||
save_black = f'black.save("{self.image_folder}/module{number}_black.png", "PNG")'
|
||||
save_colour = f'colour.save("{self.image_folder}/module{number}_colour.png", "PNG")'
|
||||
full_command = generate_im+'\n'+save_black+'\n'+save_colour
|
||||
|
||||
try:
|
||||
exec(full_command)
|
||||
print('OK!')
|
||||
self.info += f"module {number}: OK "
|
||||
except Exception as Error:
|
||||
errors.append(number)
|
||||
print('Error!')
|
||||
print(traceback.format_exc())
|
||||
self.info += f"module {number}: Error! "
|
||||
|
||||
if errors:
|
||||
print('Error/s in modules:',*errors)
|
||||
counter = 0
|
||||
else:
|
||||
counter += 1
|
||||
print('successful')
|
||||
del errors
|
||||
|
||||
# Assemble image from each module - add info section if specified
|
||||
self._assemble()
|
||||
|
||||
# Check if image should be rendered
|
||||
if self.render == True:
|
||||
Display = self.Display
|
||||
|
||||
self._calibration_check()
|
||||
|
||||
if self.supports_colour == True:
|
||||
im_black = Image.open(f"{self.image_folder}/canvas.png")
|
||||
im_colour = Image.open(f"{self.image_folder}/canvas_colour.png")
|
||||
|
||||
# Flip the image by 180° if required
|
||||
if self.settings['orientaton'] == 180:
|
||||
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.settings['orientaton'] == 180:
|
||||
im_black = upside_down(im_black)
|
||||
|
||||
Display.render(im_black)
|
||||
|
||||
print('\ninkycal has been running without any errors for '
|
||||
f"{counter} display updates \n"
|
||||
f'Programm started {runtime.humanize()}')
|
||||
|
||||
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 os.path.exists(im1_path) and os.path.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 os.path.exists(im1_path) and not os.path.exists(im2_path):
|
||||
im1 = Image.open(im1_name).convert('RGBA')
|
||||
|
||||
return im1
|
||||
|
||||
|
||||
def _assemble(self):
|
||||
"""Assembles all sub-images to a single image"""
|
||||
|
||||
# Create 2 blank images with the same resolution as the display
|
||||
width, height = self.Display.get_display_size(self.settings["model"])
|
||||
|
||||
# Since Inkycal runs in vertical mode, switch the height and width
|
||||
width, height = height, width
|
||||
|
||||
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 number in range(1, self._module_number):
|
||||
|
||||
# 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"
|
||||
|
||||
# Check if there is an image for the black band
|
||||
if os.path.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 = [i for i in self.settings['modules'] if \
|
||||
i['position'] == number][0]['config']['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 os.path.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 = [i for i in self.settings['modules'] if \
|
||||
i['position'] == number][0]['config']['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]
|
||||
|
||||
|
||||
# Add info-section if specified --
|
||||
|
||||
# Calculate the max. fontsize for info-section
|
||||
if self.settings['info_section'] == True:
|
||||
info_height = self.settings["info_section_height"]
|
||||
info_width = width
|
||||
font = self.font = ImageFont.truetype(
|
||||
fonts['NotoSansUI-Regular'], size = 14)
|
||||
|
||||
info_x = im_black.size[1] - info_height
|
||||
write(im_black, (0, info_x), (info_width, info_height),
|
||||
self.info, font = font)
|
||||
|
||||
# optimize the image by mapping colours to pure black and white
|
||||
if self.optimize == True:
|
||||
im_black = self._optimize_im(im_black)
|
||||
im_colour = self._optimize_im(im_colour)
|
||||
|
||||
im_black.save(self.image_folder+'/canvas.png', 'PNG')
|
||||
im_colour.save(self.image_folder+'/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)
|
||||
use this command to manually calibrate the display"""
|
||||
|
||||
self.Display.calibrate()
|
||||
|
||||
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
|
||||
else:
|
||||
self._calibration_state = False
|
||||
|
||||
# Work in progress : Adding and removing modules - Please stand by
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('running {0} in standalone/debug mode'.format(filename))
|
||||
|
||||
|
||||
print('running {0} in standalone/debug mode'.format('inkycal main'))
|
||||
|
Loading…
Reference in New Issue
Block a user