piframe/lcd_control.py

487 lines
16 KiB
Python

#!/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)