piframe/lcd_control.py

487 lines
16 KiB
Python
Raw Normal View History

2020-08-16 05:54:36 +00:00
#!/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
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
########################################
2020-08-16 05:54:36 +00:00
# Inspiration / Further Reading
########################################
2020-08-16 05:54:36 +00:00
# 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
2020-08-16 05:54:36 +00:00
import board
import busio
import dbus
import digitalio
import os
import RPi.GPIO as GPIO
2020-08-16 05:54:36 +00:00
import time
from datetime import datetime
2020-08-16 05:54:36 +00:00
from PIL import Image, ImageDraw, ImageFont
from threading import Timer
2020-08-16 05:54:36 +00:00
########################################
# Generic watchdog timer class
########################################
2020-08-16 05:54:36 +00:00
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
########################################
2020-08-16 05:54:36 +00:00
# Cheat for walking only one level deep via os.walk (faster than other options)
########################################
2020-08-16 05:54:36 +00:00
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[:]
########################################
2020-08-16 05:54:36 +00:00
# bh1740 lux sensor
########################################
2020-08-16 05:54:36 +00:00
i2c = busio.I2C(board.SCL, board.SDA)
sensor = adafruit_bh1750.BH1750(i2c)
########################################
2020-08-16 05:54:36 +00:00
# Configuration for CS and DC pins (these are FeatherWing defaults on M0/M4):
########################################
2020-08-16 05:54:36 +00:00
cs_pin = digitalio.DigitalInOut(board.CE0)
dc_pin = digitalio.DigitalInOut(board.D25)
reset_pin = None
########################################
2020-08-16 05:54:36 +00:00
# Setup button pins as inputs
########################################
2020-08-16 05:54:36 +00:00
GPIO.setup(23, GPIO.IN)
GPIO.setup(24, GPIO.IN)
########################################
# Config for display baudrate (default max is 24mhz)
########################################
2020-08-16 05:54:36 +00:00
BAUDRATE = 64000000
########################################
# Setup SPI bus using hardware SPI
########################################
2020-08-16 05:54:36 +00:00
spi = board.SPI()
########################################
# Create the ST7789 display
########################################
2020-08-16 05:54:36 +00:00
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,
)
########################################
2020-08-16 05:54:36 +00:00
# Create blank image for drawing.
# Make sure to create image with mode 'RGB' for full color.
########################################
2020-08-16 05:54:36 +00:00
height = disp.width # we swap height/width to rotate it to landscape!
width = disp.height
image = Image.new('RGB', (width, height))
rotation = 90
########################################
2020-08-16 05:54:36 +00:00
# Get drawing object to draw on image.
########################################
2020-08-16 05:54:36 +00:00
draw = ImageDraw.Draw(image)
########################################
# Draw a black filled box to clear the image
########################################
2020-08-16 05:54:36 +00:00
draw.rectangle((0, 0, width, height), outline=0, fill=(0, 0, 0))
disp.image(image, rotation)
########################################
# Draw some shapes
2020-08-16 05:54:36 +00:00
# First define some constants to allow easy resizing of shapes.
########################################
2020-08-16 05:54:36 +00:00
padding = -2
top = padding
bottom = height - padding
########################################
2020-08-16 05:54:36 +00:00
# Move left to right keeping track of the current x position for drawing shapes.
########################################
2020-08-16 05:54:36 +00:00
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)
2020-08-16 05:54:36 +00:00
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSansMono-Bold.ttf', 48)
########################################
2020-08-16 05:54:36 +00:00
# Turn on the backlight
########################################
2020-08-16 05:54:36 +00:00
backlight = digitalio.DigitalInOut(board.D22)
backlight.switch_to_output()
backlight.value = True
########################################
2020-08-16 05:54:36 +00:00
# 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
########################################
2020-08-16 05:54:36 +00:00
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
########################################
2020-08-16 05:54:36 +00:00
watchdog = Watchdog(10.0, backlight_callback)
########################################
2020-08-16 05:54:36 +00:00
# Figure out which albums are present as well as the special 'All' album
########################################
2020-08-16 05:54:36 +00:00
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'
2020-08-16 05:54:36 +00:00
########################################
# Draw main UI
########################################
def draw_main_ui():
global width, height, x, y, top, draw, selected_album, albums, font, current_ui
current_ui = 'main'
2020-08-16 05:54:36 +00:00
# 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
2020-08-16 05:54:36 +00:00
lux = 'Light: %.2f Lux' % sensor.lux
y = top
2020-08-16 05:54:36 +00:00
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')
2020-08-16 05:54:36 +00:00
# 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()
########################################
2020-08-16 05:54:36 +00:00
# Write current album when restarting the slideshow
########################################
2020-08-16 05:54:36 +00:00
def write_selected_album():
with open('/etc/default/fim_album_index', 'w') as f:
2020-08-16 05:54:36 +00:00
f.write(str(selected_album))
with open('/etc/default/fim_album', 'w') as f:
2020-08-16 05:54:36 +00:00
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
########################################
2020-08-16 05:54:36 +00:00
# Interrupt callback for changing albums (up)
########################################
2020-08-16 05:54:36 +00:00
def up_button(channel):
global selected_album, albums, backlight, up_held_rising, btn_is_prompting, current_ui
2020-08-16 05:54:36 +00:00
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()
########################################
2020-08-16 05:54:36 +00:00
# Interrupt callback for changing albums (dn)
########################################
2020-08-16 05:54:36 +00:00
def dn_button(channel):
global selected_album, albums, backlight, dn_held_rising, btn_is_prompting
2020-08-16 05:54:36 +00:00
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()
2020-08-16 05:54:36 +00:00
########################################
2020-08-16 05:54:36 +00:00
# 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)
2020-08-16 05:54:36 +00:00
########################################
2020-08-16 05:54:36 +00:00
# Draw initial screen state
########################################
refresh_screen(current_ui)
2020-08-16 05:54:36 +00:00
########################################
2020-08-16 05:54:36 +00:00
# Kick off watchdog to start the 10s initial 'on' status
########################################
2020-08-16 05:54:36 +00:00
watchdog.reset()
########################################
# We use watchdogs/interrupts for ALL operations, no need for anything in a main loop other than keeping the app alive
########################################
2020-08-16 05:54:36 +00:00
while True:
time.sleep(300)