Add reboot, poweroff, debug screens to lcd_control

This commit is contained in:
KemoNine 2020-08-18 19:00:17 -04:00
parent a243b2c8fb
commit 06e43b3648
2 changed files with 316 additions and 46 deletions

View File

@ -2,6 +2,12 @@
The below will setup an Adafruit mini LCD with 2 buttons as a controller for selecting which photo albums are displayed on a PiFrame. This setup also includes the Adafruit stemma/qwiic LUX sensor setup which will be integrated further in the future.
Please note:
- Single button presses need about 1/4 of a second to register
- Holding 'Up' for at least 3 seconds will trigger a prompt to reboot
- Holding 'Down' for at least 3 seconds will trigger a prompt to power off
- If you hold 'Down' on the reboot or power off prompts a debug menu will be displayed
``` sh
apt install -y python3-pip ttf-dejavu python3-pil python3-numpy

View File

@ -1,28 +1,40 @@
#!/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 time
from threading import Timer
from PIL import Image, ImageDraw, ImageFont
import RPi.GPIO as GPIO
import adafruit_rgb_display.st7789 as st7789
import adafruit_bh1750
import time
from datetime import datetime
from PIL import Image, ImageDraw, ImageFont
from threading import Timer
# Generic watchdog timer used for backlight controls
########################################
# Generic watchdog timer class
########################################
class Watchdog:
def __init__(self, timeout, userHandler=None): # timeout in seconds
self.timeout = timeout
@ -43,7 +55,9 @@ class Watchdog:
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)
@ -54,26 +68,38 @@ def walklevel(some_dir, level=1):
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):
########################################
# Config for display baudrate (default max is 24mhz)
########################################
BAUDRATE = 64000000
# Setup SPI bus using hardware SPI:
########################################
# Setup SPI bus using hardware SPI
########################################
spi = board.SPI()
# Create the ST7789 display:
########################################
# Create the ST7789 display
########################################
disp = st7789.ST7789(
spi,
cs=cs_pin,
@ -86,43 +112,59 @@ disp = st7789.ST7789(
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 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.
########################################
# 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
# Alternatively load a TTF font. Make sure the .ttf font file is in the
# same directory as the python script!
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
#font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 24)
font_small = font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 24)
########################################
# 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
@ -131,9 +173,14 @@ def backlight_callback():
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:
@ -150,13 +197,17 @@ for root, dirs, files in walklevel('/tank/pictures'):
albums.append(folder)
albums.sort()
# 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():
global width, height, x, y, top, draw, selected_album, albums, font, font_small
########################################
# Track which UI screen is shown
########################################
current_ui = 'main'
# Draw a black filled box to clear the image.
draw.rectangle((0, 0, width, height), outline=0, fill=0)
########################################
# 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(' ')
@ -168,14 +219,154 @@ def refresh_screen():
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')
lux = 'Light: %.2f Lux' % sensor.lux
y = height - font_small.getsize(lux)[1]
draw.text((x, y), lux, font=font_small, 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))
@ -185,38 +376,111 @@ def write_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
global selected_album, albums, backlight, up_held_rising, btn_is_prompting, current_ui
backlight.value = True
selected_album += 1
if selected_album >= len(albums):
selected_album = 0
refresh_screen()
write_selected_album()
watchdog.reset()
# 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
global selected_album, albums, backlight, dn_held_rising, btn_is_prompting
backlight.value = True
selected_album -= 1
if selected_album < 0:
selected_album = len(albums) - 1
refresh_screen()
write_selected_album()
watchdog.reset()
# 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.FALLING, callback=up_button, bouncetime=300)
GPIO.add_event_detect(24, GPIO.FALLING, callback=dn_button, bouncetime=300)
########################################
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()
########################################
refresh_screen(current_ui)
########################################
# Kick off watchdog to start the 10s initial 'on' status
########################################
watchdog.reset()
# Work with the LCD / Buttons in an infinate loop
########################################
# 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)