Skip to content

Commit

Permalink
Major fixings (#158)
Browse files Browse the repository at this point in the history
* Add details and better network handling

Network code should now deal more consistently with token issues
and reset the frame data to allow you to authorize again.

Details is a new button for the google photos part which will expose
more info about what content it saw.

* Re-implement freshness logic

Allow individual keywords to be expired if too old.
Moved erasing of indexes into services instead of servicemanager

* Major rewrites

- Event system (not yet in use)
- Improved memory handling (found major bug)
- Streamlined code flow so it happens where it makes sense

* Reworking memory handling and history

* First working version with random and order

Cleaned up API, now random and order works
as expected. Next/Prev is broken and so is
album next/prev.

* Handle error display on frame better

Used to just run full tilt when error shows,
now it refreshes at a normal rate.

* Many fixes

- Uses unique filenames to avoid clashing when only one image is available
- Handles changes to services/keywords better
- Removed dead code
- Removed printouts which clogged the log
- Fixed issue with lingering .cache files filling /tmp
  • Loading branch information
mrworf authored Apr 30, 2020
1 parent 760b875 commit 1c52933
Show file tree
Hide file tree
Showing 25 changed files with 1,132 additions and 479 deletions.
27 changes: 25 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,15 @@ You're done! Reboot your RPi3 (So I2C gets enabled) and from now on, all images

If photoframe is unable to use the sensor, it "usually" gives you helpful hints. Check the `/var/log/syslog` file for `frame.py` entries.

*Note*

The sensor is automatically detected as long as it is a TCS34725 device and it's connected correctly to the I2C bus of the raspberry pi. Once detected you'll get a new read-out in the web interface which details both white balance (kelvin) and light (lux).

If you don't get this read-out, look at your logfile. There will be hints like sensor not found or sensor not being the expected one, etc.

## Annoyed with the LED showing on the TCS34725 board from Adafruit?

Just ground the LED pin (for example by connecting it to Pin 9 on your RPi3)
Just ground the LED pin on the Adafruit board (for example by connecting it to Pin 9 on your RPi3)

## Ambient powersave?

Expand All @@ -94,7 +100,7 @@ regardless of what the sensor says. The sensor is only used to extend the period

# Power on/off?

Photoframe listens to GPIO 26 to power off (and also power on). If you connect a switch between pin 37 (GPIO 26) and pin 39 (GND), you'll be able
Photoframe listens to GPIO 26 (default, can be changed) to power off (and also power on). If you connect a switch between pin 37 (GPIO 26) and pin 39 (GND), you'll be able
to do a graceful shutdown as well as power on.

# How come you contact photoframe.sensenet.nu ???
Expand Down Expand Up @@ -184,3 +190,20 @@ sudo apt install exfat-fuse exfat-utils

After this, you should be able to use exFAT

## How does "Refresh keywords" option work?

By default, most photo providers will fetch a list of available photos for a given keyword. This list isn't refreshed until one of the following events happen:

- No more photos are available from ANY photo provider in your frame
- User presses "Forget Memory" in web UI
- The age of the downloaded index exceeds the hours specified by "Refresh keywords" option

To disable the last item on that list, set the "Refresh keywords" to 0 (zero). This effectively disables this and now the frame will only refresh if no more photos are available or if user presses the forget memory item.

## Why isn't it showing all my photos?

Given you haven't set any options to limit based on orientation or a refresh which is too short to show them all, it should walk through the provided list from your provider.

*however*

Not all content is supported. To help troubleshoot why some content is missing, you can press "Details" for any keyword (given that the provider supports it) and the frame will let you know what content it has found. It should also give you an indication if there's a lot of content which is currently unsupported.
11 changes: 9 additions & 2 deletions frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
from modules.cachemanager import CacheManager
from modules.path import path
from modules.server import WebServer
from modules.events import Events
from modules.history import ImageHistory

# Make sure we run from our own directory
os.chdir(os.path.dirname(sys.argv[0]))
Expand Down Expand Up @@ -69,17 +71,21 @@ def __init__(self, cmdline):
if not path().validate():
sys.exit(255)

self.eventMgr = Events()
self.eventMgr.add('Hello world')

self.cacheMgr = CacheManager()
self.settingsMgr = settings()
self.displayMgr = display(self.emulator)
# Validate all settings, prepopulate with defaults if needed
self.validateSettings()

self.imageHistory = ImageHistory(self.settingsMgr)
self.driverMgr = drivers()
self.serviceMgr = ServiceManager(self.settingsMgr, self.cacheMgr)

self.colormatch = colormatch(self.settingsMgr.get('colortemp-script'), 2700) # 2700K = Soft white, lowest we'll go
self.slideshow = slideshow(self.displayMgr, self.settingsMgr, self.colormatch)
self.slideshow = slideshow(self.displayMgr, self.settingsMgr, self.colormatch, self.imageHistory)
self.timekeeperMgr = timekeeper()
self.timekeeperMgr.registerListener(self.displayMgr.enable)
self.powerMgr = shutdown(self.settingsMgr.getUser('shutdown-pin'))
Expand Down Expand Up @@ -130,11 +136,12 @@ def setupWebserver(self, listen, port):
self._loadRoute('orientation', 'RouteOrientation', self.cacheMgr)
self._loadRoute('overscan', 'RouteOverscan', self.cacheMgr)
self._loadRoute('maintenance', 'RouteMaintenance', self.emulator, self.driverMgr, self.slideshow)
self._loadRoute('details', 'RouteDetails', self.displayMgr, self.driverMgr, self.colormatch, self.slideshow)
self._loadRoute('details', 'RouteDetails', self.displayMgr, self.driverMgr, self.colormatch, self.slideshow, self.serviceMgr, self.settingsMgr)
self._loadRoute('upload', 'RouteUpload', self.settingsMgr, self.driverMgr)
self._loadRoute('oauthlink', 'RouteOAuthLink', self.serviceMgr, self.slideshow)
self._loadRoute('service', 'RouteService', self.serviceMgr, self.slideshow)
self._loadRoute('control', 'RouteControl', self.slideshow)
self._loadRoute('events', 'RouteEvents', self.eventMgr)

def validateSettings(self):
if not self.settingsMgr.load():
Expand Down
2 changes: 1 addition & 1 deletion modules/cachemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def __init__(self):

def enableCache(self, enable):
self.enable = enable
logging.info('Cache is set to %s' + repr(enable))
logging.info('Cache is set to ' + repr(enable))

def validate(self):
self.createDirs()
Expand Down
3 changes: 3 additions & 0 deletions modules/colormatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ def adjust(self, filename, filenameTemp, temperature=None):

try:
result = subprocess.call([self.script, '-t', "%d" % temperature, filename + '[0]', filenameTemp], stderr=self.void) == 0
if os.path.exists(filenameTemp + '.cache'):
logging.warning('colormatch called without filename extension, lingering .cache file will stay behind')

return result
except:
logging.exception('Unable to run %s:', self.script)
Expand Down
4 changes: 2 additions & 2 deletions modules/debug.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ def _stringify(args):
return result.replace('\n', '\\n')

def subprocess_call(cmds, stderr=None, stdout=None):
logging.debug('subprocess.call(%s)', _stringify(cmds))
#logging.debug('subprocess.call(%s)', _stringify(cmds))
return subprocess.call(cmds, stderr=stderr, stdout=stdout)

def subprocess_check_output(cmds, stderr=None):
logging.debug('subprocess.check_output(%s)', _stringify(cmds))
#logging.debug('subprocess.check_output(%s)', _stringify(cmds))
return subprocess.check_output(cmds, stderr=stderr)

def stacktrace():
Expand Down
44 changes: 44 additions & 0 deletions modules/dedupe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# This file is part of photoframe (https://github.com/mrworf/photoframe).
#
# photoframe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# photoframe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with photoframe. If not, see <http://www.gnu.org/licenses/>.
#

class DedupeManager:
def __init__(self, memoryLocation):
try:
from PIL import Image
import imagehash
self.hasImageHash = True
logging.info('ImageHash functionality is available')
except:
self.hasImageHash = False
logging.info('ImageHash functionality is unavailable')

def _hamming_distance(self, i1, i2):
x = i1 ^ i2
setBits = 0

while (x > 0):
setBits += x & 1
x >>= 1

return setBits

def _hamming(self, s1, s2):
h = 0
for i in range(0, len(s1)/2):
i1 = int(s1[i*2:i*2+2], 16)
i2 = int(s2[i*2:i*2+2], 16)
h += self._hamming_distance(i1, i2)
return h
58 changes: 58 additions & 0 deletions modules/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# This file is part of photoframe (https://github.com/mrworf/photoframe).
#
# photoframe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# photoframe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with photoframe. If not, see <http://www.gnu.org/licenses/>.
#

class Events:
TYPE_PERSIST = 1
TYPE_NORMAL = 0

LEVEL_INFO = 0
LEVEL_WARN = 1
LEVEL_ERR = 2
LEVEL_DEBUG = 3

def __init__(self):
self.idcount = 0
self.msgs = []

def add(self, message, unique=None, link=None, level=LEVEL_INFO, type=TYPE_NORMAL):
record = {'id': self.idcount, 'unique' : unique, 'type' : type, 'level' : level, 'message' : message, 'link' : link}
if unique is not None:
unique = repr(unique) # Make it a string to be safe
for i in range(0, len(self.msgs)):
if self.msgs[i]['unique'] == unique:
self.msgs[i] = record
record = None
break

if record is not None:
self.msgs.append(record)
self.idcount += 1

def remove(self, id):
for i in range(0, len(self.msgs)):
if self.msgs[i]['id'] == id and self.msgs[i]['type'] != Events.TYPE_PERSIST:
self.msgs.pop(i)
break

def getAll(self):
return self.msgs

def getSince(self, id):
ret = []
for msg in self.msgs:
if msg['id'] > id:
ret.append(msg)
return ret
31 changes: 22 additions & 9 deletions modules/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
class helper:
TOOL_ROTATE = '/usr/bin/jpegtran'

MIMETYPES = {
'image/jpeg' : 'jpg',
'image/png' : 'png',
'image/gif' : 'gif',
'image/bmp' : 'bmp'
# HEIF to be added once I get ImageMagick running with support
}


@staticmethod
def isValidUrl(url):
# Catches most invalid URLs
Expand Down Expand Up @@ -94,18 +103,18 @@ def _checkNetwork():

@staticmethod
def getExtension(mime):
mapping = {
'image/jpeg' : 'jpg',
'image/png' : 'png',
'image/gif' : 'gif',
'image/x-adobe-dng' : 'dng',
'image/bmp' : 'bmp'
}
mime = mime.lower()
if mime in mapping:
return mapping[mime]
if mime in helper.MIMETYPES:
return helper.MIMETYPES[mime]
return None

@staticmethod
def getSupportedTypes():
ret = []
for i in helper.MIMETYPES:
ret.append(i)
return ret

@staticmethod
def getMimetype(filename):
if not os.path.isfile(filename):
Expand Down Expand Up @@ -208,6 +217,10 @@ def makeFullframe(filename, displayWidth, displayHeight, zoomOnly=False, autoCho

logging.debug('Size of image is %dx%d, screen is %dx%d. New size is %dx%d', width, height, displayWidth, displayHeight, adjWidth, adjHeight)

if width < 100 or height < 100:
logging.error('Image size is REALLY small, please check "%s" ... something isn\'t right', filename)
#a=1/0

if adjHeight < displayHeight:
border = '0x%d' % width_border
spacing = '0x%d' % width_spacing
Expand Down
75 changes: 75 additions & 0 deletions modules/history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# This file is part of photoframe (https://github.com/mrworf/photoframe).
#
# photoframe is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# photoframe is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with photoframe. If not, see <http://www.gnu.org/licenses/>.
#

import logging
import os
import shutil

from modules.path import path as syspath

class ImageHistory:
MAX_HISTORY = 20
def __init__(self, settings):
self._HISTORY = []
self.settings = settings

# Also, make sure folder exists AND clear any existing content
if not os.path.exists(syspath.HISTORYFOLDER):
os.mkdir(syspath.HISTORYFOLDER)

# Clear it
for p, _dirs, files in os.walk(syspath.HISTORYFOLDER):
for filename in [os.path.join(p, f) for f in files]:
try:
os.unlink(filename)
except:
logging.exception('Failed to delete "%s"' % filename)

def _find(self, file):
return next((entry for entry in self._HISTORY if entry.filename == file), None)

def add(self, image):
if image is None or image.error is not None:
return
historyFile = os.path.join(syspath.HISTORYFOLDER, image.getCacheId())
if not self._find(historyFile):
shutil.copy(image.filename, historyFile)
h = image.copy()
h.setFilename(historyFile)
h.allowCache(False)

self._HISTORY.insert(0, h)
self._obeyLimits()

def _obeyLimits(self):
# Make sure history isn't too big
while len(self._HISTORY) > ImageHistory.MAX_HISTORY:
entry = self._HISTORY.pop()
if not self._find(entry.filename):
os.unlink(entry.filename)

def getAvailable(self):
return len(self._HISTORY)

def getByIndex(self, index):
if index < 0 or index >= len(self._HISTORY):
logging.warning('History index requested is out of bounds (%d wanted, have 0-%d)', index, len(self._HISTORY)-1)
return None
entry = self._HISTORY[index]
# We need to make a copy which is safe to delete!
f = os.path.join(self.settings.get('tempfolder'), 'history')
shutil.copy(entry.filename, f)
return entry.copy().setFilename(f)
Loading

0 comments on commit 1c52933

Please sign in to comment.