Compare commits

...

1 commit

Author SHA1 Message Date
Ubuntu 27c75c7a2d Move to go for secondary services we have built specifically for piframe 2020-08-27 20:26:56 -04:00
6 changed files with 9 additions and 660 deletions

View file

@ -6,9 +6,9 @@ Can be used for setup and/or re-configuring WiFi if something changes.
``` sh
git clone https://git.kemonine.info/PiFrame/piframe.git /opt/piframe
cp /opt/piframe/wifi_setup.py /usr/local/bin
chmod a+x /usr/local/bin/wifi_setup.py
DL_URL=$(curl https://git.kemonine.info/api/v1/repos/PiFrame/piframe-go/releases | jq -r '.[0].assets[] | select(.name == "wifi") | .browser_download_url')
wget $DL_URL -O /usr/local/bin/pf-wifi
chmod a+x /usr/local/bin/pf-wifi
cat > /etc/systemd/system/wifi_setup.service <<EOF
[Unit]
@ -17,7 +17,7 @@ Description=Automatic configuration of WiFi on boot
[Service]
User=root
PrivateTmp=true
ExecStart=/usr/local/bin/wifi_setup.py
ExecStart=/usr/local/bin/pf-wifi
[Install]
WantedBy=multi-user.target

View file

@ -1,39 +0,0 @@
# LCD Control
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
pip3 install adafruit-circuitpython-rgb-display adafruit-circuitpython-bh1750 RPI.GPIO
pip3 install --upgrade --force-reinstall spidev
git clone https://git.kemonine.info/kemonine/piframe.git /opt/piframe
cp /opt/piframe/lcd_control.py /usr/local/bin/
chmod a+x /usr/local/bin/lcd_control.py
cat > /etc/systemd/system/lcd_control.service <<EOF
[Unit]
Description=Control which album is shown as a slideshow
After=fim.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/lcd_control.py
TimeoutSec=15
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now lcd_control
```

View file

@ -225,9 +225,10 @@ EOF
systemctl daemon-reload
systemctl enable --now no-cursor-tty1
systemctl enable --now fim
git clone https://git.kemonine.info/kemonine/piframe.git /opt/piframe
cp /opt/piframe/inotify-pictures.py /usr/local/bin/
chmod a+x /usr/local/bin/inotify-pictures.py
DL_URL=$(curl https://git.kemonine.info/api/v1/repos/PiFrame/piframe-go/releases | jq -r '.[0].assets[] | select(.name == "inotify") | .browser_download_url')
wget $DL_URL -O /usr/local/bin/pf-inotify
chmod a+x /usr/local/bin/pf-inotify
cat > /etc/systemd/system/inotify-pictures.service <<EOF
[Unit]
Description=Watch for picture folder changes and restart slideshow
@ -236,7 +237,7 @@ After=fim.service
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/inotify-pictures.py
ExecStart=/usr/local/bin/pf-inotify
TimeoutSec=15
Restart=always

View file

@ -1,71 +0,0 @@
#!/usr/bin/env python3
####################
# Inspiration / Further Reading
####################
# https://github.com/chrisjbillington/inotify_simple
# https://stackoverflow.com/questions/16148735/how-to-implement-a-watchdog-timer-in-python
####################
# Dependencies
####################
# apt install python3 python3-pip python3-dbus
# pip3 install inotify_simple
from inotify_simple import INotify, flags
import os
from threading import Timer
####################
# Simple 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
####################
# Setup inotify on folder(s)
####################
inotify = INotify()
watch_flags = flags.CREATE | flags.DELETE
wd = inotify.add_watch('/tank/pictures', watch_flags)
####################
# Restart fim slideshow once inotify storm is over (3s timeout)
####################
def inotify_handler():
print('restarting fim slideshow')
import dbus
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')
####################
# Watch for inotify events in an infinate loop
# This will BLOCK waiting for events within inotify.read(None)
# This makes it pretty safe for an infinate loop scenario
####################
watchdog = Watchdog(3.0, inotify_handler)
while True:
for event in inotify.read(None):
print(event)
watchdog.reset()
for flag in flags.from_mask(event.mask):
print(' ' + str(flag))

View file

@ -1,486 +0,0 @@
#!/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)

View file

@ -1,56 +0,0 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
import os
import subprocess
# Make sure mount point exists
if not os.path.isdir('/var/wifi'):
os.mkdir('/var/wifi')
# Scan unmounted disks/partitions to see if we need to reconfig wifi
blkids = subprocess.run(['/usr/sbin/blkid', '-o', 'device'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
for blkid in blkids.stdout.split('\n'):
blkid = blkid.strip()
if not blkid:
continue
# Verify the disk/partition isn't mounted
findmnt = subprocess.run(['/usr/bin/findmnt', '-n', '-o', 'TARGET', blkid], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
mnt = findmnt.stdout.strip()
if not mnt and not 'zram' in blkid:
print('checking ' + blkid)
try:
# Mount the disk
subprocess.run(['/usr/bin/mount', blkid, '/var/wifi/'], universal_newlines=True)
# Check if the wifi.txt confg file is present (line 1 is essid, line 2 is PSK)
if os.path.exists('/var/wifi/wifi.txt'):
# Get wifi config
essid = ''
password = ''
with open('/var/wifi/wifi.txt', 'r') as f:
essid = f.readline().strip()
password = f.readline().strip()
if essid and password:
print('Found wifi config (' + essid + ' / ' + password + ')')
# Look for existing wifi connections that should be cleaned up
nmcli = subprocess.run(['/usr/bin/nmcli', '-t', 'connection', 'show'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
for connection in nmcli.stdout.split('\n'):
# Ignore empty lines in output
if not connection:
continue
connection_details = connection.split(':')
if connection_details[2] == '802-11-wireless':
# If we cannot clean up a wifi connection assume it's a non-issue
try:
# Cleanup existing wifi connection
print('cleaning up wifi connection ' + connection_details[0])
subprocess.run(['/usr/bin/nmcli', 'connection', 'del', connection_details[1]], universal_newlines=True)
except:
pass
print('Connecting to ' + essid + ' with password ' + password)
subprocess.run(['/usr/bin/nmcli', 'd', 'wifi', 'connect', essid, 'password', password], universal_newlines=True)
# Unmount disk so it's safe to remove
subprocess.run(['/usr/bin/umount', '/var/wifi/'], universal_newlines=True)
except:
# Unmount disk so it's safe to remove
subprocess.run(['/usr/bin/umount', '/var/wifi/'], universal_newlines=True)