From 418422fa5277dbbcca02fb234c01ed1fe875648c Mon Sep 17 00:00:00 2001 From: Ace Date: Tue, 24 Nov 2020 00:40:49 +0100 Subject: [PATCH] Improved documentation --- inkycal/custom/functions.py | 131 +++++++++++++++++++++++++++---- inkycal/display/display.py | 107 +++++++++++++++++++++++-- inkycal/main.py | 59 ++++++++++---- inkycal/modules/inkycal_image.py | 3 + 4 files changed, 260 insertions(+), 40 deletions(-) diff --git a/inkycal/custom/functions.py b/inkycal/custom/functions.py index f3474bf..3eded54 100644 --- a/inkycal/custom/functions.py +++ b/inkycal/custom/functions.py @@ -40,13 +40,41 @@ for path,dirs,files in os.walk(fonts_location): available_fonts = [key for key,values in fonts.items()] def get_fonts(): - """Print all available fonts by name""" + """Print all available fonts by name. + + Searches the /font folder in Inykcal and displays all fonts found in + there. + + Returns: + printed output of all available fonts. To access a fontfile, use the + fonts dictionary to access it. + + >>> fonts['fontname'] + + To use a font, use the following sytax, where fontname is one of the + printed fonts of this function: + + >>> ImageFont.truetype(fonts['fontname'], size = 10) + """ for fonts in available_fonts: print(fonts) def get_system_tz(): - """Get the timezone set by the system""" + """Gets the system-timezone + + Gets the timezone set by the system. + + Returns: + - A timezone if a system timezone was found. + - None if no timezone was found. + + The extracted timezone can be used to show the local time instead of UTC. e.g. + + >>> import arrow + >>> print(arrow.now()) # returns non-timezone-aware time + >>> print(arrow.now(tz=get_system_tz()) # prints timezone aware time. + """ try: local_tz = time.tzname[1] except: @@ -57,8 +85,21 @@ def get_system_tz(): def auto_fontsize(font, max_height): - """Adjust the fontsize to fit 80% of max_height - returns the font object with modified size""" + """Scales a given font to 80% of max_height. + + Gets the height of a font and scales it until 80% of the max_height + is filled. + + + Args: + - font: A PIL Font object. + - max_height: An integer representing the height to adjust the font to + which the given font should be scaled to. + + Returns: + A PIL font object with modified height. + """ + fontsize = font.getsize('hg')[1] while font.getsize('hg')[1] <= (max_height * 0.80): fontsize += 1 @@ -67,12 +108,29 @@ def auto_fontsize(font, max_height): def write(image, xy, box_size, text, font=None, **kwargs): - """Write text on specified image - image = on which image should the text be added? - xy = (x,y) coordinates as tuple -> (x,y) - box_size = size of text-box -> (width,height) - text = string (what to write) - font = which font to use + """Writes text on a image. + + Writes given text at given position on the specified image. + + Args: + - image: The image to draw this text on, usually im_black or im_colour. + - xy: tuple-> (x,y) representing the x and y co-ordinate. + - box_size: tuple -> (width, height) representing the size of the text box. + - text: string, the actual text to add on the image. + - font: A PIL Font object e.g. + ImageFont.truetype(fonts['fontname'], size = 10). + + Args: (optional) + - alignment: alignment of the text, use 'center', 'left', 'right'. + - autofit: bool (True/False). Automatically increases fontsize to fill in + as much of the box-height as possible. + - colour: black by default, do not change as it causes issues with rendering + on e-Paper. + - rotation: Rotate the text with the text-box by a given angle anti-clockwise. + - fill_width: Decimal representing a percentage e.g. 0.9 # 90%. Fill a + maximum of 90% of the size of the full width of text-box. + - fill_height: Decimal representing a percentage e.g. 0.9 # 90%. Fill a + maximum of 90% of the size of the full height of the text-box. """ allowed_kwargs = ['alignment', 'autofit', 'colour', 'rotation', 'fill_width', 'fill_height'] @@ -142,7 +200,19 @@ def write(image, xy, box_size, text, font=None, **kwargs): def text_wrap(text, font=None, max_width = None): - """Split long text (text-wrapping). Returns a list""" + """Splits a very long text into smaller parts + + Splits a long text to smaller lines which can fit in a line with max_width. + Uses a Font object for more accurate calculations. + + Args: + - font: A PIL font object which is used to calculate the size. + - max_width: int-> a width in pixels defining the maximum width before + splitting the text into the next chunk. + + Returns: + A list containing chunked strings of the full text. + """ lines = [] if font.getsize(text)[0] < max_width: lines.append(text) @@ -162,7 +232,20 @@ def text_wrap(text, font=None, max_width = None): def internet_available(): - """check if the internet is available""" + """checks if the internet is available. + + Attempts to connect to google.com with a timeout of 5 seconds to check + if the network can be reached. + + Returns: + - True if connection could be established. + - False if the internet could not be reached. + + Returned output can be used to add a check for internet availability: + + >>> if internet_available() == True: + >>> #...do something that requires internet connectivity + """ try: urlopen('https://google.com',timeout=5) @@ -172,11 +255,25 @@ def internet_available(): def draw_border(image, xy, size, radius=5, thickness=1, shrinkage=(0.1,0.1)): - """Draws a border with round corners at (x,y) - xy = position e.g: (5,10) - size = size of border (width, height), radius: corner radius - thickness = border thickness - shrinkage = shrink and center border by given percentage:(width_%, height_%) + """Draws a border at given coordinates. + + Args: + - image: The image on which the border should be drawn (usually im_black or + im_colour. + + - xy: Tuple representing the top-left corner of the border e.g. (32, 100) + where 32 is the x co-ordinate and 100 is the y-coordinate. + + - size: Size of the border as a tuple -> (width, height). + + - radius: Radius of the corners, where 0 = plain rectangle, 5 = round corners. + + - thickness: Thickness of the border in pixels. + + - shrinkage: A tuple containing decimals presenting a percentage of shrinking + -> (width_shrink_percentage, height_shrink_percentage). + e.g. (0.1, 0.2) ~ shrinks the width of border by 10%, shrinks height of + border by 20% """ colour='black' diff --git a/inkycal/display/display.py b/inkycal/display/display.py index 92e7e1c..ed5d9a5 100644 --- a/inkycal/display/display.py +++ b/inkycal/display/display.py @@ -12,7 +12,15 @@ import glob class Display: """Display class for inkycal - Handles rendering on display""" + + Creates an instance of the driver for the selected E-Paper model and allows + rendering images and calibrating the E-Paper display + + args: + - epaper_model: The name of your E-Paper model. + + + """ def __init__(self, epaper_model): """Load the drivers for this epaper model""" @@ -35,8 +43,41 @@ class Display: raise Exception('SPI could not be found. Please check if SPI is enabled') def render(self, im_black, im_colour = None): - """Render an image on the epaper - im_colour is required for three-colour epapers""" + """Renders an image on the selected E-Paper display. + + Initlializes the E-Paper display, sends image data and executes command + to update the display. + + Args: + - im_black: The image for the black-pixels. Anything in this image that is + black is rendered as black on the display. This is required and ideally + should be a black-white image. + + - im_colour: For E-Paper displays supporting colour, a separate image, + ideally black-white is required for the coloured pixels. Anything that is + black in this image will show up as either red/yellow. + + Rendering an image for black-white E-Paper displays: + + >>> sample_image = PIL.Image.open('path/to/file.png') + >>> display = Display('my_black_white_display') + >>> display.render(sample_image) + + + Rendering black-white on coloured E-Paper displays: + + >>> sample_image = PIL.Image.open('path/to/file.png') + >>> display = Display('my_coloured_display') + >>> display.render(sample_image, sample_image) + + + Rendering coloured image where 2 images are available: + + >>> black_image = PIL.Image.open('path/to/file.png') # black pixels + >>> colour_image = PIL.Image.open('path/to/file.png') # coloured pixels + >>> display = Display('my_coloured_display') + >>> display.render(black_image, colour_image) + """ epaper = self._epaper @@ -65,9 +106,23 @@ class Display: print('Done') def calibrate(self, cycles=3): - """Flush display with single colour to prevent burn-ins (ghosting) - cycles -> int. How many times should each colour be flushed? - recommended cycles = 3""" + """Calibrates the display to retain crisp colours + + Flushes the selected display several times with it's supported colours, + removing any previous effects of ghosting. + + Args: + - cycles: -> int. The number of times to flush the display with it's + supported colours. + + It's recommended to calibrate the display after every 6 display updates + for best results. For black-white only displays, calibration is less + 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 + E-Paper displays. + """ epaper = self._epaper epaper.init() @@ -102,7 +157,21 @@ class Display: @classmethod def get_display_size(cls, model_name): - "returns (width, height) of given display" + """Returns the size of the display as a tuple -> (width, height) + + Looks inside drivers folder for the given model name, then returns it's + size. + + Args: + - model_name: str -> The name of the E-Paper display to get it's size. + + Returns: + (width, height) ->tuple, showing the size of the display + + You can use this function directly without creating the Display class: + + >>> Display.get_display_size('model_name') + """ if not isinstance(model_name, str): print('model_name should be a string') return @@ -110,6 +179,8 @@ class Display: 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') if model_name not in drivers: print('This model name was not found. Please double check your spellings') return @@ -122,6 +193,28 @@ class Display: height = int(line.rstrip().replace(" ", "").split('=')[-1]) return width, height + @classmethod + def get_display_names(cls): + """Prints all supported E-Paper models. + + Fetches all filenames in driver folder and prints them on the console. + + Returns: + Printed version of all supported Displays. + + Use one of the models to intilialize the Display class in order to gain + access to the E-Paper. + + You can use this function directly without creating the Display class: + + >>> Display.get_display_names() + """ + 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") diff --git a/inkycal/main.py b/inkycal/main.py index d12c44d..8704257 100644 --- a/inkycal/main.py +++ b/inkycal/main.py @@ -24,8 +24,9 @@ except ImportError: try: import numpy except ImportError: - print('numpy is not installed! Please install with:') - print('pip3 install numpy') + 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.') logging.basicConfig( level = logging.INFO, #DEBUG > #INFO > #ERROR > #WARNING > #CRITICAL @@ -34,16 +35,31 @@ logging.basicConfig( logger = logging.getLogger('inykcal main') +# TODO: fix issue with non-render mode requiring SPI +# TODO: fix info section not updating after a calibration +# TODO: add function to add/remove third party modules +# TODO: autostart -> supervisor? +# TODO: logging to files + class Inkycal: - """Inkycal main class""" + """Inkycal main class + + Main class of Inkycal, test and run the main Inkycal program. + + 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? + + 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. + """ + def __init__(self, settings_path=None, render=True): - """Initialise Inkycal - settings_path = str -> the full path to your settings.json file - if no path is given, try looking for settings file in /boot folder + """Initialise Inkycal""" - render = bool (True/False) -> show the image on the epaper display? - """ self._release = '2.0.0' # Check if render was set correctly @@ -87,7 +103,6 @@ class Inkycal: # 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'] @@ -152,9 +167,14 @@ class Inkycal: def test(self): - """Inkycal test run - Generates images for each module, one by one and prints OK if no - problems were found.""" + """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"]}') @@ -183,8 +203,12 @@ class Inkycal: 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""" + """Runs main programm in nonstop mode. + + Uses a infinity loop to run Inkycal nonstop. Inkycal generates the image + 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() @@ -411,8 +435,11 @@ class Inkycal: return image def calibrate(self): - """Calibrate the ePaper display to prevent burn-ins (ghosting) - use this command to manually calibrate the display""" + """Calibrate the E-Paper display + + 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. + """ self.Display.calibrate() diff --git a/inkycal/modules/inkycal_image.py b/inkycal/modules/inkycal_image.py index b5f8729..a5eb031 100644 --- a/inkycal/modules/inkycal_image.py +++ b/inkycal/modules/inkycal_image.py @@ -47,6 +47,9 @@ class Inkyimage(inkycal_module): } + # TODO: thorough testing and code cleanup + # TODO: presentation mode (cycle through images in folder) + def __init__(self, config): """Initialize inkycal_rss module"""