-
+
{model}
- substituted by the E-Paper model name.
+ {width}
- substituted by the panel width.
+ {height}
- substituted by the panel width.
+
diff --git a/Changelog.md b/Changelog.md index 1d5fa76..607d61b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,11 +4,30 @@ The order is from latest to oldest and structured in the following way: * Version name with date of publishing * Sections with either 'added', 'fixed', 'updated' and 'changed' -## [1.7] Mid December 2019 (date not confirmed yet) +## [1.7.1] Mid January 2020 + +### Added +* Added support for 4.2", 5.83", 7.5" (v2) E-Paper display +* Added driver files for above mentioned E-Paper displays + +### Changed +* Slight changes in naming of generated images +* Slight changes in importing module names (now using dynamic imports) +* Changed driver files for all E-Papers with the latest ones from waveshare (v4) +* Slightly changed the way modules are executed + +### Removed +* Removed option for selecting colour from settings file + +### Fixed +* Fixed a problem where the calibration function would only update half the display on the 7.5" black-white E-Paper +* Implemented a possible bugfix for 'begin must be before end' error. + +## [1.7] Mid December 2019 ### Added * Added support for sections (top-,middle-,and bottom section) -* Added support for weather forecasts. +* Added support for weather forecasts. * Added support for moon phase * Added support for events in Calendar module * Added support for coloured negative temperature @@ -16,31 +35,37 @@ The order is from latest to oldest and structured in the following way: * Added support for wind direction in weather module * Added support for decimal places in weather module * Added extra customisation options (see configuration file) +* Added support for recurring events +* Added forecasts in weather module +* Added info about moon phase in weather module +* Added info about sunrise and sunset time in weather module +* Added support for colour-changing temperature (for coloured E-Paper displays, the temperature will red if it drops below 0°Celcius) +* Added support for decimal places in weather section (wind speed, temperature) +* Added beaufort scale to show windspeed +* Added option to show wind direction with an arrow +* Added new event and today icon in Calendar module +* Added sections showing upcoming events within Calendar module +* Added configuration file for additional configuration options +* Added new fonts with better readability +* Added support to manually change fontsize in each module +* Added more design customisation (text colour, background colours etc.) ### Changed -* Refactoring of software. Split software into several smaller modules -* Re-arranged weather section layout -* Icons (today, events) are generated on demand -* Merged calibration files into inkycal_drivers -* Changed layout of Agenda module -* Changed icons for marking today on Calendar module -* Added more options in function 'write_text' -* Text does not have any background colour anymore (transparent) -* Optimised calibration function for faster calibration, especially for coloured E-Papers -* Changed settings file +* Changed folder structure (Full software refactoring) +* Split main file into smaller modules, each with a specific task +* Changed layout of E-Paper (top_section, middle_section, bottom_section) +* Changed settings file, installer and web-UI +* Black and white E-Papers now use dithering option to map pixels to either black and white ### Removed -* Removed last-updated feature -* Removed all icons stored as images -* Removed calibration file (calibration.py) - +* Removed non-readable fonts +* Removed all icons in form of image files. The new icons are generated with PIL on the spot +* Removed option to reduce colours for black and white E-Papers ### Fixed -* Fixed a few bugs related to the ics library -* Fine-tuned image pre-processing (mapping pixels to specific colours) -* Fixed a problem where RSS feeds would not display more than one post -* Fixed a problem where certain weather icons would not be shown - +* Fixed problem with RSS feeds not displaying more than one feed +* Fixed image rendering +* Fixed problems when setting the weekstart to Sunday ## [1.6] Mid May 2019 diff --git a/Installer.sh b/Installer.sh index 6cfc1a5..e09477d 100644 --- a/Installer.sh +++ b/Installer.sh @@ -1,6 +1,6 @@ #!/bin/bash # E-Paper-Calendar software installer for Raspberry Pi running Debian 10 (a.k.a. Buster) with Desktop -# Version: 1.7 (Early Dec 2019) +# Version: 1.7.2 (Mid Feb 2020) echo -e "\e[1mPlease select an option from below:" echo -e "\e[97mEnter \e[91m[1]\e[97m to update Inky-Calendar software" #Option 1 : UPDATE @@ -90,17 +90,6 @@ if [ "$option" = 1 ] || [ "$option" = 2 ]; then # This happens when installing o # Create symlinks of settings and configuration file ln -s /home/"$USER"/Inky-Calendar/settings/settings.py /home/"$USER"/Inky-Calendar/modules/ ln -s /home/"$USER"/Inky-Calendar/settings/configuration.py /home/"$USER"/Inky-Calendar/modules/ - - # add a short info - cat > /home/pi/Inky-Calendar/Info.txt << EOF -This document contains a short info of the Inky-Calendar software version - -Version: 1.7 -Installer version: 1.7 (Mid December 2019) -settings file: /home/$USER/Inky-Calendar/settings/settings.py -If the time was set correctly, you installed this software on: -$(date) -EOF echo "" echo -e "\e[97mDo you want the software to start automatically at boot?" diff --git a/modules/inkycal.py b/modules/inkycal.py index 896f0bc..b265503 100644 --- a/modules/inkycal.py +++ b/modules/inkycal.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- """ -v1.7.1 +v1.7.2 Main file of Inky-Calendar software. Creates dynamic images for each section, assembles them and sends it to the E-Paper @@ -17,17 +17,35 @@ import gc """Perepare for execution of main programm""" calibration_countdown = 'initial' skip_calibration = False +upside_down = False + image_cleanup() -top_section_module = importlib.import_module(top_section) -middle_section_module = importlib.import_module(middle_section) -bottom_section_module = importlib.import_module(bottom_section) +try: + top_section_module = importlib.import_module(top_section) +except ValueError: + print('Something went wrong while importing the top-section module:', top_section) + pass + +try: + middle_section_module = importlib.import_module(middle_section) +except ValueError: + print('Something went wrong while importing the middle_section module', middle_section) + pass + +try: + bottom_section_module = importlib.import_module(bottom_section) +except ValueError: + print('Something went wrong while importing the bottom_section module', bottom_section) + pass """Check time and calibrate display if time """ while True: now = arrow.now(tz=get_tz()) for _ in range(1): image = Image.new('RGB', (display_width, display_height), background_colour) + if three_colour_support == True: + image_col = Image.new('RGB', (display_width, display_height), 'white') """------------------Add short info------------------""" print('Current Date: {0} \nCurrent Time: {1}'.format(now.format( @@ -53,36 +71,62 @@ while True: 'displays causes ghosting') - """----------------Generating and assembling images------""" + """----------------------top-section-image-----------------------------""" try: top_section_module.main() top_section_image = Image.open(image_path + top_section+'.png') image.paste(top_section_image, (0, 0)) - print('Done') + + if three_colour_support == True: + top_section_image_col = Image.open(image_path + top_section+'_col.png') + image_col.paste(top_section_image_col, (0, 0)) + except Exception as error: print(error) pass + """----------------------middle-section-image---------------------------""" try: middle_section_module.main() middle_section_image = Image.open(image_path + middle_section+'.png') image.paste(middle_section_image, (0, middle_section_offset)) - print('Done') + + if three_colour_support == True: + middle_section_image_col = Image.open(image_path + middle_section+'_col.png') + image_col.paste(middle_section_image_col, (0, middle_section_offset)) + except Exception as error: print(error) pass + + """----------------------bottom-section-image---------------------------""" try: bottom_section_module.main() bottom_section_image = Image.open(image_path + bottom_section+'.png') image.paste(bottom_section_image, (0, bottom_section_offset)) - print('Done') + + if three_colour_support == True: + bottom_section_image_col = Image.open(image_path + bottom_section+'_col.png') + image_col.paste(bottom_section_image_col, (0, bottom_section_offset)) + except Exception as error: print(error) pass + """---------------------------------------------------------------------""" + if upside_down == True: + image = image.rotate(180, expand=True) + if three_colour_support == True: + image_col = image_col.rotate(180, expand=True) + + image = optimise_colours(image) image.save(image_path + 'canvas.png') + if three_colour_support == True: + image_col = optimise_colours(image_col) + image_col.save(image_path+'canvas_col.png') + """---------Refreshing E-Paper with newly created image-----------""" epaper = driver.EPD() print('Initialising E-Paper...', end = '') @@ -91,12 +135,11 @@ while True: if three_colour_support == True: print('Sending image data and refreshing display...', end='') - black_im, red_im = split_colours(image) - epaper.display(epaper.getbuffer(black_im), epaper.getbuffer(red_im)) + epaper.display(epaper.getbuffer(image), epaper.getbuffer(image_col)) print('Done') else: print('Sending image data and refreshing display...', end='') - epaper.display(epaper.getbuffer(image.convert('1', dither=True))) + epaper.display(epaper.getbuffer(image)) print('Done') print('Sending E-Paper to deep sleep...', end = '') diff --git a/modules/inkycal_agenda.py b/modules/inkycal_agenda.py index e362b3d..e569549 100644 --- a/modules/inkycal_agenda.py +++ b/modules/inkycal_agenda.py @@ -18,7 +18,7 @@ border_top = int(middle_section_height * 0.02) border_left = int(middle_section_width * 0.02) """Choose font optimised for the agenda section""" -font = ImageFont.truetype(NotoSans+'Medium.ttf', agenda_font_size) +font = ImageFont.truetype(NotoSans+'Medium.ttf', agenda_fontsize) line_height = int(font.getsize('hg')[1] * 1.2) + 1 line_width = int(middle_section_width - (border_left*2)) @@ -33,10 +33,7 @@ event_col_start = time_col_start + time_col_width """Find max number of lines that can fit in the middle section and allocate a position for each line""" -if bottom_section: - max_lines = int((middle_section_height - border_top*2) // line_height) -else: - max_lines = int(middle_section_height+bottom_section_height - +max_lines = int(middle_section_height+bottom_section_height - (border_top * 2))// line_height line_pos = [(border_left, int(top_section_height + border_top + line * line_height)) @@ -104,8 +101,14 @@ def generate_image(): agenda_events[events]['date_str'], line_pos[events], font = font) previous_date = agenda_events[events]['date'] - draw.line((date_col_start, line_pos[events][1], - line_width,line_pos[events][1]), fill = 'red' if three_colour_support == True else 'black') + + if three_colour_support == True: + draw_col.line((date_col_start, line_pos[events][1], + line_width,line_pos[events][1]), fill = 'black') + else: + draw.line((date_col_start, line_pos[events][1], + line_width,line_pos[events][1]), fill = 'black') + elif agenda_events[events]['type'] == 'timed_event': write_text(time_col_width, line_height, agenda_events[events]['time'], @@ -123,12 +126,13 @@ def generate_image(): (event_col_start, line_pos[events][1]), alignment = 'left', font = font) """Crop the image to show only the middle section""" - if bottom_section: - agenda_image = crop_image(image, 'middle_section') - else: - agenda_image = image.crop((0,middle_section_offset,display_width, display_height)) - + agenda_image = image.crop((0,middle_section_offset,display_width, display_height)) agenda_image.save(image_path+'inkycal_agenda.png') + + if three_colour_support == True: + agenda_image_col = image_col.crop((0,middle_section_offset,display_width, display_height)) + agenda_image_col.save(image_path+'inkycal_agenda_col.png') + print('Done') except Exception as e: @@ -136,8 +140,16 @@ def generate_image(): print('Failed!') print('Error in Agenda module!') print('Reason: ',e) + + clear_image('middle_section') + write_text(middle_section_width, middle_section_height, str(e), + (0, middle_section_offset), font = font) + calendar_image = crop_image(image, 'middle_section') + calendar_image.save(image_path+'inkycal_agenda.png') pass + + def main(): generate_image() diff --git a/modules/inkycal_calendar.py b/modules/inkycal_calendar.py index ce5284c..0218d55 100644 --- a/modules/inkycal_calendar.py +++ b/modules/inkycal_calendar.py @@ -16,7 +16,7 @@ at_in_your_language = 'at' event_icon = 'square' # dot #square style = "DD MMM" -font = ImageFont.truetype(NotoSans+'.ttf', calendar_font_size) +font = ImageFont.truetype(NotoSans+'.ttf', calendar_fontsize) space_between_lines = 0 if show_events == True: @@ -98,7 +98,7 @@ def generate_image(): """Add the numbers on the correct positions""" for i in range(len(calendar_flat)): - if calendar_flat[i] != 0: + if calendar_flat[i] not in (0, int(now.day)): write_text(icon_width, icon_height, str(calendar_flat[i]), grid[i]) """Draw a red/black circle with the current day of month in white""" @@ -110,11 +110,13 @@ def generate_image(): x_text = int((icon_width / 2) - (text_width / 2)) y_text = int((icon_height / 2) - (text_height / 1.7)) ImageDraw.Draw(icon).ellipse((x_circle-radius, y_circle-radius, - x_circle+radius, y_circle+radius), fill= 'red' if - three_colour_support == True else 'black', outline=None) + x_circle+radius, y_circle+radius), fill= 'black', outline=None) ImageDraw.Draw(icon).text((x_text, y_text), str(now.day), fill='white', font=bold) - image.paste(icon, current_day_pos, icon) + if three_colour_support == True: + image_col.paste(icon, current_day_pos, icon) + else: + image.paste(icon, current_day_pos, icon) """Create some reference points for the current month""" days_current_month = calendar.monthrange(now.year, now.month)[1] @@ -152,38 +154,47 @@ def generate_image(): for days in days_with_events: draw_square((int(grid[calendar_flat.index(days)][0]+center_x), int(grid[calendar_flat.index(days)][1] + center_y )), - 8, square_size , square_size) + 8, square_size , square_size, colour='black') """Add a small section showing events of today and tomorrow""" - event_list = ['{0} {1} {2} : {3}'.format(today_in_your_language, - at_in_your_language, event.begin.format('HH:mm' if hours == 24 else - 'hh:mm'), event.name) for event in calendar_events if event.begin.day - == now.day and now < event.end] - - event_list += ['{0} {1} {2} : {3}'.format(tomorrow_in_your_language, - at_in_your_language, event.begin.format('HH:mm' if hours == 24 else - 'hh:mm'), event.name) for event in calendar_events if event.begin.day - == now.replace(days=1).day] - + event_list = [] after_two_days = now.replace(days=2).floor('day') - event_list += ['{0} {1} {2} : {3}'.format(event.begin.format('D MMM'), - at_in_your_language, event.begin.format('HH:mm' if hours == 24 else - 'hh:mm'), event.name) for event in upcoming_events if event.end > - after_two_days] + for event in calendar_events: + if event.begin.day == now.day and now < event.end: + if event.all_day: + event_list.append('{}: {}'.format(today_in_your_language, event.name)) + else: + event_list.append('{0} {1} {2} : {3}'.format(today_in_your_language, + at_in_your_language, event.begin.format('HH:mm' if hours == '24' else + 'hh:mm a'), event.name)) + + elif event.begin.day == now.replace(days=1).day: + if event.all_day: + event_list.append('{}: {}'.format(tomorrow_in_your_language, event.name)) + else: + event_list.append('{0} {1} {2} : {3}'.format(tomorrow_in_your_language, + at_in_your_language, event.begin.format('HH:mm' if hours == '24' else + 'hh:mm a'), event.name)) + + elif event.begin > after_two_days: + if event.all_day: + event_list.append('{}: {}'.format(event.begin.format('D MMM'), event.name)) + else: + event_list.append('{0} {1} {2} : {3}'.format(event.begin.format('D MMM'), + at_in_your_language, event.begin.format('HH:mm' if hours == '24' else + 'hh:mm a'), event.name)) del event_list[max_event_lines:] if event_list: for lines in event_list: write_text(main_area_width, int(events_height/max_event_lines), lines, - event_lines[event_list.index(lines)], alignment='left', - fill_height = 0.7) + event_lines[event_list.index(lines)], font=font, alignment='left') else: write_text(main_area_width, int(events_height/max_event_lines), - 'No upcoming events.', event_lines[0], alignment='left', - fill_height = 0.7) + 'No upcoming events.', event_lines[0], font=font, alignment='left') """Set print_events_to True to print all events in this month""" style = 'DD MMM YY HH:mm' @@ -197,6 +208,10 @@ def generate_image(): calendar_image = crop_image(image, 'middle_section') calendar_image.save(image_path+'inkycal_calendar.png') + if three_colour_support == True: + calendar_image_col = crop_image(image_col, 'middle_section') + calendar_image_col.save(image_path+'inkycal_calendar_col.png') + print('Done') except Exception as e: @@ -204,6 +219,11 @@ def generate_image(): print('Failed!') print('Error in Calendar module!') print('Reason: ',e) + clear_image('middle_section') + write_text(middle_section_width, middle_section_height, str(e), + (0, middle_section_offset), font = font) + calendar_image = crop_image(image, 'middle_section') + calendar_image.save(image_path+'inkycal_calendar.png') pass def main(): diff --git a/modules/inkycal_icalendar.py b/modules/inkycal_icalendar.py index c61b248..421c174 100644 --- a/modules/inkycal_icalendar.py +++ b/modules/inkycal_icalendar.py @@ -74,6 +74,7 @@ def fetch_events(): for events in upcoming_events: if events.all_day and events.duration.days > 1: events.end = events.end.replace(days=-2) + events.make_all_day() if not events.all_day: events.end = events.end.to(timezone) diff --git a/modules/inkycal_image.py b/modules/inkycal_image.py index 209afb0..6ee4ab8 100644 --- a/modules/inkycal_image.py +++ b/modules/inkycal_image.py @@ -2,82 +2,178 @@ # -*- coding: utf-8 -*- """ Experimental image module for Inky-Calendar software -Displays an image on the E-Paper. Currently only supports black and white +Displays an image on the E-Paper. Work in progress! Copyright by aceisace """ from __future__ import print_function -from PIL import Image from configuration import * -import os +from os import path +from PIL import ImageOps +import requests +import numpy -import inkycal_drivers as drivers +"""----------------------------------------------------------------""" +#path = 'https://github.com/aceisace/Inky-Calendar/raw/master/Gallery/Inky-Calendar-logo.png' +#path ='/home/pi/Inky-Calendar/images/canvas.png' +path = inkycal_image_path +path_body = inkycal_image_path_body +mode = 'auto' # 'horizontal' # 'vertical' # 'auto' +upside_down = True # Flip image by 180 deg (upside-down) +alignment = 'center' # top_center, top_left, center_left, bottom_right etc. +colours = 'bwr' # bwr # bwy # bw +render = True # show image on E-Paper? +"""----------------------------------------------------------------""" -display = drivers.EPD() +# First determine dimensions +if mode == 'horizontal': + display_width, display_height == display_height, display_width -# Where is the image? -path = '/home/pi//Desktop/test.JPG' +if mode == 'vertical': + pass -class inkycal_image: - - def __init__(self, path): - self.image = Image.open(path) - self.im_width = self.image.width - self.im_height = self.image.height - - def check_mode(self): - if self.image.mode != 'RGB' or 'L' or '1': - print('Image mode not supported, converting') - self.image = self.image.convert('RGB') - - def preview(self): - self.image.save(path+'temp.png') - os.system("gpicview "+path+'temp.png') - os.system('rm '+path+'temp.png') - - - def check_size(self, alignment = 'middle', padding_colour='white'): - if display_height < self.im_height or display_width < self.im_width: - print('Image too large for the display, cropping image') - if alignment == 'middle' or None: - x1 = int((self.im_width - display_width) / 2) - y1 = int((self.im_height - display_height) / 2) - x2,y2 = x1+display_width, y1+display_height - self.image = self.image.crop((x1,y1,x2,y2)) - - if alignment != 'middle' or None: - print('Sorry, this feature has not been implemented yet') - raise NotImplementedError - - elif display_height > self.im_height and display_width > self.im_width: - print('Image smaller than display, shifting image to center') - x = int( (display_width - self.im_width) /2) - y = int( (display_height - self.im_height) /2) - canvas = Image.new('RGB', (display_width, display_height), color=padding_colour) - canvas.paste(self.image, (x,y)) - self.image = canvas +# .. Then substitute possibly parameterized path +# TODO Get (assigned) panel dimensions instead of display dimensions +path = path.replace('{model}', model).replace('{width}',str(display_width)).replace('{height}',str(display_height)) +"""Try to open the image if it exists and is an image file""" +try: + if 'http' in path: + if path_body is None: + # Plain GET + im = Image.open(requests.get(path, stream=True).raw) else: - print('Image file exact. no further action required') + # POST request, passing path_body in the body + im = Image.open(requests.post(path, json=path_body, stream=True).raw) + else: + im = Image.open(path) +except FileNotFoundError: + print('Your file could not be found. Please check the path to your file.') + raise +except OSError: + print('Please check if the path points to an image file.') + raise - def auto_flip(self): - if self.im_height < self.im_width: - print('rotating image') - self.image = self.image.rotate(270, expand=True) - self.im_width = self.image.width - self.im_height = self.image.height - - - def to_mono(self): - self.image = self.image.convert('1', dither=True) +"""Turn image upside-down if specified""" +if upside_down == True: + im.rotate(180, expand = True) - def prepare_image(self, alignment='middle'): - self.check_mode() - self.auto_flip() - self.check_size(alignment = alignment) - self.to_mono() +if mode == 'auto': + if (im.width > im.height) and (display_width < display_height): + print('display vertical, image horizontal -> flipping image') + im = im.rotate(90, expand=True) + if (im.width < im.height) and (display_width > display_height): + print('display horizontal, image vertical -> flipping image') + im = im.rotate(90, expand=True) - return self.image +def fit_width(image, width): + """Resize an image to desired width""" + print('resizing width from', image.width, 'to', end = ' ') + wpercent = (display_width/float(image.width)) + hsize = int((float(image.height)*float(wpercent))) + img = image.resize((width, hsize), Image.ANTIALIAS) + print(img.width) + return img -#single line command: -display.show_image(inkycal_image(path).prepare_image(), reduce_colours=False) - +def fit_height(image, height): + """Resize an image to desired height""" + print('resizing height from', image.height, 'to', end = ' ') + hpercent = (height / float(image.height)) + wsize = int(float(image.width) * float(hpercent)) + img = image.resize((wsize, height), Image.ANTIALIAS) + print(img.height) + return img + +if im.width > display_width: + im = fit_width(im, display_width) +if im.height > display_height: + im = fit_height(im, display_height) + +if alignment == 'center': + x,y = int((display_width-im.width)/2), int((display_height-im.height)/2) +elif alignment == 'center_right': + x, y = display_width-im.width, int((display_height-im.height)/2) +elif alignment == 'center_left': + x, y = 0, int((display_height-im.height)/2) + +elif alignment == 'top_center': + x, y = int((display_width-im.width)/2), 0 +elif alignment == 'top_right': + x, y = display_width-im.width, 0 +elif alignment == 'top_left': + x, y = 0, 0 + +elif alignment == 'bottom_center': + x, y = int((display_width-im.width)/2), display_height-im.height +elif alignment == 'bottom_right': + x, y = display_width-im.width, display_height-im.height +elif alignment == 'bottom_left': + x, y = display_width-im.width, display_height-im.height + +if len(im.getbands()) == 4: + print('removing transparency') + bg = Image.new('RGBA', (im.width, im.height), 'white') + im = Image.alpha_composite(bg, im) + +image.paste(im, (x,y)) +im = image + +if colours == 'bw': + """For black-white images, use monochrome dithering""" + black = im.convert('1', dither=True) +elif colours == 'bwr': + """For black-white-red images, create corresponding palette""" + pal = [255,255,255, 0,0,0, 255,0,0, 255,255,255] +elif colours == 'bwy': + """For black-white-yellow images, create corresponding palette""" + pal = [255,255,255, 0,0,0, 255,255,0, 255,255,255] + + +"""Map each pixel of the opened image to the Palette""" +if colours != 'bw': + palette_im = Image.new('P', (3,1)) + palette_im.putpalette(pal * 64) + quantized_im = im.quantize(palette=palette_im) + quantized_im.convert('RGB') + + """Create buffer for coloured pixels""" + buffer1 = numpy.array(quantized_im.convert('RGB')) + r1,g1,b1 = buffer1[:, :, 0], buffer1[:, :, 1], buffer1[:, :, 2] + + """Create buffer for black pixels""" + buffer2 = numpy.array(quantized_im.convert('RGB')) + r2,g2,b2 = buffer2[:, :, 0], buffer2[:, :, 1], buffer2[:, :, 2] + + if colours == 'bwr': + """Create image for only red pixels""" + buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white + buffer2[numpy.logical_and(r2 == 255, b2 == 0)] = [0,0,0] #red->black + colour = Image.fromarray(buffer2) + """Create image for only black pixels""" + buffer1[numpy.logical_and(r1 == 255, b1 == 0)] = [255,255,255] + black = Image.fromarray(buffer1) + + if colours == 'bwy': + """Create image for only yellow pixels""" + buffer2[numpy.logical_and(r2 == 0, b2 == 0)] = [255,255,255] # black->white + buffer2[numpy.logical_and(g2 == 255, b2 == 0)] = [0,0,0] #yellow -> black + colour = Image.fromarray(buffer2) + """Create image for only black pixels""" + buffer1[numpy.logical_and(g1 == 255, b1 == 0)] = [255,255,255] + black = Image.fromarray(buffer1) + +if render == True: + epaper = driver.EPD() + print('Initialising E-Paper...', end = '') + epaper.init() + print('Done') + + print('Sending image data and refreshing display...', end='') + if three_colour_support == True: + epaper.display(epaper.getbuffer(black), epaper.getbuffer(colour)) + else: + epaper.display(epaper.getbuffer(black)) + print('Done') + + print('Sending E-Paper to deep sleep...', end = '') + epaper.sleep() +print('Done') diff --git a/modules/inkycal_rss.py b/modules/inkycal_rss.py index 8c4ce4b..0293258 100644 --- a/modules/inkycal_rss.py +++ b/modules/inkycal_rss.py @@ -14,7 +14,7 @@ border_top = int(bottom_section_height * 0.05) border_left = int(bottom_section_width * 0.02) """Choose font optimised for the weather section""" -font = ImageFont.truetype(NotoSans+'.ttf', rss_font_size) +font = ImageFont.truetype(NotoSans+'.ttf', rss_fontsize) space_between_lines = 1 line_height = font.getsize('hg')[1] + space_between_lines line_width = bottom_section_width - (border_left*2) @@ -69,6 +69,11 @@ def generate_image(): rss_image = crop_image(image, 'bottom_section') rss_image.save(image_path+'inkycal_rss.png') + + if three_colour_support == True: + rss_image_col = crop_image(image_col, 'bottom_section') + rss_image_col.save(image_path+'inkycal_rss_col.png') + print('Done') except Exception as e: @@ -76,8 +81,14 @@ def generate_image(): print('Failed!') print('Error in RSS module!') print('Reason: ',e) + clear_image('bottom_section') + write_text(bottom_section_width, bottom_section_height, str(e), + (0, bottom_section_offset), font = font) + rss = crop_image(image, 'bottom_section') + rss.save(image_path+'inkycal_rss.png') pass + def main(): generate_image() diff --git a/modules/inkycal_weather.py b/modules/inkycal_weather.py index 294e8e4..6e361cf 100644 --- a/modules/inkycal_weather.py +++ b/modules/inkycal_weather.py @@ -120,7 +120,7 @@ def to_units(kelvin): ndigits = decimal_places_temperature) if units == 'metric': conversion = str(degrees_celsius) + '°C' - + if units == 'imperial': conversion = str(fahrenheit) + 'F' @@ -171,7 +171,7 @@ def generate_image(): forecast = owm.three_hours_forecast(location) """Round the hour to the nearest multiple of 3""" - now = arrow.now(tz=get_tz()) + now = arrow.utcnow() if (now.hour % 3) != 0: hour_gap = 3 - (now.hour % 3) else: @@ -258,9 +258,10 @@ def generate_image(): write_text(icon_small, icon_small, '\uf0b1', windspeed_icon_now_pos, font = w_font, fill_height = 0.9, rotation = -wind_degrees) - write_text(coloumn_width-icon_small, row_height, - temperature_now, temperature_now_pos, font = font, colour = - red_temp(temperature_now)) + write_text(coloumn_width-icon_small, row_height, temperature_now, + temperature_now_pos, font = font, colour= red_temp(temperature_now)) + + write_text(coloumn_width-icon_small, row_height, humidity_now+'%', humidity_now_pos, font = font) write_text(coloumn_width-icon_small, row_height, wind, @@ -326,24 +327,31 @@ def generate_image(): draw.line((coloumn5, line_start_y, coloumn5, line_end_y), fill='black') draw.line((coloumn6, line_start_y, coloumn6, line_end_y), fill='black') draw.line((coloumn7, line_start_y, coloumn7, line_end_y), fill='black') - draw.line((0, top_section_height-border_top, top_section_width- - border_left, top_section_height-border_top), - fill='red' if three_colour_support == 'True' else 'black' , width=3) - weather_image = crop_image(image, 'top_section') + if three_colour_support == True: + draw_col.line((0, top_section_height-border_top, top_section_width- + border_left, top_section_height-border_top), fill='black', width=3) + else: + draw.line((0, top_section_height-border_top, top_section_width- + border_left, top_section_height-border_top), fill='black', width=3) + + weather_image = crop_image(image, 'top_section') weather_image.save(image_path+'inkycal_weather.png') + + if three_colour_support == True: + weather_image_col = crop_image(image_col, 'top_section') + weather_image_col.save(image_path+'inkycal_weather_col.png') + print('Done') except Exception as e: - """If no response was received from the openweathermap - api server, add the cloud with question mark""" - print('__________OWM-ERROR!__________') + """If something went wrong, print a Error message on the Terminal""" + print('Failed!') + print('Error in weather module!') print('Reason: ',e) - write_text(icon_medium, icon_medium, '\uf07b', weather_icon_now_pos, - font = w_font, fill_height = 1.0) - message = 'No internet connectivity or API timeout' - write_text(coloumn_width*6, row_height, message, humidity_icon_now_pos, - font = font) + clear_image('top_section') + write_text(top_section_width, top_section_height, str(e), + (0, 0), font = font) weather_image = crop_image(image, 'top_section') weather_image.save(image_path+'inkycal_weather.png') pass diff --git a/release.txt b/release.txt new file mode 100644 index 0000000..3b34d22 --- /dev/null +++ b/release.txt @@ -0,0 +1 @@ +v1.7.2 diff --git a/settings/configuration.py b/settings/configuration.py index 357ef13..361afe2 100644 --- a/settings/configuration.py +++ b/settings/configuration.py @@ -16,6 +16,7 @@ from pytz import timezone import os from glob import glob import importlib +import subprocess as subp """Set the image background colour and text colour""" background_colour = 'white' @@ -34,10 +35,23 @@ else: """Create 3 sections of the display, based on percentage""" top_section_width = middle_section_width = bottom_section_width = display_width -top_section_height = int(display_height*0.11) -middle_section_height = int(display_height*0.65) -bottom_section_height = int(display_height - middle_section_height - - top_section_height) +if top_section and bottom_section: + top_section_height = int(display_height*0.11) + bottom_section_height = int(display_height*0.24) + +elif top_section and not bottom_section: + top_section_height = int(display_height*0.11) + bottom_section_height = 0 + +elif bottom_section and not top_section: + top_section_height = 0 + bottom_section_height = int(display_height*0.24) + +elif not top_section and not bottom_section: + top_section_height = bottom_section_height = 0 + +middle_section_height = int(display_height - top_section_height - + bottom_section_height) """Find out the y-axis position of each section""" top_section_offset = 0 @@ -60,48 +74,52 @@ NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-' NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed' weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf' -"""Fonts sizes""" -default_font_size = 18 -agenda_font_size = 14 -calendar_font_size = 16 -rss_font_size = 14 -weather_font_size = 12 +"""Fontsizes""" +default_fontsize = 18 +agenda_fontsize = 14 +calendar_fontsize = 14 +rss_fontsize = 14 +weather_fontsize = 12 """Automatically select correct fonts to support set language""" if language in ['ja','zh','zh_tw','ko']: - default = ImageFont.truetype(NotoSansCJK+'Regular.otf', default_font_size) - semi = ImageFont.truetype(NotoSansCJK+'Medium.otf', default_font_size) - bold = ImageFont.truetype(NotoSansCJK+'Bold.otf', default_font_size) + default = ImageFont.truetype(NotoSansCJK+'Regular.otf', default_fontsize) + semi = ImageFont.truetype(NotoSansCJK+'Medium.otf', default_fontsize) + bold = ImageFont.truetype(NotoSansCJK+'Bold.otf', default_fontsize) else: - default = ImageFont.truetype(NotoSans+'.ttf', default_font_size) - semi = ImageFont.truetype(NotoSans+'Medium.ttf', default_font_size) - bold = ImageFont.truetype(NotoSans+'SemiBold.ttf', default_font_size) + default = ImageFont.truetype(NotoSans+'.ttf', default_fontsize) + semi = ImageFont.truetype(NotoSans+'Medium.ttf', default_fontsize) + bold = ImageFont.truetype(NotoSans+'SemiBold.ttf', default_fontsize) -w_font = ImageFont.truetype(weatherfont, weather_font_size) +w_font = ImageFont.truetype(weatherfont, weather_fontsize) -"""Create image with given parameters""" +"""Create a blank image for black pixels and a colour image for coloured pixels""" image = Image.new('RGB', (display_width, display_height), background_colour) +image_col = Image.new('RGB', (display_width, display_height), 'white') + draw = ImageDraw.Draw(image) +draw_col = ImageDraw.Draw(image_col) + """Custom function to add text on an image""" def write_text(space_width, space_height, text, tuple, font=default, alignment='middle', autofit = False, fill_width = 1.0, fill_height = 0.8, colour = text_colour, rotation = None): - + """tuple refers to (x,y) position on display""" if autofit == True or fill_width != 1.0 or fill_height != 0.8: size = 8 font = ImageFont.truetype(font.path, size) - text_width, text_height = font.getsize(text) + text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] while text_width < int(space_width * fill_width) and text_height < int(space_height * fill_height): size += 1 font = ImageFont.truetype(font.path, size) - text_width, text_height = font.getsize(text) + text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] - text_width, text_height = font.getsize(text) + text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] while (text_width, text_height) > (space_width, space_height): text=text[0:-1] - text_width, text_height = font.getsize(text) + text_width, text_height = font.getsize(text)[0], font.getsize('hg')[1] if alignment is "" or "middle" or None: x = int((space_width / 2) - (text_width / 2)) if alignment is 'left': @@ -115,7 +133,11 @@ def write_text(space_width, space_height, text, tuple, ImageDraw.Draw(space).text((x, y), text, fill=colour, font=font) if rotation != None: space.rotate(rotation, expand = True) - image.paste(space, tuple, space) + + if colour == 'black' or 'white': + image.paste(space, tuple, space) + else: + image_col.paste(space, tuple, space) def clear_image(section, colour = background_colour): """Clear the image""" @@ -124,6 +146,10 @@ def clear_image(section, colour = background_colour): box = Image.new('RGB', (width, height), colour) image.paste(box, position) + if three_colour_support == True: + image_col.paste(box, position) + + def crop_image(input_image, section): """Crop an input image to the desired section""" x1, y1 = 0, eval(section+'_offset') @@ -152,8 +178,8 @@ def draw_square(tuple, radius, width, height, colour=text_colour, line_width=1): """Draws a square with round corners at position (x,y) from tuple""" x, y, diameter = tuple[0], tuple[1], radius*2 line_length = width - diameter - - p1, p2 = (x+radius, y), (x+radius+line_length, y) + + p1, p2 = (x+radius, y), (x+radius+line_length, y) p3, p4 = (x+width, y+radius), (x+width, y+radius+line_length) p5, p6 = (p2[0], y+height), (p1[0], y+height) p7, p8 = (x, p4[1]), (x,p3[1]) @@ -161,15 +187,26 @@ def draw_square(tuple, radius, width, height, colour=text_colour, line_width=1): c3, c4 = ((x+width)-diameter, y), (x+width, y+diameter) c5, c6 = ((x+width)-diameter, (y+height)-diameter), (x+width, y+height) c7, c8 = (x, (y+height)-diameter), (x+diameter, y+height) - - draw.line( (p1, p2) , fill=colour, width = line_width) - draw.line( (p3, p4) , fill=colour, width = line_width) - draw.line( (p5, p6) , fill=colour, width = line_width) - draw.line( (p7, p8) , fill=colour, width = line_width) - draw.arc( (c1, c2) , 180, 270, fill=colour, width=line_width) - draw.arc( (c3, c4) , 270, 360, fill=colour, width=line_width) - draw.arc( (c5, c6) , 0, 90, fill=colour, width=line_width) - draw.arc( (c7, c8) , 90, 180, fill=colour, width=line_width) + + if three_colour_support == True: + draw_col.line( (p1, p2) , fill=colour, width = line_width) + draw_col.line( (p3, p4) , fill=colour, width = line_width) + draw_col.line( (p5, p6) , fill=colour, width = line_width) + draw_col.line( (p7, p8) , fill=colour, width = line_width) + draw_col.arc( (c1, c2) , 180, 270, fill=colour, width=line_width) + draw_col.arc( (c3, c4) , 270, 360, fill=colour, width=line_width) + draw_col.arc( (c5, c6) , 0, 90, fill=colour, width=line_width) + draw_col.arc( (c7, c8) , 90, 180, fill=colour, width=line_width) + else: + draw.line( (p1, p2) , fill=colour, width = line_width) + draw.line( (p3, p4) , fill=colour, width = line_width) + draw.line( (p5, p6) , fill=colour, width = line_width) + draw.line( (p7, p8) , fill=colour, width = line_width) + draw.arc( (c1, c2) , 180, 270, fill=colour, width=line_width) + draw.arc( (c3, c4) , 270, 360, fill=colour, width=line_width) + draw.arc( (c5, c6) , 0, 90, fill=colour, width=line_width) + draw.arc( (c7, c8) , 90, 180, fill=colour, width=line_width) + def internet_available(): """check if the internet is available""" @@ -206,23 +243,12 @@ def image_cleanup(): os.remove(temp_files) print('Done') -def split_colours(image): - if three_colour_support == True: - """Split image into two, one for red pixels, the other for black pixels""" - buffer = numpy.array(image.convert('RGB')) - red, green = buffer[:, :, 0], buffer[:, :, 1] - buffer_red, buffer_black = numpy.array(image), numpy.array(image) - - buffer_red[numpy.logical_and(red >= 200, green <= 90)] = [0,0,0] #red->black - red1 = buffer_red[:,:,0] - buffer_red[red1 != 0] = [255,255,255] #white - red_im = Image.fromarray(buffer_red).convert('1',dither=True).rotate(270,expand=True) - - buffer_black[numpy.logical_and(red <= 180, red == green)] = [0,0,0] #black - red2 = buffer_black[:,:,0] - buffer_black[red2 != 0] = [255,255,255] # white - black_im = Image.fromarray(buffer_black).convert('1', dither=True).rotate(270,expand=True) - return black_im, red_im +def optimise_colours(image, threshold=220): + buffer = numpy.array(image.convert('RGB')) + red, green = buffer[:, :, 0], buffer[:, :, 1] + buffer[numpy.logical_and(red <= threshold, green <= threshold)] = [0,0,0] #grey->black + image = Image.fromarray(buffer) + return image def calibrate_display(no_of_cycles): """How many times should each colour be calibrated? Default is 3""" @@ -235,20 +261,46 @@ def calibrate_display(no_of_cycles): print('----------Started calibration of E-Paper display----------') if 'colour' in model: for _ in range(no_of_cycles): - print('Calibrating black...') + print('Calibrating...', end= ' ') + print('black...', end= ' ') epaper.display(epaper.getbuffer(black), epaper.getbuffer(white)) - print('Calibrating red/yellow...') + print('colour...', end = ' ') epaper.display(epaper.getbuffer(white), epaper.getbuffer(black)) - print('Calibrating white...') + print('white...') epaper.display(epaper.getbuffer(white), epaper.getbuffer(white)) print('Cycle {0} of {1} complete'.format(_+1, no_of_cycles)) else: for _ in range(no_of_cycles): - print('Calibrating black...') + print('Calibrating...', end= ' ') + print('black...', end = ' ') epaper.display(epaper.getbuffer(black)) - print('Calibrating white...') + print('white...') epaper.display(epaper.getbuffer(white)), print('Cycle {0} of {1} complete'.format(_+1, no_of_cycles)) - + print('-----------Calibration complete----------') epaper.sleep() + +def check_for_updates(): + with open(path+'release.txt','r') as file: + lines = file.readlines() + installed_release = lines[0].rstrip() + + temp = subp.check_output(['curl','-s','https://github.com/aceisace/Inky-Calendar/releases/latest']) + latest_release_url = str(temp).split('"')[1] + latest_release = latest_release_url.split('/tag/')[1] + + def get_id(version): + if not version.startswith('v'): + print('incorrect release format!') + v = ''.join(version.split('v')[1].split('.')) + if len(v) == 2: + v += '0' + return int(v) + + if get_id(installed_release) < get_id(latest_release): + print('New update available!. Please update to the latest version') + print('current release:', installed_release, 'new version:', latest_release) + else: + print('You are using the latest version of the Inky-Calendar software:', end = ' ') + print(installed_release) diff --git a/settings/settings-UI.html b/settings/settings-UI.html index 411ddcd..4d6ce39 100644 --- a/settings/settings-UI.html +++ b/settings/settings-UI.html @@ -35,7 +35,7 @@ body{ -
Instructions
-Insert your peesonal details and preferences and click on 'Generate'. Copy the downloaded file to the Raspberry Pi and place it in: '/home/pi/Inky-Calendar/settings/' (inside the settings folder within the Inky-Calendar folder. Lastly, reboot the Raspberry Pi to apply the changes. You can also manually run the software with:
+Insert your personal details and preferences and click on 'Generate'. Copy the downloaded file to the Raspberry Pi and place it in: '/home/pi/Inky-Calendar/settings/' (inside the settings folder within the Inky-Calendar folder. Lastly, reboot the Raspberry Pi to apply the changes. You can also manually run the software with:
python3 /home/pi/Inky-Calendar/modules/inkycal.py.