├── __init__.py ├── lib ├── sysfs_gpio.so ├── sysfs_software_spi.so ├── epdconfig.py ├── school_calendar.py └── epd7in5_V2.py ├── fonts ├── Garamond-Bold.TTF └── Garamond-Regular.TTF ├── time_quote_finder.py ├── README.md ├── quote_display.py ├── main.py ├── time_quote_generator.py └── quote_parser.py /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/sysfs_gpio.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jesseranon/author-clock/HEAD/lib/sysfs_gpio.so -------------------------------------------------------------------------------- /fonts/Garamond-Bold.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jesseranon/author-clock/HEAD/fonts/Garamond-Bold.TTF -------------------------------------------------------------------------------- /fonts/Garamond-Regular.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jesseranon/author-clock/HEAD/fonts/Garamond-Regular.TTF -------------------------------------------------------------------------------- /lib/sysfs_software_spi.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jesseranon/author-clock/HEAD/lib/sysfs_software_spi.so -------------------------------------------------------------------------------- /time_quote_finder.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import random 4 | libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib') 5 | from csv import DictReader 6 | 7 | def find_time_quote(s): 8 | with open(f'{libdir}/time-quotes-litclock-combined.csv', 'r') as read_obj: 9 | 10 | now_time = s 11 | filtered_results = [] 12 | 13 | csv_dict_reader = DictReader(read_obj) 14 | 15 | # print(column_names) 16 | for row in csv_dict_reader: 17 | # print(row['time-of-quote'], row['author']) 18 | if row['time-of-text'] == now_time: #change to result of current time conversion function 19 | filtered_results.append(row) 20 | 21 | if len(filtered_results) > 0: 22 | return random.choice(filtered_results) 23 | else: 24 | return False -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raspberry Pi Author Clock 2 | This is a DIY version of The Author Clock. The underlying script is written in Python and deployed on Raspberry Pi with a Waveshare e-paper display, mounted on a normal picture frame. 3 | 4 | **Link to project:** Blog coming soon 5 | 6 | ![Resized_20220225_064755(1)](https://user-images.githubusercontent.com/5935095/167156711-4d3d4a55-9f20-4d06-9535-06a4c157880d.jpeg) 7 | 8 | 9 | ## How It's Made: 10 | 11 | **Tech used:** Raspberry Pi Zero WH, Waveshare e-paper display, Python 12 | 13 | Aside from Waveshare's e-paper software that controls the display, the script that runs the clock is written in python. It uses a Cron job to start the Python script every day, as it is set to stop at a certain time every day because it is in a classroom. 14 | 15 | Gathered quotes from multiple sources including: 16 | https://github.com/solarkennedy/epaper-watch/ 17 | 18 | e-paper hat 19 | https://www.waveshare.com/7.5inch-e-paper-hat.htm 20 | https://github.com/waveshare/e-Paper/ 21 | 22 | ## Lessons Learned: 23 | 24 | Python library CSV reader - I learned to use this to parse through a .csv file with the necessary quotes with their information and corresponding times. 25 | 26 | Python library PIL - I learned to use this to generate image files for display on the e-paper display. 27 | 28 | ## Optimizations: 29 | 30 | I also learned that there needs to be some sort of time checking against a service to keep machine time in sync with actual time, as the clock has fallen out of sync. Being that the clock doesn't have access to the wifi in the building it is in, this is another problem to figure out. But overall this was a really fun project and it got me to fall in love with coding all over again. 31 | -------------------------------------------------------------------------------- /quote_display.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | import time 5 | import traceback 6 | 7 | picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic') 8 | libdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib') 9 | if os.path.exists(libdir): 10 | sys.path.append(libdir) 11 | 12 | 13 | # import epd7in5_V2 14 | from PIL import Image,ImageDraw,ImageFont 15 | 16 | 17 | # define display function 18 | # put everything below in it 19 | def display_quote(o): 20 | try: 21 | write_dict = o 22 | 23 | # print(write_dict) 24 | 25 | # THIS IS THE DRAW PART OF THE DISPLAY FUNCTION 26 | im = Image.new('L', (800,480), 255) 27 | draw = ImageDraw.Draw(im) 28 | 29 | # draw text 30 | for l in list(write_dict.keys()): 31 | if l == 'attribution': 32 | k = write_dict[l] 33 | draw.multiline_text((k['x'], k['y']), k['text'], font=k['font'], spacing=k['spacing'], align='right', fill=0) 34 | else: 35 | wdl = write_dict[l] 36 | for t in wdl: 37 | k = wdl[t] 38 | draw.text((k['x'], k['y']), k['text'], font=k['font'], fill=0) 39 | 40 | # FOR TESTING save to image 41 | im.save(f"1.bmp") 42 | 43 | # draw to display 44 | # logging.info("Starting quote display") 45 | # epd = epd7in5_V2.EPD() 46 | 47 | # logging.info("init and Clear") 48 | # epd.init() 49 | # epd.Clear() 50 | 51 | # epd.display(epd.getbuffer(im)) 52 | 53 | # logging.info("sending epd to sleep") 54 | # epd.sleep() 55 | 56 | except IOError as e: 57 | logging.info(e) 58 | 59 | except KeyboardInterrupt: 60 | logging.info("ctrl + c:") 61 | # epd7in5_V2.epdconfig.module_exit() 62 | exit() -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | import time 5 | from datetime import datetime 6 | libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'lib') 7 | if os.path.exists(libdir): 8 | sys.path.append(libdir) 9 | sys.path.append(os.path.dirname(__file__)) 10 | # print(sys.path) 11 | 12 | from school_calendar import calendar, days 13 | from time_quote_finder import find_time_quote 14 | from time_quote_generator import generate_time_quote 15 | from quote_parser import parse_quote 16 | from quote_display import display_quote 17 | 18 | 19 | # every day at 6 am, get the day 20 | # pull the day's schedule from calendar - check 21 | # set key times according to day's schedule 22 | # every 60 seconds - check 23 | # do the thing and display it 24 | # at the end of the day, sleep until 6 am the next day 25 | # set chronjob to run scheduler every day at 6? 26 | 27 | def get_time(): 28 | now = datetime.now() 29 | return now 30 | 31 | def get_schedule(y,m,d): 32 | try: 33 | return days[calendar[y][m][d]] 34 | except KeyError: 35 | return False 36 | 37 | def time_string(h,m): 38 | if len(str(m)) < 2: 39 | m = '0' + str(m) 40 | return f"{h}:{m}" 41 | 42 | last_quote = {'text-time': '', 'text': ''} 43 | 44 | t = get_time() 45 | sched = get_schedule(t.year,t.month,t.day) 46 | # print(t.strftime('%A')) # day of the week 47 | # print(t.strftime('%B')) # month of the year 48 | 49 | end = list(sched.keys())[len(list(sched.keys()))-1] 50 | 51 | # # testing 52 | # end_hour = 23 # set to whichever you want 0 - 23 53 | # end_minute = 59 # set to whichever you want 0 - 59 54 | end_hour = int(end[:end.find(':')]) 55 | end_minute = int(end[end.find(':')+1:]) 56 | 57 | while end_hour - t.hour > 0 or end_minute - t.minute > 0: 58 | try: 59 | t = get_time() 60 | ts = time_string(t.hour,t.minute) 61 | wait = 60 62 | if ts in list(sched.keys()): 63 | to = sched[ts] 64 | to.update({ 65 | 'text': t.strftime('%A') + ', ' + t.strftime('%B') + ' ' + str(t.day) + ', ' + str(t.year) + ' ' + sched[ts]['text-time'], 66 | }) #works 67 | wait *= 2 68 | elif find_time_quote(ts): 69 | to = find_time_quote(ts) 70 | else: 71 | to = generate_time_quote(ts) 72 | 73 | print(to) 74 | 75 | if to['text'] != last_quote['text'] and to['text-time'] != last_quote['text-time']: 76 | q = parse_quote(to) 77 | print(q) 78 | # send to epd 79 | display_quote(q) 80 | last_quote.update({ 81 | 'text': to['text'], 82 | 'text-time': to['text-time'] 83 | }) 84 | 85 | # sleep 86 | t = get_time() 87 | ms = (float(wait*1000000) - (t.second*1000000) - t.microsecond)/1000000 88 | time.sleep(ms) 89 | 90 | except IOError as e: 91 | logging.info(e) 92 | 93 | except KeyboardInterrupt: 94 | logging.info("ctrl + c:") 95 | exit() 96 | 97 | print('Scheduler has ended for the day') 98 | 99 | # perfect! -------------------------------------------------------------------------------- /time_quote_generator.py: -------------------------------------------------------------------------------- 1 | import random 2 | from datetime import datetime #for testing 3 | from school_calendar import default_title, default_author 4 | # generate random time quote for times that don't exist in time-quotes csv 5 | # generate special messages according to class days 6 | 7 | numbers = { 8 | 0: ['0', 'o\'clock'], 9 | 1: ['1', 'one'], 10 | 2: ['2', 'two'], 11 | 3: ['3', 'three'], 12 | 4: ['4', 'four'], 13 | 5: ['5', 'five'], 14 | 6: ['6', 'six'], 15 | 7: ['7', 'seven'], 16 | 8: ['8', 'eight'], 17 | 9: ['9', 'nine'], 18 | 10: ['10', 'ten'], 19 | 11: ['11', 'eleven'], 20 | 12: ['12', 'twelve'], 21 | 13: ['13', 'thirteen'], 22 | 14: ['14', 'fourteen'], 23 | 15: ['15', 'fifteen', 'a quarter'], 24 | 16: ['16', 'sixteen'], 25 | 17: ['17', 'seventeen'], 26 | 18: ['18', 'eighteen'], 27 | 19: ['19', 'nineteen'], 28 | 20: ['20', 'twenty'], 29 | 30: ['30', 'thirty'], 30 | 40: ['40', 'forty'], 31 | 50: ['50', 'fifty'] 32 | } 33 | 34 | 35 | def generate_time_quote(s): 36 | # print(f"{default_title} {default_author}") 37 | quote = {'text-time': '', 'text': '', 'title': default_title, 'author': default_author} 38 | 39 | formatting = { 40 | 'until': ['it', 'until', 'hours'], 41 | 'after': ['it', 'after', 'hours'], 42 | 'numbers': ['it', 'hours'] 43 | } 44 | words = { 45 | 'it': { 46 | 'The time': {'is': ['is now', 'is', 'now reads', 'has come upon']}, 47 | 'It': {'is': ['is', 'is now']}, 48 | 'The clock': {'is': ['reads', 'now reads', 'has struck', 'strikes', 'chimes']} 49 | }, 50 | 'until': ['to', 'until'], 51 | 'after': ['past', 'after'], 52 | 'am': ['am', ['in the morning', 'this morning', 'this fine morning']], 53 | 'pm': ['pm', ['in the afternoon', 'this afternoon', 'this fine afternoon']] 54 | } 55 | #choose formatting 56 | f = random.choice(list(formatting.keys())) 57 | 58 | h = int(s[:s.find(':')]) 59 | m = int(s[s.find(':')+1:]) 60 | 61 | t = '' #text 62 | tt = '' #text-time 63 | 64 | if m > 0 and f == 'until': 65 | h += 1 66 | if m > 0: 67 | m = 60 - m 68 | 69 | if h >= 12: 70 | formatting[f].append('pm') 71 | if h > 12: 72 | h -= 12 73 | else: 74 | formatting[f].append('am') 75 | 76 | if f == 'numbers': 77 | if len(str(m)) < 2: 78 | m = '0' + str(m) 79 | words.update({ 80 | 'hours': [f"{h}:{m}"] 81 | }) 82 | else: 83 | # randomly choose whether hours is a number or word 84 | h = random.choice(numbers[h]) 85 | # if m == 0, randomly choose whether to append o'clock to hours 86 | if m == 0: 87 | if random.choice(numbers[m]) == 'o\'clock': 88 | h = str(h) + ' ' + 'o\'clock' 89 | else: 90 | # randomly choose whether minutes is a number or word 91 | mins = '' 92 | try: 93 | mins += f"{random.choice(numbers[m])}" 94 | except KeyError: 95 | # # if word, generate proper wording for two-digit minutes 96 | if random.choice([0,1]) > 0: 97 | # generate word format 98 | tens = m - (m % 10) 99 | m -= tens 100 | mins = f"{numbers[tens][1]}-{numbers[m][1]}" 101 | else: 102 | mins = m 103 | # choose whether to concatenate ' minutes' 104 | if random.choice([0,1]) > 0: 105 | if int(m) > 1: 106 | mins = str(mins) + " minutes" 107 | else: 108 | mins = str(mins) + " minute" 109 | words.update({ 110 | 'minutes': [mins] 111 | }) 112 | words.update({ 113 | 'hours': [h] 114 | }) 115 | 116 | formatting[f].insert(1, 'minutes') 117 | 118 | ttl = formatting[f][slice(1,len(formatting[f]))] 119 | 120 | for w in formatting[f]: 121 | try: 122 | if w == 'it': 123 | c = random.choice(list(words[w].keys())) 124 | t += c + ' ' + random.choice(words[w][c]['is']) 125 | elif type(words[w]) == list: 126 | c = random.choice(words[w]) 127 | while type(c) == list: 128 | c = random.choice(c) 129 | t += ' ' + str(c) 130 | if w in ttl: 131 | tt += str(c) + ' ' 132 | except KeyError: 133 | continue 134 | t += '.' 135 | tt = tt[0:len(tt)-1] 136 | quote.update({ 137 | 'text-time': tt, 138 | 'text': t 139 | }) 140 | 141 | # print(quote) 142 | return quote 143 | 144 | # # TESTING BELOW THIS LINE 145 | # now = datetime.now() 146 | # hour = now.hour 147 | # minute = now.minute 148 | # def time_string(h,m): 149 | # if len(str(m)) < 2: 150 | # m = '0' + str(m) 151 | # return f"{h}:{m}" 152 | 153 | # times = time_string(hour,minute) 154 | 155 | # generate_time_quote(times) -------------------------------------------------------------------------------- /quote_parser.py: -------------------------------------------------------------------------------- 1 | from PIL import Image,ImageDraw,ImageFont # for testing 2 | 3 | def parse_quote(o): 4 | quote = o 5 | 6 | DISPLAY_WIDTH = 800 7 | DISPLAY_HEIGHT = 480 8 | FONT_SIZE = 30 9 | TOP_MARGIN = 15 10 | SIDE_MARGIN = 15 11 | SPACING = 3 # padding between lines of text 12 | 13 | garamond = { 14 | 'regular': ImageFont.truetype('fonts/Garamond-Regular.TTF',FONT_SIZE), 15 | 'bold': ImageFont.truetype('fonts/Garamond-Bold.TTF',FONT_SIZE), 16 | #'italic': ImageFont.truetype('fonts/Garamond-Italic.ttf',FONT_SIZE) 17 | } 18 | 19 | font_reg = garamond['regular'] 20 | font_bold = garamond['bold'] 21 | 22 | pre_time = {'self': 'pre_time', 'font': font_reg} 23 | text_time = {'self': 'text_time', 'font': font_bold, 'text': quote['text-time']} 24 | post_time = {'self': 'post_time', 'font': font_reg} 25 | quote_text = quote['text'].strip() 26 | quote_title = quote['title'].strip() 27 | quote_author = quote['author'].strip() 28 | ttb = quote_text.find(text_time['text']) 29 | tte = ttb + len(text_time['text']) 30 | if ttb > 0: 31 | pre_time['text'] = quote_text[0:ttb] 32 | else: 33 | pre_time['text'] = '' 34 | if tte >= len(quote_text): 35 | post_time['text'] = '' 36 | else: 37 | post_time['text'] = quote_text[tte:] 38 | attribution = f"{quote_title}\n{quote_author}" 39 | 40 | left_margin = SIDE_MARGIN 41 | top_margin = TOP_MARGIN 42 | right_margin = DISPLAY_WIDTH - left_margin 43 | bottom_margin = DISPLAY_HEIGHT - top_margin 44 | 45 | reg_ascent, reg_descent = font_reg.getmetrics() 46 | (a_width, a_height), (a_offset_x, a_offset_y) = font_reg.font.getsize(quote_author) 47 | (t_width, t_height), (t_offset_x, t_offset_y) = font_reg.font.getsize(quote_title) 48 | ## other stuff here 49 | 50 | # set attribution width 51 | att_width = 0 52 | if t_width > a_width: 53 | att_width += t_width 54 | else: 55 | att_width += a_width 56 | 57 | # set attribution x, y 58 | att_x = right_margin - att_width 59 | att_y = bottom_margin - (reg_ascent*2) - reg_descent - SPACING 60 | 61 | # # set quote box height (to center paragraph vertically) 62 | # QUOTE_BOX_HEIGHT = att_y - top_margin 63 | 64 | ## split the lines 65 | line = 1 66 | 67 | write_dict = {} 68 | 69 | xpix, ypix = (left_margin, top_margin) 70 | to_print = [pre_time, text_time, post_time] 71 | 72 | for t in range(0,len(to_print)): 73 | 74 | curr = to_print[t] 75 | text_buffer = [curr['text']] 76 | 77 | while len(text_buffer) > 0: 78 | 79 | while xpix + curr['font'].getlength(text_buffer[0]) > right_margin: 80 | 81 | try: 82 | 83 | e = text_buffer[0].rindex(' ') 84 | 85 | try: 86 | 87 | text_buffer[1] = text_buffer[0][e:] + text_buffer[1] 88 | 89 | except IndexError: 90 | 91 | text_buffer.append(text_buffer[0][e:]) 92 | 93 | text_buffer[0] = text_buffer[0][0:e] 94 | 95 | except ValueError: 96 | 97 | xpix = left_margin 98 | ypix += reg_ascent + SPACING 99 | line += 1 100 | 101 | try: 102 | text_buffer[0] += text_buffer.pop(1) 103 | 104 | except IndexError: 105 | 106 | continue 107 | 108 | try: 109 | write_dict[str(line)] 110 | except KeyError: 111 | write_dict.update({ 112 | str(line): {} 113 | }) 114 | 115 | write_dict[str(line)].update({ 116 | curr['self']: { 117 | 'font': curr['font'], 118 | 'x': xpix, 119 | 'y': ypix, 120 | 'text': text_buffer[0] 121 | } 122 | }) 123 | 124 | if len(text_buffer) > 1: # if there is more from this text still in the buffer 125 | line += 1 126 | xpix =left_margin 127 | ypix += reg_ascent + SPACING 128 | text_buffer[1] = text_buffer[1][1:] # remove extraneous space at front. 129 | else: 130 | xpix += curr['font'].getlength(text_buffer[0]) 131 | 132 | text_buffer.pop(0) 133 | 134 | write_dict.update({ 135 | 'attribution': { 136 | 'font': font_reg, 137 | 'x': att_x, 138 | 'y': att_y, 139 | 'text': attribution, 140 | 'spacing': SPACING 141 | } 142 | }) 143 | 144 | # print(write_dict) 145 | return write_dict -------------------------------------------------------------------------------- /lib/epdconfig.py: -------------------------------------------------------------------------------- 1 | # /***************************************************************************** 2 | # * | File : epdconfig.py 3 | # * | Author : Waveshare team 4 | # * | Function : Hardware underlying interface 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V1.0 8 | # * | Date : 2019-06-21 9 | # * | Info : 10 | # ****************************************************************************** 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | import os 31 | import logging 32 | import sys 33 | import time 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | class RaspberryPi: 39 | # Pin definition 40 | RST_PIN = 17 41 | DC_PIN = 25 42 | CS_PIN = 8 43 | BUSY_PIN = 24 44 | 45 | def __init__(self): 46 | import spidev 47 | import RPi.GPIO 48 | 49 | self.GPIO = RPi.GPIO 50 | self.SPI = spidev.SpiDev() 51 | 52 | def digital_write(self, pin, value): 53 | self.GPIO.output(pin, value) 54 | 55 | def digital_read(self, pin): 56 | return self.GPIO.input(pin) 57 | 58 | def delay_ms(self, delaytime): 59 | time.sleep(delaytime / 1000.0) 60 | 61 | def spi_writebyte(self, data): 62 | self.SPI.writebytes(data) 63 | 64 | def spi_writebyte2(self, data): 65 | self.SPI.writebytes2(data) 66 | 67 | def module_init(self): 68 | self.GPIO.setmode(self.GPIO.BCM) 69 | self.GPIO.setwarnings(False) 70 | self.GPIO.setup(self.RST_PIN, self.GPIO.OUT) 71 | self.GPIO.setup(self.DC_PIN, self.GPIO.OUT) 72 | self.GPIO.setup(self.CS_PIN, self.GPIO.OUT) 73 | self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) 74 | 75 | # SPI device, bus = 0, device = 0 76 | self.SPI.open(0, 0) 77 | self.SPI.max_speed_hz = 4000000 78 | self.SPI.mode = 0b00 79 | return 0 80 | 81 | def module_exit(self): 82 | logger.debug("spi end") 83 | self.SPI.close() 84 | 85 | logger.debug("close 5V, Module enters 0 power consumption ...") 86 | self.GPIO.output(self.RST_PIN, 0) 87 | self.GPIO.output(self.DC_PIN, 0) 88 | 89 | self.GPIO.cleanup() 90 | 91 | 92 | class JetsonNano: 93 | # Pin definition 94 | RST_PIN = 17 95 | DC_PIN = 25 96 | CS_PIN = 8 97 | BUSY_PIN = 24 98 | 99 | def __init__(self): 100 | import ctypes 101 | find_dirs = [ 102 | os.path.dirname(os.path.realpath(__file__)), 103 | '/usr/local/lib', 104 | '/usr/lib', 105 | ] 106 | self.SPI = None 107 | for find_dir in find_dirs: 108 | so_filename = os.path.join(find_dir, 'sysfs_software_spi.so') 109 | if os.path.exists(so_filename): 110 | self.SPI = ctypes.cdll.LoadLibrary(so_filename) 111 | break 112 | if self.SPI is None: 113 | raise RuntimeError('Cannot find sysfs_software_spi.so') 114 | 115 | import Jetson.GPIO 116 | self.GPIO = Jetson.GPIO 117 | 118 | def digital_write(self, pin, value): 119 | self.GPIO.output(pin, value) 120 | 121 | def digital_read(self, pin): 122 | return self.GPIO.input(self.BUSY_PIN) 123 | 124 | def delay_ms(self, delaytime): 125 | time.sleep(delaytime / 1000.0) 126 | 127 | def spi_writebyte(self, data): 128 | self.SPI.SYSFS_software_spi_transfer(data[0]) 129 | 130 | def module_init(self): 131 | self.GPIO.setmode(self.GPIO.BCM) 132 | self.GPIO.setwarnings(False) 133 | self.GPIO.setup(self.RST_PIN, self.GPIO.OUT) 134 | self.GPIO.setup(self.DC_PIN, self.GPIO.OUT) 135 | self.GPIO.setup(self.CS_PIN, self.GPIO.OUT) 136 | self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN) 137 | self.SPI.SYSFS_software_spi_begin() 138 | return 0 139 | 140 | def module_exit(self): 141 | logger.debug("spi end") 142 | self.SPI.SYSFS_software_spi_end() 143 | 144 | logger.debug("close 5V, Module enters 0 power consumption ...") 145 | self.GPIO.output(self.RST_PIN, 0) 146 | self.GPIO.output(self.DC_PIN, 0) 147 | 148 | self.GPIO.cleanup() 149 | 150 | 151 | if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'): 152 | implementation = RaspberryPi() 153 | else: 154 | implementation = JetsonNano() 155 | 156 | for func in [x for x in dir(implementation) if not x.startswith('_')]: 157 | setattr(sys.modules[__name__], func, getattr(implementation, func)) 158 | 159 | 160 | ### END OF FILE ### -------------------------------------------------------------------------------- /lib/school_calendar.py: -------------------------------------------------------------------------------- 1 | default_title = 'Ms. Haley\'s Class' 2 | default_author = 'Clock Bot' 3 | 4 | calendar = { 5 | # A - A Day 6 | # B - B Day 7 | # C - School Closed/Holidays 8 | # S - Teacher in-Service Day (no school for students) 9 | # T - Testing Day - Full Day 10 | # H - Homeroom / First day of school 11 | # L - Last day of school 12 | 2021: { 13 | 12: { 14 | 10: 'B', 15 | 13: 'A', 16 | 14: 'T', 17 | 15: 'T', 18 | 16: 'T', 19 | 17: 'T', 20 | 20: 'C', 21 | 21: 'C', 22 | 22: 'C', 23 | 23: 'C', 24 | 24: 'C', 25 | 27: 'C', 26 | 28: 'C', 27 | 29: 'C', 28 | 30: 'C' 29 | } 30 | }, 31 | 2022: { 32 | 1: { 33 | 3: 'C', 34 | 4: 'T', 35 | 5: 'B', 36 | 6: 'A', 37 | 7: 'B', 38 | 10: 'A', 39 | 11: 'B', 40 | 12: 'A', 41 | 13: 'B', 42 | 14: 'A', 43 | 17: 'C', 44 | 18: 'B', 45 | 19: 'A', 46 | 20: 'B', 47 | 21: 'A', 48 | 24: 'B', 49 | 25: 'A', 50 | 26: 'B', 51 | 27: 'A', 52 | 28: 'B' 53 | }, 54 | 2: { 55 | 1: 'B', 56 | 2: 'A', 57 | 3: 'B', 58 | 4: 'A', 59 | 7: 'C', 60 | 8: 'B', 61 | 9: 'A', 62 | 10: 'B', 63 | 11: 'A', 64 | 14: 'B', 65 | 15: 'A', 66 | 16: 'B', 67 | 17: 'A', 68 | 18: 'B', 69 | 21: 'C', 70 | 22: 'A', 71 | 23: 'B', 72 | 24: 'A', 73 | 25: 'B', 74 | 28: 'A' 75 | }, 76 | 3: { 77 | 1: 'T', 78 | 2: 'B', 79 | 3: 'A', 80 | 4: 'B', 81 | 7: 'A', 82 | 8: 'B', 83 | 9: 'A', 84 | 10: 'B', 85 | 11: 'A', 86 | 14: 'C', 87 | 15: 'B', 88 | 16: 'A', 89 | 17: 'B', 90 | 18: 'A', 91 | 21: 'B', 92 | 22: 'A', 93 | 23: 'B', 94 | 24: 'A', 95 | 25: 'B', 96 | 28: 'A', 97 | 29: 'B', 98 | 30: 'A', 99 | 31: 'B' 100 | }, 101 | 4: { 102 | 1: 'A', 103 | 4: 'B', 104 | 5: 'A', 105 | 6: 'B', 106 | 7: 'A', 107 | 8: 'B', 108 | 11: 'C', 109 | 12: 'C', 110 | 13: 'C', 111 | 14: 'C', 112 | 15: 'C', 113 | 18: 'C', 114 | 19: 'A', 115 | 20: 'B', 116 | 21: 'A', 117 | 22: 'B', 118 | 25: 'S', 119 | 26: 'A', 120 | 27: 'B', 121 | 28: 'A', 122 | 29: 'B' 123 | }, 124 | 5: { 125 | 2: 'A', 126 | 3: 'B', 127 | 4: 'A', 128 | 5: 'B', 129 | 6: 'A', 130 | 9: 'B', 131 | 10: 'A', 132 | 11: 'B', 133 | 12: 'A', 134 | 13: 'B', 135 | 16: 'A', 136 | 17: 'B', 137 | 18: 'A', 138 | 19: 'B', 139 | 20: 'T', 140 | 23: 'T', 141 | 24: 'T', 142 | 25: 'T' 143 | }, 144 | 8: {} 145 | } 146 | } 147 | 148 | days = { 149 | # key times for each type of school day 150 | # Day 151 | # Date 152 | # Period 153 | # Time 154 | 'A': { 155 | '7:00': {'text-time': 'Period 1', 'author': default_author, 'title': 'Welcome'}, 156 | '8:25': {'text-time': 'Period 1', 'author': default_author, 'title': 'Have a nice day'}, 157 | '8:30': {'text-time': 'Period 3', 'author': default_author, 'title': 'Welcome'}, 158 | '9:55': {'text-time': 'Period 3', 'author': default_author, 'title': 'Have a nice day'}, 159 | '10:00': {'text-time': 'Period 5', 'author': default_author, 'title': 'Welcome'}, 160 | '10:45': {'text-time': 'Lunch 2', 'author': default_author, 'title': 'Why are you here'}, 161 | '11:10': {'text-time': 'Lunch 2', 'author': default_author, 'title': 'Go to class'}, 162 | '11:15': {'text-time': 'Period 5', 'author': default_author, 'title': 'Welcome back'}, 163 | '11:55': {'text-time': 'Period 5', 'author': default_author, 'title': 'Have a nice day'}, 164 | '12:00': {'text-time': 'Period 7', 'author': default_author, 'title': 'Welcome'}, 165 | '13:25': {'text-time': 'Period 7', 'author': default_author, 'title': 'Have a nice day'}, 166 | '14:00': {'text-time': 'End of day', 'author': default_author, 'title': 'Your boys miss you'} 167 | }, 168 | 'B': { 169 | '7:00': {'text-time': 'Period 2', 'author': default_author, 'title': 'Welcome'}, 170 | '8:25': {'text-time': 'Period 2', 'author': default_author, 'title': 'Have a nice day'}, 171 | '8:30': {'text-time': 'Period 4', 'author': default_author, 'title': 'Welcome'}, 172 | '9:55': {'text-time': 'Period 4', 'author': default_author, 'title': 'Have a nice day'}, 173 | '10:00': {'text-time': 'Period 6', 'author': default_author, 'title': 'Welcome'}, 174 | '10:45': {'text-time': 'Lunch 2', 'author': default_author, 'title': 'Why are you here'}, 175 | '11:10': {'text-time': 'Lunch 2', 'author': default_author, 'title': 'Go to class'}, 176 | '11:15': {'text-time': 'Period 6', 'author': default_author, 'title': 'Welcome back'}, 177 | '11:55': {'text-time': 'Period 6', 'author': default_author, 'title': 'Have a nice day'}, 178 | '12:00': {'text-time': 'Period 8', 'author': default_author, 'title': 'Welcome'}, 179 | '13:25': {'text-time': 'Period 8', 'author': default_author, 'title': 'Have a nice day'}, 180 | '14:00': {'text-time': 'End of day', 'author': default_author, 'title': 'Your boys miss you'} 181 | }, 182 | 'C': { 183 | '7:00': {'text-time': 'Why are you here?', 'author': default_author, 'title': 'School\'s closed'}, 184 | '14:00': {'text-time': 'Why are you here?', 'author': default_author, 'title': 'School\'s closed'} 185 | }, 186 | 'H': { 187 | '7:00': {'text-time': 'Welcome to school!','author': default_author, 'title': 'Ms. Haley and'}, 188 | '13:25': {'text-time': 'Welcome to school!','author': default_author, 'title': 'Ms. Haley and'}, 189 | '14:00': {'text-time': 'First day done!','author': default_author, 'title': 'Time to go home'} 190 | }, 191 | 'L': { 192 | '7:00': {'text-time': 'Last day of school!', 'author': default_author, 'title': 'Congrats!'}, 193 | '13:25': {'text-time': 'Last day of school!', 'author': default_author, 'title': 'Congrats!'}, 194 | '14:00': {'text-time': 'Last day of school!', 'author': default_author, 'title': 'Congrats!'} 195 | }, 196 | 'S': { 197 | '7:00': {'text-time': 'Time for busy work!', 'author': default_author, 'title': 'You can do it!'}, 198 | '13:25': {'text-time': 'Time for busy work!', 'author': default_author, 'title': 'Your students would be leaving if they were here'}, 199 | '14:00': {'text-time': 'Time for busy work!', 'author': default_author, 'title': 'Time to go home!'} 200 | }, 201 | 'T': { 202 | '7:00': {'text-time': 'Take your tests!', 'author': default_author, 'title': 'Good luck!'}, 203 | '13:25': {'text-time': 'Congrats on a job well done!', 'author': default_author, 'title': 'You made it!'}, 204 | '14:00': {'text-time': 'Take your tests!', 'author': default_author, 'title': 'Time to go home!'} 205 | } 206 | } -------------------------------------------------------------------------------- /lib/epd7in5_V2.py: -------------------------------------------------------------------------------- 1 | # ***************************************************************************** 2 | # * | File : epd7in5.py 3 | # * | Author : Waveshare team 4 | # * | Function : Electronic paper driver 5 | # * | Info : 6 | # *---------------- 7 | # * | This version: V4.0 8 | # * | Date : 2019-06-20 9 | # # | Info : python demo 10 | # ----------------------------------------------------------------------------- 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documnetation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | # 29 | 30 | 31 | import logging 32 | from . import epdconfig 33 | 34 | # Display resolution 35 | EPD_WIDTH = 800 36 | EPD_HEIGHT = 480 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | class EPD: 41 | def __init__(self): 42 | self.reset_pin = epdconfig.RST_PIN 43 | self.dc_pin = epdconfig.DC_PIN 44 | self.busy_pin = epdconfig.BUSY_PIN 45 | self.cs_pin = epdconfig.CS_PIN 46 | self.width = EPD_WIDTH 47 | self.height = EPD_HEIGHT 48 | 49 | Voltage_Frame_7IN5_V2 = [ 50 | 0x6, 0x3F, 0x3F, 0x11, 0x24, 0x7, 0x17, 51 | ] 52 | 53 | LUT_VCOM_7IN5_V2 = [ 54 | 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 55 | 0x0, 0xF, 0x1, 0xF, 0x1, 0x2, 56 | 0x0, 0xF, 0xF, 0x0, 0x0, 0x1, 57 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 58 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 59 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 60 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 61 | ] 62 | 63 | LUT_WW_7IN5_V2 = [ 64 | 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 65 | 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 66 | 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 67 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 68 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 69 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 70 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 71 | ] 72 | 73 | LUT_BW_7IN5_V2 = [ 74 | 0x10, 0xF, 0xF, 0x0, 0x0, 0x1, 75 | 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 76 | 0x20, 0xF, 0xF, 0x0, 0x0, 0x1, 77 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 78 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 79 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 80 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 81 | ] 82 | 83 | LUT_WB_7IN5_V2 = [ 84 | 0x80, 0xF, 0xF, 0x0, 0x0, 0x1, 85 | 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 86 | 0x40, 0xF, 0xF, 0x0, 0x0, 0x1, 87 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 88 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 89 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 90 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 91 | ] 92 | 93 | LUT_BB_7IN5_V2 = [ 94 | 0x80, 0xF, 0xF, 0x0, 0x0, 0x1, 95 | 0x84, 0xF, 0x1, 0xF, 0x1, 0x2, 96 | 0x40, 0xF, 0xF, 0x0, 0x0, 0x1, 97 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 98 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 99 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 100 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 101 | ] 102 | 103 | # Hardware reset 104 | def reset(self): 105 | epdconfig.digital_write(self.reset_pin, 1) 106 | epdconfig.delay_ms(20) 107 | epdconfig.digital_write(self.reset_pin, 0) 108 | epdconfig.delay_ms(2) 109 | epdconfig.digital_write(self.reset_pin, 1) 110 | epdconfig.delay_ms(20) 111 | 112 | def send_command(self, command): 113 | epdconfig.digital_write(self.dc_pin, 0) 114 | epdconfig.digital_write(self.cs_pin, 0) 115 | epdconfig.spi_writebyte([command]) 116 | epdconfig.digital_write(self.cs_pin, 1) 117 | 118 | def send_data(self, data): 119 | epdconfig.digital_write(self.dc_pin, 1) 120 | epdconfig.digital_write(self.cs_pin, 0) 121 | epdconfig.spi_writebyte([data]) 122 | epdconfig.digital_write(self.cs_pin, 1) 123 | 124 | def send_data2(self, data): 125 | epdconfig.digital_write(self.dc_pin, 1) 126 | epdconfig.digital_write(self.cs_pin, 0) 127 | epdconfig.SPI.writebytes2(data) 128 | epdconfig.digital_write(self.cs_pin, 1) 129 | 130 | def ReadBusy(self): 131 | logger.debug("e-Paper busy") 132 | self.send_command(0x71) 133 | busy = epdconfig.digital_read(self.busy_pin) 134 | while(busy == 0): 135 | self.send_command(0x71) 136 | busy = epdconfig.digital_read(self.busy_pin) 137 | epdconfig.delay_ms(20) 138 | logger.debug("e-Paper busy release") 139 | 140 | def SetLut(self, lut_vcom, lut_ww, lut_bw, lut_wb, lut_bb): 141 | self.send_command(0x20) 142 | for count in range(0, 42): 143 | self.send_data(lut_vcom[count]) 144 | 145 | self.send_command(0x21) 146 | for count in range(0, 42): 147 | self.send_data(lut_ww[count]) 148 | 149 | self.send_command(0x22) 150 | for count in range(0, 42): 151 | self.send_data(lut_bw[count]) 152 | 153 | self.send_command(0x23) 154 | for count in range(0, 42): 155 | self.send_data(lut_wb[count]) 156 | 157 | self.send_command(0x24) 158 | for count in range(0, 42): 159 | self.send_data(lut_bb[count]) 160 | 161 | def init(self): 162 | if (epdconfig.module_init() != 0): 163 | return -1 164 | # EPD hardware init start 165 | self.reset() 166 | 167 | # self.send_command(0x06) # btst 168 | # self.send_data(0x17) 169 | # self.send_data(0x17) 170 | # self.send_data(0x28) # If an exception is displayed, try using 0x38 171 | # self.send_data(0x17) 172 | 173 | # self.send_command(0x01) #POWER SETTING 174 | # self.send_data(0x07) 175 | # self.send_data(0x07) #VGH=20V,VGL=-20V 176 | # self.send_data(0x3f) #VDH=15V 177 | # self.send_data(0x3f) #VDL=-15V 178 | 179 | self.send_command(0x01); # power setting 180 | self.send_data(0x17); # 1-0=11: internal power 181 | self.send_data(self.Voltage_Frame_7IN5_V2[6]); # VGH&VGL 182 | self.send_data(self.Voltage_Frame_7IN5_V2[1]); # VSH 183 | self.send_data(self.Voltage_Frame_7IN5_V2[2]); # VSL 184 | self.send_data(self.Voltage_Frame_7IN5_V2[3]); # VSHR 185 | 186 | self.send_command(0x82); # VCOM DC Setting 187 | self.send_data(self.Voltage_Frame_7IN5_V2[4]); # VCOM 188 | 189 | self.send_command(0x06); # Booster Setting 190 | self.send_data(0x27); 191 | self.send_data(0x27); 192 | self.send_data(0x2F); 193 | self.send_data(0x17); 194 | 195 | self.send_command(0x30); # OSC Setting 196 | self.send_data(self.Voltage_Frame_7IN5_V2[0]); # 2-0=100: N=4 ; 5-3=111: M=7 ; 3C=50Hz 3A=100HZ 197 | 198 | self.send_command(0x04) #POWER ON 199 | epdconfig.delay_ms(100) 200 | self.ReadBusy() 201 | 202 | self.send_command(0X00) #PANNEL SETTING 203 | self.send_data(0x3F) #KW-3f KWR-2F BWROTP 0f BWOTP 1f 204 | 205 | self.send_command(0x61) #tres 206 | self.send_data(0x03) #source 800 207 | self.send_data(0x20) 208 | self.send_data(0x01) #gate 480 209 | self.send_data(0xE0) 210 | 211 | self.send_command(0X15) 212 | self.send_data(0x00) 213 | 214 | self.send_command(0X50) #VCOM AND DATA INTERVAL SETTING 215 | self.send_data(0x10) 216 | self.send_data(0x07) 217 | 218 | self.send_command(0X60) #TCON SETTING 219 | self.send_data(0x22) 220 | 221 | self.send_command(0x65); # Resolution setting 222 | self.send_data(0x00); 223 | self.send_data(0x00); # 800*480 224 | self.send_data(0x00); 225 | self.send_data(0x00); 226 | 227 | self.SetLut(self.LUT_VCOM_7IN5_V2, self.LUT_WW_7IN5_V2, self.LUT_BW_7IN5_V2, self.LUT_WB_7IN5_V2, self.LUT_BB_7IN5_V2) 228 | # EPD hardware init end 229 | return 0 230 | 231 | def getbuffer(self, image): 232 | img = image 233 | imwidth, imheight = img.size 234 | if(imwidth == self.width and imheight == self.height): 235 | img = img.convert('1') 236 | elif(imwidth == self.height and imheight == self.width): 237 | # image has correct dimensions, but needs to be rotated 238 | img = img.rotate(90, expand=True).convert('1') 239 | else: 240 | logger.warning("Wrong image dimensions: must be " + str(self.width) + "x" + str(self.height)) 241 | # return a blank buffer 242 | return [0x00] * (int(self.width/8) * self.height) 243 | 244 | buf = bytearray(img.tobytes('raw')) 245 | # The bytes need to be inverted, because in the PIL world 0=black and 1=white, but 246 | # in the e-paper world 0=white and 1=black. 247 | for i in range(len(buf)): 248 | buf[i] ^= 0xFF 249 | return buf 250 | 251 | def display(self, image): 252 | self.send_command(0x13) 253 | self.send_data2(image) 254 | 255 | self.send_command(0x12) 256 | epdconfig.delay_ms(100) 257 | self.ReadBusy() 258 | 259 | def Clear(self): 260 | buf = [0x00] * (int(self.width/8) * self.height) 261 | self.send_command(0x10) 262 | self.send_data2(buf) 263 | self.send_command(0x13) 264 | self.send_data2(buf) 265 | self.send_command(0x12) 266 | epdconfig.delay_ms(100) 267 | self.ReadBusy() 268 | 269 | def sleep(self): 270 | self.send_command(0x02) # POWER_OFF 271 | self.ReadBusy() 272 | 273 | self.send_command(0x07) # DEEP_SLEEP 274 | self.send_data(0XA5) 275 | 276 | epdconfig.delay_ms(2000) 277 | epdconfig.module_exit() 278 | ### END OF FILE ### --------------------------------------------------------------------------------