Allow usage without display and SPI when setting render->False
Generated images will be available in the images folder
This commit is contained in:
		| @@ -10,6 +10,7 @@ from PIL import Image | ||||
| from inkycal.custom import top_level | ||||
| import glob | ||||
|  | ||||
|  | ||||
| class Display: | ||||
|     """Display class for inkycal | ||||
|  | ||||
| @@ -42,7 +43,7 @@ class Display: | ||||
|         except FileNotFoundError: | ||||
|             raise Exception('SPI could not be found. Please check if SPI is enabled') | ||||
|  | ||||
|   def render(self, im_black, im_colour = None): | ||||
|     def render(self, im_black, im_colour=None): | ||||
|         """Renders an image on the selected E-Paper display. | ||||
|  | ||||
|         Initlializes the E-Paper display, sends image data and executes command | ||||
| @@ -81,23 +82,23 @@ class Display: | ||||
|  | ||||
|         epaper = self._epaper | ||||
|  | ||||
|     if self.supports_colour == False: | ||||
|       print('Initialising..', end = '') | ||||
|         if not self.supports_colour: | ||||
|             print('Initialising..', end='') | ||||
|             epaper.init() | ||||
|       print('Updating display......', end = '') | ||||
|             print('Updating display......', end='') | ||||
|             epaper.display(epaper.getbuffer(im_black)) | ||||
|             print('Done') | ||||
|  | ||||
|     elif self.supports_colour == True: | ||||
|         elif self.supports_colour: | ||||
|             if not im_colour: | ||||
|                 raise Exception('im_colour is required for coloured epaper displays') | ||||
|       print('Initialising..', end = '') | ||||
|             print('Initialising..', end='') | ||||
|             epaper.init() | ||||
|       print('Updating display......', end = '') | ||||
|             print('Updating display......', end='') | ||||
|             epaper.display(epaper.getbuffer(im_black), epaper.getbuffer(im_colour)) | ||||
|             print('Done') | ||||
|  | ||||
|     print('Sending E-Paper to deep sleep...', end = '') | ||||
|         print('Sending E-Paper to deep sleep...', end='') | ||||
|         epaper.sleep() | ||||
|         print('Done') | ||||
|  | ||||
| @@ -116,7 +117,7 @@ class Display: | ||||
|         critical, but not calibrating regularly results in grey-ish text. | ||||
|  | ||||
|         Please note that calibration takes a while to complete. 3 cycles may | ||||
|     take 10 mins on black-white E-Papers while it takes 20 minutes on coloured | ||||
|         take 10 minutes on black-white E-Papers while it takes 20 minutes on coloured | ||||
|         E-Paper displays. | ||||
|         """ | ||||
|  | ||||
| @@ -129,35 +130,34 @@ class Display: | ||||
|         black = Image.new('1', display_size, 'black') | ||||
|  | ||||
|         print('----------Started calibration of ePaper display----------') | ||||
|     if self.supports_colour == True: | ||||
|         if self.supports_colour: | ||||
|             for _ in range(cycles): | ||||
|         print('Calibrating...', end= ' ') | ||||
|         print('black...', end= ' ') | ||||
|                 print('Calibrating...', end=' ') | ||||
|                 print('black...', end=' ') | ||||
|                 epaper.display(epaper.getbuffer(black), epaper.getbuffer(white)) | ||||
|         print('colour...', end = ' ') | ||||
|                 print('colour...', end=' ') | ||||
|                 epaper.display(epaper.getbuffer(white), epaper.getbuffer(black)) | ||||
|                 print('white...') | ||||
|                 epaper.display(epaper.getbuffer(white), epaper.getbuffer(white)) | ||||
|         print(f'Cycle {_+1} of {cycles} complete') | ||||
|                 print(f'Cycle {_ + 1} of {cycles} complete') | ||||
|  | ||||
|     if self.supports_colour == False: | ||||
|         if not self.supports_colour: | ||||
|             for _ in range(cycles): | ||||
|         print('Calibrating...', end= ' ') | ||||
|         print('black...', end = ' ') | ||||
|                 print('Calibrating...', end=' ') | ||||
|                 print('black...', end=' ') | ||||
|                 epaper.display(epaper.getbuffer(black)) | ||||
|                 print('white...') | ||||
|                 epaper.display(epaper.getbuffer(white)), | ||||
|         print(f'Cycle {_+1} of {cycles} complete') | ||||
|                 print(f'Cycle {_ + 1} of {cycles} complete') | ||||
|  | ||||
|             print('-----------Calibration complete----------') | ||||
|             epaper.sleep() | ||||
|  | ||||
|  | ||||
|     @classmethod | ||||
|     def get_display_size(cls, model_name): | ||||
|         """Returns the size of the display as a tuple -> (width, height) | ||||
|  | ||||
|     Looks inside drivers folder for the given model name, then returns it's | ||||
|         Looks inside "drivers" folder for the given model name, then returns it's | ||||
|         size. | ||||
|  | ||||
|         Args: | ||||
| @@ -174,7 +174,7 @@ class Display: | ||||
|             print('model_name should be a string') | ||||
|             return | ||||
|         else: | ||||
|       driver_files = top_level+'/inkycal/display/drivers/*.py' | ||||
|             driver_files = top_level + '/inkycal/display/drivers/*.py' | ||||
|             drivers = glob.glob(driver_files) | ||||
|             drivers = [i.split('/')[-1].split('.')[0] for i in drivers] | ||||
|             drivers.remove('__init__') | ||||
| @@ -183,7 +183,7 @@ class Display: | ||||
|                 print('This model name was not found. Please double check your spellings') | ||||
|                 return | ||||
|             else: | ||||
|         with open(top_level+'/inkycal/display/drivers/'+model_name+'.py') as file: | ||||
|                 with open(top_level + '/inkycal/display/drivers/' + model_name + '.py') as file: | ||||
|                     for line in file: | ||||
|                         if 'EPD_WIDTH=' in line.replace(" ", ""): | ||||
|                             width = int(line.rstrip().replace(" ", "").split('=')[-1]) | ||||
| @@ -207,13 +207,13 @@ class Display: | ||||
|  | ||||
|         >>> Display.get_display_names() | ||||
|         """ | ||||
|     driver_files = top_level+'/inkycal/display/drivers/*.py' | ||||
|         driver_files = top_level + '/inkycal/display/drivers/*.py' | ||||
|         drivers = glob.glob(driver_files) | ||||
|         drivers = [i.split('/')[-1].split('.')[0] for i in drivers] | ||||
|         drivers.remove('__init__') | ||||
|         drivers.remove('epdconfig') | ||||
|         print(*drivers, sep='\n') | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     print("Running Display class in standalone mode") | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| #!/usr/bin/python3 | ||||
| #!python3 | ||||
| # -*- coding: utf-8 -*- | ||||
|  | ||||
| """ | ||||
| @@ -44,7 +44,7 @@ stream_handler.setLevel(logging.ERROR) | ||||
| on_rtd = os.environ.get('READTHEDOCS') == 'True' | ||||
| if on_rtd: | ||||
|     logging.basicConfig( | ||||
|     level = logging.INFO, | ||||
|         level=logging.INFO, | ||||
|         format='%(asctime)s | %(name)s |  %(levelname)s: %(message)s', | ||||
|         datefmt='%d-%m-%Y %H:%M:%S', | ||||
|         handlers=[stream_handler]) | ||||
| @@ -52,7 +52,7 @@ if on_rtd: | ||||
| else: | ||||
|     # Save all logs to a file, which contains more detailed output | ||||
|     logging.basicConfig( | ||||
|     level = logging.INFO, | ||||
|         level=logging.INFO, | ||||
|         format='%(asctime)s | %(name)s |  %(levelname)s: %(message)s', | ||||
|         datefmt='%d-%m-%Y %H:%M:%S', | ||||
|         handlers=[ | ||||
| @@ -73,6 +73,7 @@ logging.getLogger("PIL").setLevel(logging.WARNING) | ||||
| filename = os.path.basename(__file__).split('.py')[0] | ||||
| logger = logging.getLogger(filename) | ||||
|  | ||||
|  | ||||
| # TODO: autostart -> supervisor? | ||||
|  | ||||
| class Inkycal: | ||||
| @@ -122,15 +123,13 @@ class Inkycal: | ||||
|                 print('No settings file found in /boot') | ||||
|                 return | ||||
|  | ||||
|  | ||||
|         # Option to use epaper image optimisation, reduces colours | ||||
|         self.optimize = True | ||||
|  | ||||
|         # Load drivers if image should be rendered | ||||
|         if self.render == True: | ||||
|  | ||||
|             # Init Display class with model in settings file | ||||
|       from inkycal.display import Display | ||||
|             # from inkycal.display import Display | ||||
|             self.Display = Display(settings["model"]) | ||||
|  | ||||
|             # check if colours can be rendered | ||||
| @@ -154,9 +153,9 @@ class Inkycal: | ||||
|                 # 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]))) | ||||
|                     name=module_name, | ||||
|                     width=module['config']['size'][0], | ||||
|                     height=module['config']['size'][1]))) | ||||
|  | ||||
|                 self._module_number += 1 | ||||
|  | ||||
| @@ -169,7 +168,7 @@ class Inkycal: | ||||
|                 print(str(e)) | ||||
|  | ||||
|         # Path to store images | ||||
|     self.image_folder = top_level+'/images' | ||||
|         self.image_folder = top_level + '/images' | ||||
|  | ||||
|         # Give an OK message | ||||
|         print('loaded inkycal') | ||||
| @@ -183,22 +182,21 @@ class Inkycal: | ||||
|  | ||||
|         # 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] | ||||
|         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 | ||||
|         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) | ||||
|         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. | ||||
|  | ||||
| @@ -223,7 +221,7 @@ class Inkycal: | ||||
|             module = eval(f'self.module_{number}') | ||||
|             print(f'generating image(s) for {name}...', end="") | ||||
|             try: | ||||
|         black,colour=module.generate_image() | ||||
|                 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!') | ||||
| @@ -234,7 +232,7 @@ class Inkycal: | ||||
|                 print(traceback.format_exc()) | ||||
|  | ||||
|         if errors: | ||||
|       print('Error/s in modules:',*errors) | ||||
|             print('Error/s in modules:', *errors) | ||||
|         del errors | ||||
|  | ||||
|         self._assemble() | ||||
| @@ -276,7 +274,7 @@ class Inkycal: | ||||
|                 module = eval(f'self.module_{number}') | ||||
|  | ||||
|                 try: | ||||
|           black,colour=module.generate_image() | ||||
|                     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  " | ||||
| @@ -288,7 +286,7 @@ class Inkycal: | ||||
|                     logger.exception(f'Exception in module {number}') | ||||
|  | ||||
|             if errors: | ||||
|         print('error/s in modules:',*errors) | ||||
|                 print('error/s in modules:', *errors) | ||||
|                 counter = 0 | ||||
|             else: | ||||
|                 counter += 1 | ||||
| @@ -340,7 +338,7 @@ class Inkycal: | ||||
|  | ||||
|         im_path = images | ||||
|  | ||||
|     im1_path, im2_path = images+'canvas.png', images+'canvas_colour.png' | ||||
|         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): | ||||
| @@ -356,7 +354,6 @@ class Inkycal: | ||||
|  | ||||
|         return im1 | ||||
|  | ||||
|  | ||||
|     def _assemble(self): | ||||
|         """Assembles all sub-images to a single image""" | ||||
|  | ||||
| @@ -366,8 +363,8 @@ class Inkycal: | ||||
|         # 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') | ||||
|         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 | ||||
| @@ -391,16 +388,16 @@ class Inkycal: | ||||
|                                 i['position'] == number][0]['config']['size'] | ||||
|  | ||||
|                 # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0] - im1_size[0]) /2) | ||||
|                 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) | ||||
|                     y = int((section_size[1] - im1_size[1]) / 2) | ||||
|                 else: | ||||
|           y = im1_cursor + int( (section_size[1]-im1_size[1]) /2) | ||||
|                     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) | ||||
|                 im_black.paste(im1, (x, y), im1) | ||||
|  | ||||
|                 # Shift the y-axis cursor at the beginning of next section | ||||
|                 im1_cursor += section_size[1] | ||||
| @@ -417,21 +414,20 @@ class Inkycal: | ||||
|                                 i['position'] == number][0]['config']['size'] | ||||
|  | ||||
|                 # Calculate coordinates to center the image | ||||
|         x = int( (section_size[0]-im2_size[0]) /2) | ||||
|                 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) | ||||
|                     y = int((section_size[1] - im2_size[1]) / 2) | ||||
|                 else: | ||||
|           y = im2_cursor + int( (section_size[1]-im2_size[1]) /2) | ||||
|                     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) | ||||
|                 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 | ||||
| @@ -439,19 +435,19 @@ class Inkycal: | ||||
|             info_height = self.settings["info_section_height"] | ||||
|             info_width = width | ||||
|             font = self.font = ImageFont.truetype( | ||||
|         fonts['NotoSansUI-Regular'], size = 14) | ||||
|                 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) | ||||
|                   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') | ||||
|         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""" | ||||
| @@ -460,7 +456,7 @@ class Inkycal: | ||||
|         red, green = buffer[:, :, 0], buffer[:, :, 1] | ||||
|  | ||||
|         # grey->black | ||||
|     buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] | ||||
|         buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0, 0, 0] | ||||
|         image = Image.fromarray(buffer) | ||||
|         return image | ||||
|  | ||||
| @@ -485,7 +481,6 @@ class Inkycal: | ||||
|         else: | ||||
|             self._calibration_state = False | ||||
|  | ||||
|  | ||||
|     @classmethod | ||||
|     def add_module(cls, filepath): | ||||
|         """registers a third party module for inkycal. | ||||
| @@ -515,7 +510,7 @@ class Inkycal: | ||||
|               >>> Inkycal.add_module('/full/path/to/the/module/in/inkycal/modules.py') | ||||
|         """ | ||||
|  | ||||
|     module_folder = top_level+'/inkycal/modules' | ||||
|         module_folder = top_level + '/inkycal/modules' | ||||
|  | ||||
|         # Check if module is inside the modules folder | ||||
|         if not module_folder in filepath: | ||||
| @@ -538,9 +533,8 @@ class Inkycal: | ||||
|             raise TypeError("your module doesn't seem to be a correct inkycal module.." | ||||
|                             "Please check your module again.") | ||||
|  | ||||
|  | ||||
|         # Check if filename or classname exists in init of module folder | ||||
|     with open(module_folder+'/__init__.py', mode ='r') as file: | ||||
|         with open(module_folder + '/__init__.py', mode='r') as file: | ||||
|             module_init = file.read().splitlines() | ||||
|  | ||||
|         print('checking module init file..') | ||||
| @@ -558,7 +552,7 @@ class Inkycal: | ||||
|         print('OK!') | ||||
|  | ||||
|         # Check if filename or classname exists in init of inkycal folder | ||||
|     with open(top_level+'/inkycal/__init__.py', mode ='r') as file: | ||||
|         with open(top_level + '/inkycal/__init__.py', mode='r') as file: | ||||
|             inkycal_init = file.read().splitlines() | ||||
|  | ||||
|         print('checking inkycal init file..') | ||||
| @@ -576,17 +570,16 @@ class Inkycal: | ||||
|         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: | ||||
|         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: | ||||
|         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!") | ||||
|  | ||||
|  | ||||
|     @classmethod | ||||
|     def remove_module(cls, filename, remove_file=True): | ||||
|         """unregisters a inkycal module. | ||||
| @@ -611,7 +604,7 @@ class Inkycal: | ||||
|               >>> Inkycal.remove_module('mymodule.py') | ||||
|         """ | ||||
|  | ||||
|     module_folder = top_level+'/inkycal/modules' | ||||
|         module_folder = top_level + '/inkycal/modules' | ||||
|  | ||||
|         # Check if module is inside the modules folder and extract classname | ||||
|         try: | ||||
| @@ -636,28 +629,28 @@ class Inkycal: | ||||
|         filename = filename.split('.py')[0] | ||||
|  | ||||
|         # Create a memory backup of /modules init file | ||||
|     with open(module_folder+'/__init__.py', mode ='r') as file: | ||||
|         with open(module_folder + '/__init__.py', mode='r') as file: | ||||
|             module_init = file.read().splitlines() | ||||
|  | ||||
|         print('removing line from module_init') | ||||
|         # Remove lines that contain classname | ||||
|     with open(module_folder+'/__init__.py', mode ='w') as file: | ||||
|         with open(module_folder + '/__init__.py', mode='w') as file: | ||||
|             for line in module_init: | ||||
|                 if not classname in line: | ||||
|           file.write(line+'\n') | ||||
|                     file.write(line + '\n') | ||||
|                 else: | ||||
|                     print('found, removing') | ||||
|  | ||||
|         # Create a memory backup of inkycal init file | ||||
|     with open(f"{top_level}/inkycal/__init__.py", mode ='r') as file: | ||||
|         with open(f"{top_level}/inkycal/__init__.py", mode='r') as file: | ||||
|             inkycal_init = file.read().splitlines() | ||||
|  | ||||
|         print('removing line from inkycal init') | ||||
|         # Remove lines that contain classname | ||||
|     with open(f"{top_level}/inkycal/__init__.py", mode ='w') as file: | ||||
|         with open(f"{top_level}/inkycal/__init__.py", mode='w') as file: | ||||
|             for line in inkycal_init: | ||||
|                 if not filename in line: | ||||
|           file.write(line+'\n') | ||||
|                     file.write(line + '\n') | ||||
|                 else: | ||||
|                     print('found, removing') | ||||
|  | ||||
| @@ -669,5 +662,6 @@ class Inkycal: | ||||
|  | ||||
|         print(f"Your module '{filename}' with class '{classname}' was removed.") | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     print(f'running inkycal main in standalone/debug mode') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user