#!/usr/bin/python3 # -*- coding: utf-8 -*- #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! # IMPORTANT CONSIDERATIONS # - Button presses may be missed if you push/release too fast # - Button presses read most reliably after about 0.25 seconds being held down #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! ######################################## # Inspiration / Further Reading ######################################## # https://circuitpython.readthedocs.io/projects/bh1750/en/latest/ # https://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio-part-3 # https://stackoverflow.com/questions/16148735/how-to-implement-a-watchdog-timer-in-python # https://learn.adafruit.com/adafruit-bh1750-ambient-light-sensor # https://learn.adafruit.com/adafruit-mini-pitft-135x240-color-tft-add-on-for-raspberry-pi/overview ######################################## # Imports and whatnot ######################################## import adafruit_bh1750 import adafruit_rgb_display.st7789 as st7789 import board import busio import dbus import digitalio import os import RPi.GPIO as GPIO import time from datetime import datetime from PIL import Image, ImageDraw, ImageFont from threading import Timer ######################################## # Generic watchdog timer class ######################################## class Watchdog: def __init__(self, timeout, userHandler=None): # timeout in seconds self.timeout = timeout self.handler = userHandler if userHandler is not None else self.defaultHandler self.timer = Timer(self.timeout, self.handler) # Do NOT start timer by default ; we want this triggered by reset events that come through # via the inotify filesystem watcher #self.timer.start() def reset(self): self.timer.cancel() self.timer = Timer(self.timeout, self.handler) self.timer.start() def stop(self): self.timer.cancel() def defaultHandler(self): raise self ######################################## # Cheat for walking only one level deep via os.walk (faster than other options) ######################################## def walklevel(some_dir, level=1): some_dir = some_dir.rstrip(os.path.sep) assert os.path.isdir(some_dir) num_sep = some_dir.count(os.path.sep) for root, dirs, files in os.walk(some_dir): yield root, dirs, files num_sep_this = root.count(os.path.sep) if num_sep + level <= num_sep_this: del dirs[:] ######################################## # bh1740 lux sensor ######################################## i2c = busio.I2C(board.SCL, board.SDA) sensor = adafruit_bh1750.BH1750(i2c) ######################################## # Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4): ######################################## cs_pin = digitalio.DigitalInOut(board.CE0) dc_pin = digitalio.DigitalInOut(board.D25) reset_pin = None ######################################## # Setup button pins as inputs ######################################## GPIO.setup(23, GPIO.IN) GPIO.setup(24, GPIO.IN) ######################################## # Config for display baudrate (default max is 24mhz) ######################################## BAUDRATE = 64000000 ######################################## # Setup SPI bus using hardware SPI ######################################## spi = board.SPI() ######################################## # Create the ST7789 display ######################################## disp = st7789.ST7789( spi, cs=cs_pin, dc=dc_pin, rst=reset_pin, baudrate=BAUDRATE, width=135, height=240, x_offset=53, y_offset=40, ) ######################################## # Create blank image for drawing. # Make sure to create image with mode 'RGB' for full color. ######################################## height = disp.width # we swap height/width to rotate it to landscape! width = disp.height image = Image.new('RGB', (width, height)) rotation = 90 ######################################## # Get drawing object to draw on image. ######################################## draw = ImageDraw.Draw(image) ######################################## # Draw a black filled box to clear the image ######################################## draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0)) disp.image(image, rotation) ######################################## # Draw some shapes # First define some constants to allow easy resizing of shapes. ######################################## padding = -2 top = padding bottom = height - padding ######################################## # Move left to right keeping track of the current x position for drawing shapes. ######################################## x = 0 ######################################## # Load fonts used for output on screen ######################################## font_small = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 24) font_medium = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 36) font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 48) ######################################## # Turn on the backlight ######################################## backlight = digitalio.DigitalInOut(board.D22) backlight.switch_to_output() backlight.value = True ######################################## # Watchdog timer for screen on/off control based on button presses # Turns off screen 10s after last button press # Turns on screen for 10s when starting up # Restart fim slideshow when the screen blanks so there is an indicator when it's going to change the selection ######################################## def backlight_callback(): global backlight backlight.value = False sysbus = dbus.SystemBus() systemd1 = sysbus.get_object('org.freedesktop.systemd1', '/org/freedesktop/systemd1') manager = dbus.Interface(systemd1, 'org.freedesktop.systemd1.Manager') job = manager.RestartUnit('fim.service', 'fail') ######################################## # Setup screen off watchdog ######################################## watchdog = Watchdog(10.0, backlight_callback) ######################################## # Figure out which albums are present as well as the special 'All' album ######################################## albums = [ 'All' ] selected_album = 0 try: with open('/etc/default/fim_album_index', 'r') as f: selected_album = int(f.readline()) except IOError: pass except ValueError: pass for root, dirs, files in walklevel('/tank/pictures'): for folder in dirs: if folder == '.stfolder': continue albums.append(folder) albums.sort() ######################################## # Track which UI screen is shown ######################################## current_ui = 'main' ######################################## # Draw main UI ######################################## def draw_main_ui(): global width, height, x, y, top, draw, selected_album, albums, font, current_ui current_ui = 'main' # Split the line on spaces and then add a blank line if needed to avoid crashes text_lines = albums[selected_album].split(' ') if len(text_lines) < 2: text_lines.append('') # Write some text y = top draw.text((x, y), text_lines[0], font=font, fill='#DCDCDC') y += font.getsize('0')[1] draw.text((x, y), text_lines[1], font=font, fill='#DCDCDC') pos = 'Position: %d/%d' % (selected_album + 1, len(albums)) y = height - font_small.getsize(pos)[1] draw.text((x, y), pos, font=font_small, fill='#DCDCDC') # Display image. disp.image(image, rotation) ######################################## # Draw yes/no labels ######################################## def draw_yes_no_labels(): global width, height, x, y, top, draw, font_medium # Yes draw.text((x, top+15), 'Yes', font=font_medium, fill='#DCDCDC') # No y = height - font_medium.getsize('No')[1] draw.text((x, y-15), 'No', font=font_medium, fill='#DCDCDC') # Display image. disp.image(image, rotation) ######################################## # Draw rebootING UI ######################################## def draw_rebooting_ui(): global width, height, x, y, top, draw, font_medium, current_ui current_ui = 'rebooting' font_size = font_medium.getsize('0') left = x + font_size[0] y = (height / 2) - (font_size[1] / 2) draw.text((left, y), 'Rebooting', font=font_medium, fill='#DCDCDC') disp.image(image, rotation) ######################################## # Draw reboot UI ######################################## def draw_reboot_ui(): global width, height, x, y, top, draw, font_medium, current_ui current_ui = 'reboot' draw_yes_no_labels() font_size = font_medium.getsize('Yes ') left = x + font_size[0] y = (height / 2) - (font_size[1] / 2) draw.text((left, y), 'Reboot', font=font_medium, fill='#DCDCDC') disp.image(image, rotation) ######################################## # Draw powerING off UI ######################################## def draw_poweringoff_ui(): global width, height, x, y, top, draw, font_medium, current_ui current_ui = 'poweringoff' font_size = font_medium.getsize('0') left = x + font_size[0] y = (height / 2) - font_size[1] draw.text((left, y), 'Powering', font=font_medium, fill='#DCDCDC') y += font_size[1] left += font_medium.getsize('00')[0] draw.text((left, y), 'Off', font=font_medium, fill='#DCDCDC') disp.image(image, rotation) ######################################## # Draw poweroff UI ######################################## def draw_poweroff_ui(): global width, height, x, y, top, draw, font_medium, current_ui current_ui = 'poweroff' draw_yes_no_labels() font_size = font_medium.getsize('Yes ') left = x + font_size[0] y = (height / 2) - font_size[1] draw.text((left, y), 'Power', font=font_medium, fill='#DCDCDC') y += font_size[1] left += font_medium.getsize('0')[0] draw.text((left, y), 'Off', font=font_medium, fill='#DCDCDC') disp.image(image, rotation) ######################################## # Draw debug UI ######################################## def draw_debug_ui(): global width, height, x, y, top, draw, font_small, current_ui import subprocess current_ui = 'debug' ip_cmd = '/usr/sbin/ip' process = subprocess.run([ip_cmd, '-o', 'link', 'show'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) addresses = [] for line in process.stdout.split('\n'): if line == '': continue values = line.split(':') adapter = values[1].strip() if adapter != 'lo' and not 'wg' in adapter: addr = subprocess.run([ip_cmd, '-f', 'inet', '-o', 'addr', 'show', adapter], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) addr = addr.stdout.split() if len(addr) >=4: addresses.append(addr[3].split('/')[0]) # Write some text lux = 'Light: %.2f Lux' % sensor.lux y = top draw.text((x, y), lux, font=font_small, fill='#DCDCDC') for addr in addresses: y += font_small.getsize('0')[1] draw.text((x, y), addr, font=font_small, fill='#DCDCDC') # Display image. disp.image(image, rotation) ######################################## # Method for drawing on the screen # Called by the button handlers since we don't need anything more than a basic while(1) main for this setup ######################################## def refresh_screen(screen): global draw, width, height # Draw a black filled box to clear the image. draw.rectangle((0, 0, width, height), outline=0, fill=0) if screen == 'main': draw_main_ui() elif screen == 'debug': draw_debug_ui() elif screen == 'reboot': draw_reboot_ui() elif screen == 'poweroff': draw_poweroff_ui() elif screen == 'rebooting': draw_rebooting_ui() elif screen == 'poweringoff': draw_poweringoff_ui() ######################################## # Write current album when restarting the slideshow ######################################## def write_selected_album(): with open('/etc/default/fim_album_index', 'w') as f: f.write(str(selected_album)) with open('/etc/default/fim_album', 'w') as f: if not selected_album == 0: f.write('/tank/pictures/' + albums[selected_album]) else: f.write('/tank/pictures') ######################################## # Button held tracking date/times ######################################## up_held_rising = None dn_held_rising = None btn_is_prompting = False ######################################## # Interrupt callback for changing albums (up) ######################################## def up_button(channel): global selected_album, albums, backlight, up_held_rising, btn_is_prompting, current_ui backlight.value = True # Pull ups on the LCD flip the standard logic you're expecting # Button Released if not GPIO.input(channel): up_held_rising = datetime.now() # Button pressed else: if btn_is_prompting: if current_ui == 'debug': btn_is_prompting = False up_held_rising = None refresh_screen('main') return if current_ui == 'reboot': refresh_screen('rebooting') os.system('/usr/bin/systemctl reboot') elif current_ui == 'poweroff': refresh_screen('poweringoff') os.system('/usr/bin/systemctl poweroff') btn_is_prompting = False return if up_held_rising is not None: now = datetime.now() diff = (now - up_held_rising).total_seconds() if diff >= 3: up_held_rising = None btn_is_prompting = True refresh_screen('reboot') return selected_album += 1 if selected_album >= len(albums): selected_album = 0 refresh_screen('main') write_selected_album() watchdog.reset() ######################################## # Interrupt callback for changing albums (dn) ######################################## def dn_button(channel): global selected_album, albums, backlight, dn_held_rising, btn_is_prompting backlight.value = True # Pull ups on the LCD flip the standard logic you're expecting # Button released if not GPIO.input(channel): dn_held_rising = datetime.now() # Button pressed else: if btn_is_prompting: # Show debug menu if long pressed dn on reboot/shutdown screens if dn_held_rising is not None: now = datetime.now() diff = (now - dn_held_rising).total_seconds() if diff >= 3: dn_held_rising = None btn_is_prompting = True refresh_screen('debug') return btn_is_prompting = False refresh_screen('main') if dn_held_rising is not None: now = datetime.now() diff = (now - dn_held_rising).total_seconds() if diff >= 3: dn_held_rising = None btn_is_prompting = True refresh_screen('poweroff') return selected_album -= 1 if selected_album < 0: selected_album = len(albums) - 1 refresh_screen('main') write_selected_album() watchdog.reset() ######################################## # Setup button interrupts ######################################## GPIO.add_event_detect(23, GPIO.BOTH, callback=up_button, bouncetime=250) GPIO.add_event_detect(24, GPIO.BOTH, callback=dn_button, bouncetime=250) ######################################## # Draw initial screen state ######################################## refresh_screen(current_ui) ######################################## # Kick off watchdog to start the 10s initial 'on' status ######################################## watchdog.reset() ######################################## # We use watchdogs/interrupts for ALL operations, no need for anything in a main loop other than keeping the app alive ######################################## while True: time.sleep(300)