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. 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 ``` sh
apt install -y python3-pip ttf-dejavu python3-pil python3-numpy apt install -y python3-pip ttf-dejavu python3-pil python3-numpy

View file

@ -1,28 +1,40 @@
#!/usr/bin/python3 #!/usr/bin/python3
# -*- coding: utf-8 -*- # -*- 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 # Inspiration / Further Reading
#################### ########################################
# https://circuitpython.readthedocs.io/projects/bh1750/en/latest/ # 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://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://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-bh1750-ambient-light-sensor
# https://learn.adafruit.com/adafruit-mini-pitft-135x240-color-tft-add-on-for-raspberry-pi/overview # 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 board
import busio import busio
import dbus import dbus
import digitalio import digitalio
import os import os
import time
from threading import Timer
from PIL import Image, ImageDraw, ImageFont
import RPi.GPIO as GPIO import RPi.GPIO as GPIO
import adafruit_rgb_display.st7789 as st7789 import time
import adafruit_bh1750 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: class Watchdog:
def __init__(self, timeout, userHandler=None): # timeout in seconds def __init__(self, timeout, userHandler=None): # timeout in seconds
self.timeout = timeout self.timeout = timeout
@ -43,7 +55,9 @@ class Watchdog:
def defaultHandler(self): def defaultHandler(self):
raise self raise self
########################################
# Cheat for walking only one level deep via os.walk (faster than other options) # Cheat for walking only one level deep via os.walk (faster than other options)
########################################
def walklevel(some_dir, level=1): def walklevel(some_dir, level=1):
some_dir = some_dir.rstrip(os.path.sep) some_dir = some_dir.rstrip(os.path.sep)
assert os.path.isdir(some_dir) assert os.path.isdir(some_dir)
@ -54,26 +68,38 @@ def walklevel(some_dir, level=1):
if num_sep + level <= num_sep_this: if num_sep + level <= num_sep_this:
del dirs[:] del dirs[:]
########################################
# bh1740 lux sensor # bh1740 lux sensor
########################################
i2c = busio.I2C(board.SCL, board.SDA) i2c = busio.I2C(board.SCL, board.SDA)
sensor = adafruit_bh1750.BH1750(i2c) sensor = adafruit_bh1750.BH1750(i2c)
########################################
# Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4): # Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4):
########################################
cs_pin = digitalio.DigitalInOut(board.CE0) cs_pin = digitalio.DigitalInOut(board.CE0)
dc_pin = digitalio.DigitalInOut(board.D25) dc_pin = digitalio.DigitalInOut(board.D25)
reset_pin = None reset_pin = None
########################################
# Setup button pins as inputs # Setup button pins as inputs
########################################
GPIO.setup(23, GPIO.IN) GPIO.setup(23, GPIO.IN)
GPIO.setup(24, 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 BAUDRATE = 64000000
# Setup SPI bus using hardware SPI: ########################################
# Setup SPI bus using hardware SPI
########################################
spi = board.SPI() spi = board.SPI()
# Create the ST7789 display: ########################################
# Create the ST7789 display
########################################
disp = st7789.ST7789( disp = st7789.ST7789(
spi, spi,
cs=cs_pin, cs=cs_pin,
@ -86,43 +112,59 @@ disp = st7789.ST7789(
y_offset=40, y_offset=40,
) )
########################################
# Create blank image for drawing. # Create blank image for drawing.
# Make sure to create image with mode 'RGB' for full color. # Make sure to create image with mode 'RGB' for full color.
########################################
height = disp.width # we swap height/width to rotate it to landscape! height = disp.width # we swap height/width to rotate it to landscape!
width = disp.height width = disp.height
image = Image.new('RGB', (width, height)) image = Image.new('RGB', (width, height))
rotation = 90 rotation = 90
########################################
# Get drawing object to draw on image. # Get drawing object to draw on image.
########################################
draw = ImageDraw.Draw(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)) draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
disp.image(image, rotation) disp.image(image, rotation)
# Draw some shapes.
########################################
# Draw some shapes
# First define some constants to allow easy resizing of shapes. # First define some constants to allow easy resizing of shapes.
########################################
padding = -2 padding = -2
top = padding top = padding
bottom = height - padding bottom = height - padding
########################################
# Move left to right keeping track of the current x position for drawing shapes. # Move left to right keeping track of the current x position for drawing shapes.
########################################
x = 0 x = 0
# Alternatively load a TTF font. Make sure the .ttf font file is in the ########################################
# same directory as the python script! # Load fonts used for output on screen
# 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 = 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) 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) font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 48)
########################################
# Turn on the backlight # Turn on the backlight
########################################
backlight = digitalio.DigitalInOut(board.D22) backlight = digitalio.DigitalInOut(board.D22)
backlight.switch_to_output() backlight.switch_to_output()
backlight.value = True backlight.value = True
########################################
# Watchdog timer for screen on/off control based on button presses # Watchdog timer for screen on/off control based on button presses
# Turns off screen 10s after last button press # Turns off screen 10s after last button press
# Turns on screen for 10s when starting up # 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 # Restart fim slideshow when the screen blanks so there is an indicator when it's going to change the selection
########################################
def backlight_callback(): def backlight_callback():
global backlight global backlight
backlight.value = False backlight.value = False
@ -131,9 +173,14 @@ def backlight_callback():
manager = dbus.Interface(systemd1, 'org.freedesktop.systemd1.Manager') manager = dbus.Interface(systemd1, 'org.freedesktop.systemd1.Manager')
job = manager.RestartUnit('fim.service', 'fail') job = manager.RestartUnit('fim.service', 'fail')
########################################
# Setup screen off watchdog
########################################
watchdog = Watchdog(10.0, backlight_callback) watchdog = Watchdog(10.0, backlight_callback)
########################################
# Figure out which albums are present as well as the special 'All' album # Figure out which albums are present as well as the special 'All' album
########################################
albums = [ 'All' ] albums = [ 'All' ]
selected_album = 0 selected_album = 0
try: try:
@ -150,13 +197,17 @@ for root, dirs, files in walklevel('/tank/pictures'):
albums.append(folder) albums.append(folder)
albums.sort() 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 # Track which UI screen is shown
def refresh_screen(): ########################################
global width, height, x, y, top, draw, selected_album, albums, font, font_small 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 # Split the line on spaces and then add a blank line if needed to avoid crashes
text_lines = albums[selected_album].split(' ') text_lines = albums[selected_album].split(' ')
@ -168,14 +219,154 @@ def refresh_screen():
draw.text((x, y), text_lines[0], font=font, fill='#DCDCDC') draw.text((x, y), text_lines[0], font=font, fill='#DCDCDC')
y += font.getsize('0')[1] y += font.getsize('0')[1]
draw.text((x, y), text_lines[1], font=font, fill='#DCDCDC') draw.text((x, y), text_lines[1], font=font, fill='#DCDCDC')
lux = 'Light: %.2f Lux' % sensor.lux pos = 'Position: %d/%d' % (selected_album + 1, len(albums))
y = height - font_small.getsize(lux)[1] y = height - font_small.getsize(pos)[1]
draw.text((x, y), lux, font=font_small, fill='#DCDCDC') draw.text((x, y), pos, font=font_small, fill='#DCDCDC')
# Display image. # Display image.
disp.image(image, rotation) 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 # Write current album when restarting the slideshow
########################################
def write_selected_album(): def write_selected_album():
with open('/etc/default/fim_album_index', 'w') as f: with open('/etc/default/fim_album_index', 'w') as f:
f.write(str(selected_album)) f.write(str(selected_album))
@ -185,38 +376,111 @@ def write_selected_album():
else: else:
f.write('/tank/pictures') 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) # Interrupt callback for changing albums (up)
########################################
def up_button(channel): 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 backlight.value = True
selected_album += 1 # Pull ups on the LCD flip the standard logic you're expecting
if selected_album >= len(albums): # Button Released
selected_album = 0 if not GPIO.input(channel):
refresh_screen() up_held_rising = datetime.now()
write_selected_album() # Button pressed
watchdog.reset() 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) # Interrupt callback for changing albums (dn)
########################################
def dn_button(channel): def dn_button(channel):
global selected_album, albums, backlight global selected_album, albums, backlight, dn_held_rising, btn_is_prompting
backlight.value = True backlight.value = True
selected_album -= 1 # Pull ups on the LCD flip the standard logic you're expecting
if selected_album < 0: # Button released
selected_album = len(albums) - 1 if not GPIO.input(channel):
refresh_screen() dn_held_rising = datetime.now()
write_selected_album() # Button pressed
watchdog.reset() 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 # 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 # Draw initial screen state
refresh_screen() ########################################
refresh_screen(current_ui)
########################################
# Kick off watchdog to start the 10s initial 'on' status # Kick off watchdog to start the 10s initial 'on' status
########################################
watchdog.reset() 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: while True:
time.sleep(300) time.sleep(300)