From 60e4e4f9e9367555548c2620698db5e26ec6cb55 Mon Sep 17 00:00:00 2001 From: Henric Andersson Date: Tue, 19 Jun 2018 12:57:54 -0700 Subject: [PATCH] Internal display support * Issues with power management - Was unable to change off time due to copy'n'paste - Was unable to turn off scheduling * Initial version of RGB888<->RGB565 conversion tool * Initial change to support non-HDMI displays * Reframing limits Reframing should not happen if zoomed area is smaller than 20px on each side of the frame. Looks silly. * Major rewrite to tvservice handling No longer deals with pixels, uses tv service string to deduce all values, avoids trying to use non-existant resolution if TV is unplugged. * Handle lack of ANY display photoframe should not crash if a display is missing, instead UX should show the issue at hand. * Stop exception when no colorsensor is available * Handle broken json from google Sometimes downloads fail and the JSON is broken, which we now deal with, but the rest of the code doesn't. This fixes the issue. * Initial support for uploading drivers Also enables listing and activating them * Added support for uploading drivers * Better handling of no display at all * Reboot/power off now works as expcted Turns out that replacing the HTML during async call will cancel out that call. Now we just replace the body instead of the document. * Add driver documentation * Removed usage of old setting name resolution is gone, replaced with tvservice * Relocate all config files into separate folder * Corrected README.md since it used old paths Also clarified the need for chmod --- README.md | 4 +- display-drivers/README.md | 93 ++ display-drivers/waveshare35b.zip | Bin 0 -> 1619 bytes frame.py | 104 +- modules/colormatch.py | 6 +- modules/display.py | 236 +++- modules/drivers.py | 246 ++++ modules/helper.py | 12 +- modules/settings.py | 13 +- modules/slideshow.py | 3 + modules/timekeeper.py | 2 +- rgb565/README.md | 3 + rgb565/rgb565 | Bin 0 -> 8392 bytes rgb565/rgb565.c | 95 ++ static/{ => css}/index.css | 0 static/index.html | 11 +- static/{ => js}/handlebars-v4.0.11.js | 0 static/{ => js}/index.js | 14 +- static/{ => js}/jquery-3.3.1.min.js | 0 static/js/jquery.fileupload.js | 1486 +++++++++++++++++++++++++ static/js/jquery.iframe-transport.js | 224 ++++ static/js/jquery.ui.widget.js | 748 +++++++++++++ static/template/main.html | 88 +- update.sh | 9 + 24 files changed, 3267 insertions(+), 130 deletions(-) create mode 100644 display-drivers/README.md create mode 100644 display-drivers/waveshare35b.zip create mode 100644 modules/drivers.py create mode 100644 rgb565/README.md create mode 100755 rgb565/rgb565 create mode 100644 rgb565/rgb565.c rename static/{ => css}/index.css (100%) rename static/{ => js}/handlebars-v4.0.11.js (100%) rename static/{ => js}/index.js (96%) rename static/{ => js}/jquery-3.3.1.min.js (100%) create mode 100644 static/js/jquery.fileupload.js create mode 100644 static/js/jquery.iframe-transport.js create mode 100644 static/js/jquery.ui.widget.js diff --git a/README.md b/README.md index 517da8c..9ffd1f0 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,9 @@ GND -> Pin 9 (GND) You also need to tell your RPi3 to enable the I2C bus, start the `raspi-config` and go to submenu 5 (interfaces) and select I2C and enable it. Once all this is done, you have one more thing left to do before rebooting, you need to download the imagemagick script that will adjust the image, -please visit http://www.fmwconcepts.com/imagemagick/colortemp/index.php and download and store it as `colortemp.sh` inside `/root/`. +please visit http://www.fmwconcepts.com/imagemagick/colortemp/index.php and download and store it as `colortemp.sh` inside `/root/photoframe_config`. + +Don't forget to make it executable by `chmod +x /root/photoframe_config/colortemp.sh` or it will still not work. You're done! Reboot your RPi3 (So I2C gets enabled) and from now on, all images will get adjusted to match the ambient color temperature. diff --git a/display-drivers/README.md b/display-drivers/README.md new file mode 100644 index 0000000..0a2e9e3 --- /dev/null +++ b/display-drivers/README.md @@ -0,0 +1,93 @@ +# Display Drivers + +Photoframe now supports uploading and enabling of internal displays for the Raspberry Pi family, +the only requirement is that it can be supported by the currently used kernel and modules. + +Since a lot of the smaller displays rely on the built-in fbtft driver, it means that in many +cases, all you really need is a DeviceTree Overlay, essentially configuration files for the +device driver so it knows how to talk to the new display. + +## What's included + +Today, only the waveshare 3.5" IPS (model B) is provided since that was my development system. +But you can create and share these display "drivers" easily yourself. + +## How to write a display driver package + +Start with an empty folder, copy the necessary files for it to work, usually one or two files +ending in `.dtb` or `.dtbo`. + +Next, create a file called `INSTALL` (yes, all caps, important) in the same folder. Open the +file and create the following structure: + +``` +[install] + +[options] +``` + +### The install section +This is a very simple `key/value` pair setup. First part (key) refers to the file included in the +package. The path to the file is based on the location of the `INSTALL` file. You can use +sub-directories if you need to, but if you do so, they must adhere to the same rule. + +The value part refers to where the file should be copied when activated. Typically this is +somewhere in `/boot/`. + +For example, in the waveshare case, this section looks like this: +``` +[install] +waveshare35b-overlay.dtb=/boot/overlays/waveshare35b.dtbo +waveshare35b-overlay.dtb=/boot/overlays/waveshare35b-overlay.dtb +``` +As you can see, the key here is used multiple times, this is because they place this file in two +locations with different names (but it's the same file). + +NOTE! The installer will NOT create any directories when activating. + +### The options section + +This is also a `key/value` setup, but unlike the `install` section, here the key is UNIQUE. If you +define a key multiple times, only the last definition will be used. + +At the very least, this section holds the key `dtoverlay` which is the `/boot/config.txt` keyword +for pointing out an overlay to use. But you can add as many things as you'd like (some DPI displays +require a multitude of key/value pairs). + +In the waveshare 3.5" display case, all it does is point out the overlay: +``` +[options] +dtoverlay=waveshare35b +``` + +## Saving the display driver package + +Once you have written your `INSTALL` file and added the needed files to the folder you +created earlier, all you need to do now is create a zip file out of the contents and give +the file a nice name (like, `waveshare35b.zip`) since that's the name used to identify +the driver. + +## I updated my driver, now what? + +Simply upload it again. The old driver will be deleted and replaced with the new one. + +## This all seem complicated, do you have an example? + +Sure, just unzip the `waveshare35b.zip` and look at it for guidance. + +## What is `manifest.json` ? + +That's a generated file by photoframe which it creates upon installing a driver. You can +create sub-directories in `display-drivers` with pre-processed drivers which will then be +available by default when installing photoframe. + +Note that if you install a driver with the same name as one of the provided ones, the new +driver will take priority + +## Known gotchas + +If you install a driver which you're already using, you need to switch to HDMI and back to +force update the active driver (and no, no need to reboot when going to HDMI, only when +you go back to your updated driver). + +This will eventually be fixed. \ No newline at end of file diff --git a/display-drivers/waveshare35b.zip b/display-drivers/waveshare35b.zip new file mode 100644 index 0000000000000000000000000000000000000000..14d7c112c3b877f1e2c815da2b8f6b21b5d686a4 GIT binary patch literal 1619 zcmWIWW@Zs#0D)H#mwdnsD8UP)%M;5|i!&07QjJZM^aH?3nHV@2v|TUxbRF6n0aCvl zh(QLRDE0IU4srDH={|QxThH^%$|LPinpfcrmOy^ z&g)-n?-F^fyqfvs^T|tvBW1FUe^$tH!JOB$>qKPu?B7F7!4)1A{=-t^t{jB`?notFf%Y1a4|5bqB{^dtaS6s zQj2mDEA>)JlBR~8%@%ePsrS^r!Edi6r{%QpOTttg-)#F!3MRE7C-n)?YEls%_~yO`o{UsYR|r} zRmt!F`;W^Yimid`z{T%7&MEmS%xyWhyg>Z?_J#k07nE|moOA2O#>+Z4{fZYB6>27j zof4fK>0A2RFx#Y3Vf~cdW^EIX+1cIyx$a%|)`Gdm?Xs_D{rbmoY2^pI?Ck$apJMj; zi3QCRRDE34bknZ<<(`Za=lB&?YjzjbEs)@r&Y9b7D)zbCyG-{J)5M=r?pDq_R+V0| zn1Ax&s=sH1kIjl#{P#dYob6cP-S~4i_boIuy`S*pS<9`8ed<5|3hAxxp88a-zP#r~ zztY{}%DJlhOj7^MIbZtYQ|0vs+ZoJ!e;!W>|LkA%W5ujFMn9H6iT-?kMVsk4ea^|o zZL+2NoS#p&{9Vnq{?{X3w*O6B^-KrvAK%*bMmI)OYV!_1<+nk(y$}A0t_$~kZhABN z*kAQq(>so7h&}yoHEGrH)K9f@3anRF^X<0__6zGc)){VFB%Im!rTwJYh28$zmdgJ= z@2t{(_G)c(dRlBm&gb}= zZmE&mnv z-9Pv7r{&h`r^}1j_HO*LC+u;#_2Km~Q+4tRI!~HAoaNhHWj~=#%F-_2{REbBi;v<~11t-h>TjTtz zM&L$Er^o-clCIsGSF1NF?=@h!&pjzjGb*{U+sgP%sUTaT<(==WXE@#~F0VZ!-SDfg zr&c?`S8rF02dtWZ%lk$17#k!(VePQ?12AiA& zr-jD~PIuLONo&paUHHReuWIK`F23d`KjPheyThb^Ro}CDx!3Ldj{`=37z4Z+nINSc z_F@v4zZn=A1V9uJAm?sip$XB7tJnl-U}R_jlG;Fwq7_<*BAbA%=mTnlny?bx1eAgh k*&J*o6v7-YMl_coI}5qI3h-uS18HLcLJ6QIRaOuW04_nKxBvhE literal 0 HcmV?d00001 diff --git a/frame.py b/frame.py index 8bbcb72..e416591 100755 --- a/frame.py +++ b/frame.py @@ -38,14 +38,26 @@ from modules.oauth import OAuth from modules.slideshow import slideshow from modules.colormatch import colormatch +from modules.drivers import drivers void = open(os.devnull, 'wb') +# Supercritical, since we store all photoframe files in a subdirectory, make sure to create it +if not os.path.exists('/root/photoframe_config'): + try: + os.mkdir('/root/photoframe_config') + except: + logging.exception('Unable to create configuration directory, cannot start') + sys.exit(255) +elif not os.path.isdir('/root/photoframe_config'): + logging.error('/root/photoframe_config isn\'t a folder, cannot start') + sys.exit(255) import requests from requests_oauthlib import OAuth2Session -from flask import Flask, request, redirect, session, url_for, abort +from flask import Flask, request, redirect, session, url_for, abort, flash from flask.json import jsonify from flask_httpauth import HTTPBasicAuth +from werkzeug.utils import secure_filename # used if we don't find authentication json class NoAuth: @@ -74,8 +86,9 @@ def wrap(*args, **kwargs): logging.getLogger('urllib3').setLevel(logging.ERROR) app = Flask(__name__, static_url_path='') +app.config['UPLOAD_FOLDER'] = '/tmp/' user = None -userfiles = ['/boot/http-auth.json', '/root/http-auth.json'] +userfiles = ['/boot/http-auth.json', '/root/photoframe_config/http-auth.json'] for userfile in userfiles: if os.path.exists(userfile): @@ -126,34 +139,31 @@ def cfg_keyvalue(key, value): return if request.method == 'PUT': + status = True if key == "keywords": # Keywords has its own API abort(404) return settings.setUser(key, value) settings.save() - if key in ['width', 'height', 'depth', 'tvservice']: - display.setConfiguration(settings.getUser('width'), settings.getUser('height'), settings.getUser('depth'), settings.getUser('tvservice')) - display.enable(True, True) + if key in ['display-driver']: + drv = settings.getUser('display-driver') + if drv == 'none': + drv = None + if not drivers.activate(drv): + settings.setUser('display-driver', 'none') + status = False if key in ['timezone']: # Make sure we convert + to / settings.setUser('timezone', value.replace('+', '/')) helper.timezoneSet(settings.getUser('timezone')) - if key in ['resolution']: - # This one needs some massaging, we essentially deduce all settings from a string (DMT/CEA CODE HDMI) - items = settings.getUser('resolution').split(' ') - logging.debug('Items: %s', repr(items)) - resolutions = display.available() - for res in resolutions: - if res['code'] == int(items[1]) and res['mode'] == items[0]: - logging.debug('Found this item: %s', repr(res)) - settings.setUser('width', res['width']) - settings.setUser('height', res['height']) - settings.setUser('depth', 32) - settings.setUser('tvservice', value) - display.setConfiguration(settings.getUser('width'), settings.getUser('height'), settings.getUser('depth'), settings.getUser('tvservice')) - display.enable(True, True) - break + if key in ['resolution', 'tvservice']: + width, height, tvservice = display.setConfiguration(value) + settings.setUser('tvservice', tvservice) + settings.setUser('width', width) + settings.setUser('height', height) + settings.save() + display.enable(True, True) if key in ['display-on', 'display-off']: timekeeper.setConfiguration(settings.getUser('display-on'), settings.getUser('display-off')) if key in ['autooff-lux', 'autooff-time']: @@ -163,7 +173,7 @@ def cfg_keyvalue(key, value): if key in ['shutdown-pin']: powermanagement.stopmonitor() powermanagement = shutdown(settings.getUser('shutdown-pin')) - return jsonify({'status':True}) + return jsonify({'status':status}) elif request.method == 'GET': if key is None: @@ -212,7 +222,7 @@ def cfg_oauth_info(): abort(500) data = request.json['web'] oauth.setOAuth(data) - with open('/root/oauth.json', 'wb') as f: + with open('/root/photoframe_config/oauth.json', 'wb') as f: json.dump(data, f); return jsonify({'result' : True}) @@ -249,6 +259,9 @@ def cfg_details(about): response = app.make_response(image) response.headers.set('Content-Type', mime) return response + elif about == 'drivers': + result = drivers.list().keys() + return jsonify(result) elif about == 'timezone': result = helper.timezoneList() return jsonify(result) @@ -263,6 +276,28 @@ def cfg_details(about): abort(404) +@app.route('/custom-driver', methods=['POST']) +@auth.login_required +def upload_driver(): + if request.method == 'POST': + # check if the post request has the file part + if 'driver' not in request.files: + logging.error('No file part') + abort(405) + file = request.files['driver'] + # if user does not select file, browser also + # submit an empty part without filename + if file.filename == '' or not file.filename.lower().endswith('.zip'): + logging.error('No filename or invalid filename') + abort(405) + filename = os.path.join('/tmp/', secure_filename(file.filename)) + file.save(filename) + if drivers.install(filename): + return '' + else: + abort(500) + abort(405) + @app.route("/link") @auth.login_required def oauth_step1(): @@ -295,20 +330,27 @@ def web_template(file): return app.send_static_file('template/' + file) settings = settings() +drivers = drivers() +display = display() + if not settings.load(): # First run, grab display settings from current mode current = display.current() - logging.info('No display settings, using: %s' % repr(current)) - settings.setUser('tvservice', '%s %s HDMI' % (current['mode'], current['code'])) - settings.setUser('width', int(current['width'])) - settings.setUser('height', int(current['height'])) - settings.save() - + if current is not None: + logging.info('No display settings, using: %s' % repr(current)) + settings.setUser('tvservice', '%s %s HDMI' % (current['mode'], current['code'])) + settings.save() + else: + logging.info('No display attached?') if settings.getUser('timezone') == '': settings.setUser('timezone', helper.timezoneCurrent()) settings.save() -display = display(settings.getUser('width'), settings.getUser('height'), settings.getUser('depth'), settings.getUser('tvservice')) +width, height, tvservice = display.setConfiguration(settings.getUser('tvservice')) +settings.setUser('tvservice', tvservice) +settings.setUser('width', width) +settings.setUser('height', height) +settings.save() # Force display to desired user setting display.enable(True, True) @@ -333,8 +375,8 @@ def oauthSetToken(token): oauth = OAuth(settings.get('local-ip'), oauthSetToken, oauthGetToken) -if os.path.exists('/root/oauth.json'): - with open('/root/oauth.json') as f: +if os.path.exists('/root/photoframe_config/oauth.json'): + with open('/root/photoframe_config/oauth.json') as f: data = json.load(f) if 'web' in data: # if someone added it via command-line data = data['web'] diff --git a/modules/colormatch.py b/modules/colormatch.py index 527a007..f2407ed 100755 --- a/modules/colormatch.py +++ b/modules/colormatch.py @@ -133,7 +133,11 @@ def run(self): # I2C address 0x29 # Register 0x12 has device ver. # Register addresses must be OR'ed with 0x80 - bus.write_byte(0x29,0x80|0x12) + try: + bus.write_byte(0x29,0x80|0x12) + except: + logging.info('ColorSensor not available') + return ver = bus.read_byte(0x29) # version # should be 0x44 if ver == 0x44: diff --git a/modules/display.py b/modules/display.py index dd025f8..b666576 100755 --- a/modules/display.py +++ b/modules/display.py @@ -21,16 +21,44 @@ import json class display: - def __init__(self, width, height, depth, tvservice_params): - self.setConfiguration(width, height, depth, tvservice_params) + def __init__(self): self.void = open(os.devnull, 'wb') + self.params = None - def setConfiguration(self, width, height, depth, tvservice_params): - self.width = width - self.height = height + def setConfiguration(self, tvservice_params): self.enabled = True - self.depth = depth - self.params = tvservice_params + + # Erase old picture + if self.params is not None: + self.clear() + + result = display.validate(tvservice_params) + if result is None: + self.enabled = False + self.params = None + return (1280, 720, '') + + self.width = result['width'] + self.height = result['height'] + self.depth = result['depth'] + self.reverse = result['reverse'] + self.params = result['tvservice'] + if self.reverse: + self.format = 'bgr' + else: + self.format = 'rgb' + if self.depth == 32: + self.format += 'a' + + return (self.width, self.height, self.params) + + def getDevice(self): + if self.params and self.params.split(' ')[0] == 'INTERNAL': + return '/dev/fb1' + return '/dev/fb0' + + def isHDMI(self): + return self.getDevice() == '/dev/fb0' def get(self): if self.enabled: @@ -40,14 +68,14 @@ def get(self): '8', '-size', '%dx%d' % (self.width, self.height), - 'bgra:/dev/fb0[0]', + '%s:-' % (self.format), 'jpg:-' ] else: args = [ 'convert', '-size', - '%dx%d' % (self.width, self.height), + '%dx%d' % (640, 360), '-background', 'black', '-fill', @@ -57,17 +85,51 @@ def get(self): '-weight', '700', '-pointsize', - '64', - 'label:%s' % "Powersave", + '32', + 'label:%s' % "Display off", '-depth', '8', 'jpg:-' ] - result = subprocess.check_output(args, stderr=self.void) + if not self.enabled: + result = subprocess.check_output(args, stderr=self.void) + elif self.depth in [24, 32]: + with open(self.getDevice(), 'rb') as fb: + pip = subprocess.Popen(args, stdin=fb, stdout=subprocess.PIPE, stderr=self.void) + result = pip.communicate()[0] + elif self.depth == 16: + with open(self.getDevice(), 'rb') as fb: + src = subprocess.Popen(['/root/photoframe/rgb565/rgb565', 'reverse'], stdout=subprocess.PIPE, stdin=fb, stderr=self.void) + pip = subprocess.Popen(args, stdin=src.stdout, stdout=subprocess.PIPE) + src.stdout.close() + result = pip.communicate()[0] + else: + logging.error('Do not know how to grab this kind of framebuffer') return (result, 'image/jpeg') + def _to_display(self, arguments): + if self.depth in [24, 32]: + logging.debug('Sending image directly to framebuffer') + with open(self.getDevice(), 'wb') as f: + ret = subprocess.call(arguments, stdout=f, stderr=self.void) + elif self.depth == 16: # Typically RGB565 + logging.debug('Sending image via RGB565 conversion to framebuffer') + # For some odd reason, cannot pipe the output directly to the framebuffer, use temp file + with open(self.getDevice(), 'wb') as fb: + src = subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=self.void) + pip = subprocess.Popen(['/root/photoframe/rgb565/rgb565'], stdin=src.stdout, stdout=fb) + src.stdout.close() + pip.communicate() + else: + logging.error('Do not know how to render this, depth is %d', self.depth) + + def message(self, message): + if not self.enabled: + logging.debug('Don\'t bother, display is off') + return + args = [ 'convert', '-size', @@ -85,12 +147,15 @@ def message(self, message): 'label:%s' % message, '-depth', '8', - 'bgra:-' + '%s:-' % self.format ] - with open('/dev/fb0', 'wb') as f: - ret = subprocess.call(args, stdout=f, stderr=self.void) + self._to_display(args) def image(self, filename): + if not self.enabled: + logging.debug('Don\'t bother, display is off') + return + logging.debug('Showing image to user') args = [ 'convert', @@ -105,63 +170,90 @@ def image(self, filename): '%dx%d' % (self.width, self.height), '-depth', '8', - 'bgra:-' + '%s:-' % self.format ] - with open('/dev/fb0', 'wb') as f: - ret = subprocess.call(args, stdout=f, stderr=self.void) + self._to_display(args) def enable(self, enable, force=False): if enable == self.enabled and not force: return + # Do not do things if we don't know how to display + if self.params is None: + return + if enable: - if force: # Make sure display is ON and set to our preference - subprocess.call(['/opt/vc/bin/tvservice', '-e', self.params], stderr=self.void, stdout=self.void) - time.sleep(1) - subprocess.call(['/bin/fbset', '-depth', '8'], stderr=self.void) - subprocess.call(['/bin/fbset', '-depth', str(self.depth), '-xres', str(self.width), '-yres', str(self.height), '-vxres', str(self.width), '-vyres', str(self.height)], stderr=self.void) - else: - subprocess.call(['/usr/bin/vcgencmd', 'display_power', '1'], stderr=self.void) + if self.isHDMI(): + if force: # Make sure display is ON and set to our preference + subprocess.call(['/opt/vc/bin/tvservice', '-e', self.params], stderr=self.void, stdout=self.void) + time.sleep(1) + subprocess.call(['/bin/fbset', '-fb', self.getDevice(), '-depth', '8'], stderr=self.void) + subprocess.call(['/bin/fbset', '-fb', self.getDevice(), '-depth', str(self.depth), '-xres', str(self.width), '-yres', str(self.height), '-vxres', str(self.width), '-vyres', str(self.height)], stderr=self.void) + else: + subprocess.call(['/usr/bin/vcgencmd', 'display_power', '1'], stderr=self.void) else: - subprocess.call(['/usr/bin/vcgencmd', 'display_power', '0'], stderr=self.void) + self.clear() + if self.isHDMI(): + subprocess.call(['/usr/bin/vcgencmd', 'display_power', '0'], stderr=self.void) self.enabled = enable def isEnabled(self): return self.enabled def clear(self): - with open('/dev/fb0', 'wb') as f: + with open(self.getDevice(), 'wb') as f: subprocess.call(['cat' , '/dev/zero'], stdout=f, stderr=self.void) @staticmethod - def current(): - ''' - output = subprocess.check_output(['/opt/vc/bin/tvservice', '-s'], stderr=subprocess.STDOUT) - print('"%s"' % (output)) - # state 0x120006 [DVI DMT (82) RGB full 16:9], 1920x1080 @ 60.00Hz, progressive - m = re.search('state 0x[0-9a-f]* \[([A-Z]*) ([A-Z]*) \(([0-9]*)\) [^,]*, ([0-9]*)x([0-9]*)', output) - result = { - 'group' : m.group(2), - 'mode' : m.group(3), - 'drive' : m.group(1), - 'width' : m.group(4), - 'height' : m.group(5) - } - return result - ''' - output = subprocess.check_output(['/opt/vc/bin/tvservice', '-s'], stderr=subprocess.STDOUT) - # state 0x120006 [DVI DMT (82) RGB full 16:9], 1920x1080 @ 60.00Hz, progressive - m = re.search('state 0x[0-9a-f]* \[([A-Z]*) ([A-Z]*) \(([0-9]*)\) [^,]*, ([0-9]*)x([0-9]*) \@ ([0-9]*)\.[0-9]*Hz, (.)', output) - result = { - 'mode' : m.group(2), - 'code' : int(m.group(3)), - 'width' : int(m.group(4)), - 'height' : int(m.group(5)), - 'rate' : int(m.group(6)), - 'aspect_ratio' : '', - 'scan' : m.group(7), - '3d_modes' : [] - } + def _internaldisplay(): + if os.path.exists('/dev/fb1'): + entry = { + 'mode' : 'INTERNAL', + 'code' : '0', + 'width' : 0, + 'height' : 0, + 'rate' : 60, + 'aspect_ratio' : '', + 'scan' : '(internal)', + '3d_modes' : [] + } + info = subprocess.check_output(['/bin/fbset', '-fb', '/dev/fb1'], stderr=subprocess.STDOUT).split('\n') + for line in info: + line = line.strip() + if line.startswith('geometry'): + parts = line.split(' ') + entry['width'] = int(parts[1]) + entry['height'] = int(parts[2]) + entry['depth'] = int(parts[5]) + entry['reverse'] = False + entry['code'] = int(parts[5]) + if entry['code'] != 0: + return entry + return None + + def current(self): + result = None + if self.isHDMI(): + output = subprocess.check_output(['/opt/vc/bin/tvservice', '-s'], stderr=subprocess.STDOUT) + # state 0x120006 [DVI DMT (82) RGB full 16:9], 1920x1080 @ 60.00Hz, progressive + m = re.search('state 0x[0-9a-f]* \[([A-Z]*) ([A-Z]*) \(([0-9]*)\) [^,]*, ([0-9]*)x([0-9]*) \@ ([0-9]*)\.[0-9]*Hz, (.)', output) + if m is None: + return None + result = { + 'mode' : m.group(2), + 'code' : int(m.group(3)), + 'width' : int(m.group(4)), + 'height' : int(m.group(5)), + 'rate' : int(m.group(6)), + 'aspect_ratio' : '', + 'scan' : m.group(7), + '3d_modes' : [], + 'depth':32, + 'reverse':True, + } + else: + result = display._internaldisplay() + return result @staticmethod @@ -171,10 +263,42 @@ def available(): result = [] for entry in cea: entry['mode'] = 'CEA' + entry['depth'] = 32 + entry['reverse'] = True result.append(entry) for entry in dmt: entry['mode'] = 'DMT' + entry['depth'] = 32 + entry['reverse'] = True result.append(entry) + internal = display._internaldisplay() + if internal: + result.append(internal) + # Finally, sort by pixelcount - return sorted(result, key=lambda k: k['width']*k['height']) \ No newline at end of file + return sorted(result, key=lambda k: k['width']*k['height']) + + @staticmethod + def validate(tvservice): + # Takes a string and returns valid width, height, depth and service + items = tvservice.split(' ') + resolutions = display.available() + if len(resolutions) == 0: + return None + + res = resolutions[0] + if len(items) == 3: + for res in resolutions: + if res['code'] == int(items[1]) and res['mode'] == items[0]: + break + else: + logging.warning('Invalid tvservice data, using first available instead') + + return { + 'width':res['width'], + 'height':res['height'], + 'depth':res['depth'], + 'reverse':res['reverse'], + 'tvservice':'%s %s %s' % (res['mode'], res['code'], 'HDMI') + } diff --git a/modules/drivers.py b/modules/drivers.py new file mode 100644 index 0000000..9c86040 --- /dev/null +++ b/modules/drivers.py @@ -0,0 +1,246 @@ +# 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 . +# +import time +import os +import subprocess +import logging +import tempfile +import shutil +import json + +class drivers: + BUILTIN = '/root/photoframe/display-drivers' + EXTERNAL = '/root/photoframe_config/display-drivers/' + MARKER = '### DO NOT EDIT BEYOND THIS COMMENT, IT\'S AUTOGENERATED BY PHOTOFRAME ###' + + def __init__(self): + self.void = open(os.devnull, 'wb') + if not os.path.exists(drivers.EXTERNAL): + try: + os.mkdir(drivers.EXTERNAL) + except: + logging.exception('Unable to create "%s"', drivers.EXTERNAL) + + def _list_dir(self, path): + result = {} + contents = os.listdir(path) + for entry in contents: + if not os.path.isdir(os.path.join(path, entry)): + continue + result[entry] = os.path.join(path, entry) + return result + + def list(self): + result = {} + result = self._list_dir(drivers.BUILTIN) + # Any driver defined in external that has the same name as an internal one + # will replace the internal one. + result.update(self._list_dir(drivers.EXTERNAL)) + + return result + + def _find(self, filename, basedir): + for root, dirnames, filenames in os.walk(basedir): + for filename in filenames: + if filename == 'INSTALL': + return os.path.join(root, filename) + return None + + def _deletefolder(self, folder): + try: + shutil.rmtree(folder) + except: + logging.exception('Failed to delete "%s"', folder) + + def _parse(self, installer): + root = os.path.dirname(installer) + config = {'driver' : os.path.basename(root), 'install' : [], 'options' : {}} + state = 0 + lc = 0 + try: + with open(installer, 'r') as f: + for line in f: + lc += 1 + line = line.strip() + if line.startswith('#') or len(line) == 0: + continue + if line.lower() == '[install]': + state = 1 + continue + if line.lower() == '[options]': + state = 2 + continue + if state == 1: + src, dst = line.split('=', 1) + src = src.strip() + dst = dst.strip() + if dst == '' or src == '': + logging.error('Install section cannot have an empty source or destination filename (Line %d)', lc) + return None + if '..' in src or src.startswith('/'): + logging.error('Install section must use files within package (Line %d)', lc) + return None + src = os.path.join(root, src) + if not os.path.exists(src): + logging.error('INSTALL manifest points to non-existant file (Line %d)', lc) + return None + config['install'].append({'src':src, 'dst':dst}) + elif state == 2: + key, value = line.split('=', 1) + key = key.strip() + value = value.strip() + if key == '' or value == '': + logging.error('Options section cannot have an empty key or value (Line %d)', lc) + return None + if key in config['options']: + logging.warning('Key "%s" will be overridden since it is defined multiple times (Line %d)', lc) + config['options'][key] = value + except: + logging.exception('Failed to read INSTALL manifest') + return None + return config + + def install(self, file): + ''' + Takes a zip file, extracts it and stores the necessary parts in a new + folder under EXTERNAL. Does NOT make it active. + ''' + folder = tempfile.mkdtemp() + extra, _ = os.path.basename(file).rsplit('.', 1) # This is to make sure we have a foldername + try: + result = subprocess.check_call(['/usr/bin/unzip', file, '-d', os.path.join(folder, extra)], stdout=self.void, stderr=self.void) + except: + result = 255 + + if result != 0: + logging.error('Failed to extract files from zipfile') + self._deletefolder(folder) + return False + + # Locate the meat of the file, ie, the INSTALL file + installer = self._find('INSTALL', folder) + if installer is None: + logging.error('No INSTALL manifest, abort driver installation') + self._deletefolder(folder) + return False + + config = self._parse(installer) + if config is None: + logging.error('INSTALL manifest corrupt, abort driver installation') + self._deletefolder(folder) + return False + + # First, make sure we erase existing driver + dstfolder = os.path.join(drivers.EXTERNAL, config['driver']) + if os.path.exists(dstfolder): + logging.info('"%s" already exists, delete before installing', dstfolder) + self._deletefolder(dstfolder) + os.mkdir(dstfolder) + + # Copy all files as needed + files = [] + for entry in config['install']: + src = entry['src'] + dst = os.path.basename(entry['src']).replace('/', '_') + files.append({'src':dst, 'dst':entry['dst']}) + try: + shutil.copyfile(os.path.join(folder, extra, src), os.path.join(dstfolder, dst)) + except: + logging.exception('Failed to copy "%s" to "%s"', os.path.join(folder, extra, src), os.path.join(dstfolder, dst)) + # Shitty, but we cannot leave this directory with partial files + self._deletefolder(dstfolder) + self._deletefolder(folder) + return False + config['install'] = files + + # Just save our config, saving us time next time + with open(os.path.join(dstfolder, 'manifest.json'), 'w') as f: + json.dump(config, f) + + self._deletefolder(folder) + return True + + def activate(self, driver=None): + ''' + Activates a driver, meaning it gets copied into the necessary places and + the config.txt is updated. Setting driver to None removes the active driver + ''' + driverlist = self.list() + if driver is not None: + # Check that this driver exists + if driver not in driverlist: + logging.error('Tried to active non-existant driver "%s"', driver) + return False + + config = {'name':'', 'install':[], 'options':{}} + root = '' + if driver: + try: + with open(os.path.join(driverlist[driver], 'manifest.json'), 'rb') as f: + config = json.load(f) + root = driverlist[driver] + except: + logging.exception('Failed to load manifest for %s', driver) + return False + + # Copy the files into desired locations + for copy in config['install']: + try: + shutil.copyfile(os.path.join(root, copy['src']), copy['dst']) + except: + logging.exception('Failed to copy "%s" to "%s"', copy['src'], copy['dst']) + return False + + # Next, load the config.txt and insert/replace our section + lines = [] + try: + with open('/boot/config.txt', 'rb') as f: + for line in f: + line = line.strip() + if line == drivers.MARKER: + break + lines.append(line) + except: + logging.exception('Failed to read /boot/config.txt') + return False + + # Add our options + if len(config['options']) > 0: + lines.append(drivers.MARKER) + for key in config['options']: + lines.append('%s=%s' % (key, config['options'][key])) + + # Save the new file + try: + with open('/boot/config.txt.new', 'wb') as f: + for line in lines: + f.write('%s\n' % line) + except: + logging.exception('Failed to generate new config.txt') + return False + + # On success, we rename and delete the old config + try: + os.rename('/boot/config.txt', '/boot/config.txt.old') + os.rename('/boot/config.txt.new', '/boot/config.txt') + # Keep the first version of the config.txt just-in-case + if os.path.exists('/boot/config.txt.original'): + os.unlink('/boot/config.txt.old') + else: + os.rename('/boot/config.txt.old', '/boot/config.txt.original') + except: + logging.exception('Failed to activate new config.txt, you may need to restore the config.txt') + return True diff --git a/modules/helper.py b/modules/helper.py index 319537a..0ff95de 100755 --- a/modules/helper.py +++ b/modules/helper.py @@ -84,11 +84,19 @@ def makeFullframe(filename, imageWidth, imageHeight): if height < imageHeight: border = '0x%d' % width_border spacing = '0x%d' % width_spacing - logging.debug('Landscape image, reframing') + zoomed = ((imageHeight-height)/2-width_border) + logging.debug('Landscape image, reframing (visible zoomed is %d)' % zoomed) + if zoomed < 20: + logging.debug('That\'s less than 20px so skip reframing') + return False elif height > imageHeight: border = '%dx0' % width_border spacing = '%dx0' % width_spacing - logging.debug('Portrait image, reframing') + zoomed = ((imageWidth-width)/2-width_border) + logging.debug('Portrait image, reframing (visible zoomed is %d)' % zoomed) + if zoomed < 20: + logging.debug('That\'s less than 20px so skip reframing') + return False else: logging.debug('Image is fullscreen, no reframing needed') return False diff --git a/modules/settings.py b/modules/settings.py index 3d87ee2..1901762 100755 --- a/modules/settings.py +++ b/modules/settings.py @@ -19,7 +19,8 @@ import random class settings: - CONFIGFILE = '/root/settings.json' + CONFIGFILE = '/root/photoframe_config/settings.json' + DEPRECATED_USER = ['resolution'] def __init__(self): self.settings = { @@ -28,7 +29,7 @@ def __init__(self): 'local-ip' : None, 'tempfolder' : '/tmp/', 'colortemp' : None, - 'colortemp-script' : '/root/colortemp.sh', + 'colortemp-script' : '/root/photoframe_config/colortemp.sh', 'cfg' : None } self.userDefaults() @@ -39,7 +40,6 @@ def userDefaults(self): 'height' : 1080, 'depth' : 32, 'tvservice' : 'DMT 82 DVI', - 'resolution' : '', # Place holder, used to deduce correct resolution before setting TV service 'timezone' : '', 'interval' : 60, # Delay in seconds between images (minimum) 'display-off' : 22, # What hour (24h) to disable display and sleep @@ -52,6 +52,7 @@ def userDefaults(self): 'autooff-time' : 0, 'powersave' : '', 'shutdown-pin' : 26, + 'display-driver' : 'none', } def load(self): @@ -66,6 +67,10 @@ def load(self): self.settings['cfg'] = tmp self.settings['cfg'].update(tmp2) + # Remove deprecated fields + for field in settings.DEPRECATED_USER: + self.settings['cfg'].pop(field, None) + # Also, we need to iterate the settings and make sure numbers and floats are # that, and not strings (which the old version did) for k in self.settings['cfg']: @@ -73,7 +78,7 @@ def load(self): # Lastly, correct the tvservice field, should be "TEXT NUMBER TEXT" # This is a little bit of a cheat parts = self.settings['cfg']['tvservice'].split(' ') - if type(self.convertToNative(parts[1])) != int and type(self.convertToNative(parts[2])) == int: + if len(parts) == 3 and type(self.convertToNative(parts[1])) != int and type(self.convertToNative(parts[2])) == int: logging.debug('Reordering tvservice value due to old bug') self.settings['cfg']['tvservice'] = "%s %s %s" % (parts[0], parts[2], parts[1]) self.save() diff --git a/modules/slideshow.py b/modules/slideshow.py index a0ffbbb..5fa2e99 100644 --- a/modules/slideshow.py +++ b/modules/slideshow.py @@ -102,6 +102,9 @@ def presentation(self): keyword = self.settings.getKeyword(index) imgs, cache = self.getImages(keyword) + if imgs is None: + # Try again! + continue # If we've seen all images for this keyword, skip to next if cache in seen: diff --git a/modules/timekeeper.py b/modules/timekeeper.py index 1fc6654..f2dd9f1 100644 --- a/modules/timekeeper.py +++ b/modules/timekeeper.py @@ -44,7 +44,7 @@ def setConfiguration(self, hourOn, hourOff): logging.debug('hourOn = %s, hourOff = %s' % (repr(hourOn), repr(hourOff))) def setPowermode(self, mode): - if mode == '': + if mode == '' or mode == 'none': self.ignoreSensor = True self.ignoreSchedule = True elif mode == 'sensor': diff --git a/rgb565/README.md b/rgb565/README.md new file mode 100644 index 0000000..30d3cea --- /dev/null +++ b/rgb565/README.md @@ -0,0 +1,3 @@ +Simple tool to convert between RGB888 and RGB565. + +It's used to support 16bit displays like the ones from WaveShare diff --git a/rgb565/rgb565 b/rgb565/rgb565 new file mode 100755 index 0000000000000000000000000000000000000000..c64e6395878ae01097df273aa516e5b67cfb4453 GIT binary patch literal 8392 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. + */ + +#include +#include +#include +#include + +#define RGB888toRGB565(r,g,b) ((((r) >> 3) << 11) | (((g) >> 2) << 5) | ((b) >> 3)) +#define RGB565toR8(x) ((((x) >> 11) & 0x1F) * 255 / 31) +#define RGB565toG8(x) ((((x) >> 5) & 0x3F) * 255 / 63) +#define RGB565toB8(x) ((((x) ) & 0x1F) * 255 / 31) + +#define RGB888_BUFSIZE (3*1024) +#define RGB565_BUFSIZE (2*1024) + +void convert565to888(void) { + unsigned short* in_buffer = (unsigned short*)malloc(RGB565_BUFSIZE); + unsigned char* out_buffer = (unsigned char*)malloc(RGB888_BUFSIZE); + int i = 0; + int size = 0; + int remain = 0; + char buf[256]; + + while (1) { + size = read(0, in_buffer + remain, RGB565_BUFSIZE - remain); + if (size < 1 && remain == 0) + break; + size += remain; + remain = size - (size/2)*2; + for (i = 0; i < size/2; ++i) { + out_buffer[i*3+0] = RGB565toR8(in_buffer[i]); + out_buffer[i*3+1] = RGB565toG8(in_buffer[i]); + out_buffer[i*3+2] = RGB565toB8(in_buffer[i]); + } + write(1, out_buffer, (size/2)*3); + if (remain) + memcpy(in_buffer, in_buffer+(size-remain), remain); + } + free(in_buffer); + free(out_buffer); +} + +void convert888to565(void) { + unsigned char* in_buffer = (unsigned char*)malloc(RGB888_BUFSIZE); + unsigned short* out_buffer = (unsigned short*)malloc(RGB565_BUFSIZE); + int i = 0; + int size = 0; + int remain = 0; + char buf[256]; + + while (1) { + size = read(0, in_buffer + remain, RGB888_BUFSIZE - remain); + if (size < 1 && remain == 0) + break; + size += remain; + remain = size - (size/3)*3; + for (i = 0; i < size/3; ++i) + out_buffer[i] = RGB888toRGB565(in_buffer[i*3], in_buffer[i*3+1], in_buffer[i*3+2]); + + write(1, out_buffer, (size/3)*2); + if (remain) + memcpy(in_buffer, in_buffer+(size-remain), remain); + } + free(in_buffer); + free(out_buffer); +} + +int main(int argc, char **argv) +{ + if (argc == 2) // Any argument will kick it into reverse + convert565to888(); + else + convert888to565(); + return 0; +} diff --git a/static/index.css b/static/css/index.css similarity index 100% rename from static/index.css rename to static/css/index.css diff --git a/static/index.html b/static/index.html index 4600521..1d892fa 100644 --- a/static/index.html +++ b/static/index.html @@ -1,10 +1,13 @@ PhotoFrame - - - - + + + + + + + diff --git a/update.sh b/update.sh index c153e85..2bcfac3 100755 --- a/update.sh +++ b/update.sh @@ -39,6 +39,15 @@ if [ "$1" = "post" ]; then echo "i2c-dev" >> /etc/modules-load.d/modules.conf modprobe i2c-dev fi + + # Make sure all old files are moved into the new config folder + mkdir /root/photoframe_config >/dev/null 2>/dev/null + FILES="oauth.json settings.json http_auth.json colortemp.sh" + for FILE in ${FILES}; do + mv /root/${FILE} /root/photoframe_config/ >/dev/null 2>/dev/null + done + + # Copy new service and reload systemd cp frame.service /etc/systemd/system/ systemctl daemon-reload