From 33a21df4af72bad5409f475b494c22bbed80cb4e Mon Sep 17 00:00:00 2001 From: christophski Date: Sat, 8 Jun 2019 01:16:28 +0100 Subject: [PATCH 1/7] Split into services and implement flask --- .gitignore | 10 +- networking.py | 31 ++ orm.py | 4 + projector.py | 284 ++++-------- requirements.txt | 4 + server.py | 811 ++++++++++----------------------- services.py | 249 ++++++++++ settings.py | 8 + static/js/functions.js | 1 - static/js/functionsold.js | 69 --- templates/delete.html | 11 +- templates/displaynotfound.html | 9 +- templates/displays.html | 43 +- templates/index.html | 41 +- templates/indexold.html | 28 -- templates/layout.html | 19 +- templates/layoutold.html | 41 -- templates/rename.html | 16 +- templates/settings.html | 12 +- templates/shutdown.html | 11 +- templates/upload.html | 11 +- templates/videos.html | 9 +- tests/client.py | 43 -- tests/getip.py | 12 - tests/projector.py | 14 - tests/randomname.py | 8 - tests/server.py | 21 - tests/show_logo.py | 76 --- tests/udpsend.py | 16 - uisettings.yml | 5 + 30 files changed, 724 insertions(+), 1193 deletions(-) create mode 100644 networking.py create mode 100644 orm.py create mode 100644 requirements.txt create mode 100644 services.py create mode 100644 settings.py delete mode 100644 static/js/functionsold.js delete mode 100644 templates/indexold.html delete mode 100644 templates/layoutold.html delete mode 100644 tests/client.py delete mode 100644 tests/getip.py delete mode 100644 tests/projector.py delete mode 100644 tests/randomname.py delete mode 100644 tests/server.py delete mode 100644 tests/show_logo.py delete mode 100644 tests/udpsend.py create mode 100644 uisettings.yml diff --git a/.gitignore b/.gitignore index 49fbd99..785f5e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ -*.log settings.yml +*.avi +*.pyc +*.db +*.sublime* +*.log +oldversions/* +*.bak +static/images/* +static/videos/* diff --git a/networking.py b/networking.py new file mode 100644 index 0000000..3cb3efc --- /dev/null +++ b/networking.py @@ -0,0 +1,31 @@ +import struct + +# SENDING AND RECEIVING +def send_msg(sock, msg): + """ Prefix each message with a 4-byte length (network byte order) """ + msg = struct.pack('>I', len(msg)) + msg + sock.sendall(msg) + + +def recv_msg(sock): + """ Read message length and unpack it into an integer """ + raw_msglen = recvall(sock, 4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + + +def recvall(sock, n): + """ Helper function to recv n bytes or return None if EOF is hit """ + data = '' + + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + print(packet) + data += packet + + return data diff --git a/orm.py b/orm.py new file mode 100644 index 0000000..c3a53aa --- /dev/null +++ b/orm.py @@ -0,0 +1,4 @@ +import web + +db = web.database(dbn="sqlite", db="images.db") +# CREATE TABLE images(Id INTEGER PRIMARY KEY, filename TEXT, imagename TEXT, folder TEXT); diff --git a/projector.py b/projector.py index 7601e00..a5d5650 100644 --- a/projector.py +++ b/projector.py @@ -1,5 +1,9 @@ #!/usr/bin/python +""" +Projectr - Projector Process +Works in tandem with Server Process +""" from __future__ import absolute_import, division, print_function, unicode_literals import pi3d @@ -7,23 +11,17 @@ import os import logging import time -import sys -from datetime import datetime import argparse import multiprocessing import yaml # Networking -import struct import thread import socket import pickle -""" -Projectr - Projector Process -Works in tandem with Server Process +from . import networking -""" logging.basicConfig( filename="projector.log", level=logging.INFO, @@ -31,14 +29,13 @@ ) IMAGEDIR = 'static/images/' +ALPHA_STEP = 0.025 -""" FUNCTIONS """ # Use class for settings? def write_settings(process, data): """Write the previous image to settings file""" logging.info("Writing settings...") - logging.info("Writing settings...") with open('settings.yml', 'w') as outfile: outfile.write(yaml.dump(data, default_flow_style=True)) @@ -69,19 +66,19 @@ def read_settings(process): return data -def fit_image(input_texture): +def fit_image(input_texture, display): """ Fit image to screen """ # Ripped this from demo, think I understand it # Pretty sure this bit resizes textureture to display size # Get ratio of display to textureture - x_ratio = DISPLAY.width/input_texture.ix - y_ratio = DISPLAY.height/input_texture.iy + x_ratio = display.width/input_texture.ix + y_ratio = display.height/input_texture.iy if y_ratio < x_ratio: # if y ratio is smaller than x ratio x_ratio = y_ratio # make the ratios the same width, height = input_texture.ix * x_ratio, input_texture.iy * x_ratio # width, height = tex.ix, tex.iy - x_position = (DISPLAY.width - width)/2 - y_position = (DISPLAY.height - height)/2 + x_position = (display.width - width)/2 + y_position = (display.height - height)/2 return width, height, x_position, y_position @@ -93,49 +90,11 @@ def list_files(directory, reverse=False): f.endswith(('.jpg', '.jpeg', '.png'))] if reverse is True: - # Sort newFileList by date added(?) output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) output.reverse() # reverse image list so new files are first - else: - pass - return output -""" NETWORKING """ - - -# SENDING AND RECEIVING -def send_msg(sock, msg): - """ Prefix each message with a 4-byte length (network byte order) """ - msg = struct.pack('>I', len(msg)) + msg - sock.sendall(msg) - - -def recv_msg(sock): - """ Read message length and unpack it into an integer """ - raw_msglen = recvall(sock, 4) - if not raw_msglen: - return None - msglen = struct.unpack('>I', raw_msglen)[0] - # Read the message data - return recvall(sock, msglen) - - -def recvall(sock, n): - """ Helper function to recv n bytes or return None if EOF is hit """ - data = '' - - while len(data) < n: - packet = sock.recv(n - len(data)) - if not packet: - return None - print(packet) - data += packet - - return data - - """ PROCESSES """ @@ -146,7 +105,7 @@ def slideshow(imagelist, cur_queue, killslideshowq): settings = read_settings(process) logging.info("Starting slideshow...") working = True - while working is True: + while working: for image in imagelist: # Check to see if slideshow needs to die if not killslideshowq.empty(): @@ -162,26 +121,27 @@ def slideshow(imagelist, cur_queue, killslideshowq): def client_handler(connection, client_address, cur_queue): process = "Client Handler - %s" % os.urandom(8).encode('base_64') - logging.debug(connections) # Is there a slideshow running? slideshowon = False killslideshowq = multiprocessing.Queue() logging.info("Connected to Master") while True: - # buffer size is 1024 bytes - data = recv_msg(connection) + data = networking.recv_msg(connection) - if data != "alive": + if data == "alive": + logging.info("Alive?") + networking.send_msg(connection, "alive") + else: try: data = pickle.loads(data) logging.debug(data) - except TypeError, e: + except TypeError as e: logging.exception("Failed to unpickle data") logging.info("Received TCP data: %s from %s" % (data, client_address)) - if slideshowon is True: + if slideshowon: # If slideshow is running, kill it logging.info("Tell Slideshow process to die") # Tell slideshow to die @@ -211,18 +171,13 @@ def client_handler(connection, client_address, cur_queue): print(images) elif data["action"] == "whatsplaying": settings = read_settings(process) - send_msg(connection, settings["lastimage"]) + networking.send_msg(connection, settings["lastimage"]) else: logging.info("Unkown action: %s", data["action"]) - else: - logging.info("Alive?") - send_msg(connection, "alive") def tcp_receiver(cur_queue, connections): """ Receive images via TCP """ - process = "TCP Receiver" - # TCP config tcp_ip = "127.0.0.1" # local only tcp_port = 5006 @@ -253,21 +208,18 @@ def tcp_receiver(cur_queue, connections): client_address, cur_queue)) -""" pi3d """ - class Carousel(object): """ The main object """ - def __init__(self): - # Start the image dictionary + + def __init__(self, display): self.imagedict = {} self.process = "Carousel" - + self.shader = pi3d.Shader("2d_flat") + self.display = display # Load the last image used try: # If there is a settings file, load most recent image - # Load the settings file - settings = read_settings(self.process) # Add the image to the queue logging.info("Last image: %s" % settings["lastimage"]) @@ -283,102 +235,70 @@ def __init__(self): # Load default image into queue starting_image = settings["lastimage"] - # Set up image one - texture_one = pi3d.Texture(starting_image, blend=True, mipmap=True) - image_one = pi3d.Canvas() - image_one.set_texture(texture_one) - - width, height, x_position, y_position = fit_image(texture_one) - - image_one.set_2d_size(w=width, h=height, x=x_position, y=y_position) - image_one.set_alpha(1) - image_one.set_shader(SHADER) - image_one.positionZ(0.1) - - self.imagedict[starting_image] = {"canvas": image_one, "visible": True, - "fading": True} - + self.set_up_image(starting_image, 1, 0.1) self.focus = starting_image - # print(self.imagedict[self.focus]["canvas"].z()) + def set_up_image(self, image, alpha, z_position): + texture = pi3d.Texture(image, blend=True, mipmap=True) + canvas = pi3d.Canvas() + canvas.set_texture(texture) + width, height, x_position, y_position = fit_image(texture, self.display) + canvas.set_2d_size(w=width, h=height, x=x_position, y=y_position) + canvas.set_alpha(alpha) + canvas.set_shader(self.shader) + canvas.positionZ(z_position) + self.imagedict[image] = { + "canvas": canvas, "visible": True, "fading": True} def pick(self, new_image): """ Pick an image by URL """ + if self.focus == new_image: + logging.warning("Image already projected") + return + + # Check to see if image already in dictionary + # Might be worth pre-preparing + # a dictionary with null canvas objects? + + # If image is already loaded, make it visible + if new_image in self.imagedict: + # New focus image is visible + # New focus image is the active fader + self.imagedict[new_image]["visible"] = True + self.imagedict[new_image]["fading"] = True + else: + self.set_up_image(new_image, 0, 0.1) - if self.focus != new_image: - # Check to see if image already in dictionary - # Might be worth pre-preparing - # a dictionary with null canvas objects? - - # If image is already loaded, make it visible - if new_image in self.imagedict: - # print("Image exists") - # New focus image is visible - self.imagedict[new_image]["visible"] = True - - # New focus image is the active fader - self.imagedict[new_image]["fading"] = True - - # Otherwise load it as a new image - else: - # print("Load new image") - - new_canvas = pi3d.Canvas() - - # print("Pick an image: %s" % new_image) - new_texture = pi3d.Texture(new_image, blend=True, mipmap=True) - # print("Texture loaded") - new_canvas.set_texture(new_texture) - # print("Texture set") - - # Fit image - width, height, x_position, y_position = fit_image(new_texture) - - new_canvas.set_2d_size(w=width, h=height, - x=x_position, y=y_position) - new_canvas.set_alpha(0) - new_canvas.set_shader(SHADER) - new_canvas.positionZ(0.2) - # print("New image prepared") - - self.imagedict[new_image] = {"canvas": new_canvas, - "visible": True, "fading": True} - - # Move old focused image back - - if self.imagedict[self.focus]["canvas"].z() > 0.1: - self.imagedict[self.focus]["canvas"].positionZ(0.1) + # Move old focused image back + if self.imagedict[self.focus]["canvas"].z() > 0.1: + self.imagedict[self.focus]["canvas"].positionZ(0.1) - # Bring new focused image forward - self.imagedict[new_image]["canvas"].positionZ(0.2) + # Bring new focused image forward + self.imagedict[new_image]["canvas"].positionZ(0.2) - # Old focus image not the active fader - self.imagedict[self.focus]["fading"] = False + # Old focus image not the active fader + self.imagedict[self.focus]["fading"] = False - self.focus = new_image # Change the focused image - # Write new image to settings - settings = read_settings(self.process) - settings["lastimage"] = new_image - write_settings(self.process, settings) - settings = read_settings(self.process) - else: - logging.warning("Image already projected") + self.focus = new_image # Change the focused image + # Write new image to settings + settings = read_settings(self.process) + settings["lastimage"] = new_image + write_settings(self.process, settings) + settings = read_settings(self.process) def update(self): """ Update image alphas """ - for image in self.imagedict: - alpha = self.imagedict[image]["canvas"].alpha() - if self.imagedict[image]["fading"] is True and alpha < 1: - # print("%s Increase alpha: %f" % (image, alpha)) - alpha += alpha_step - self.imagedict[image]["canvas"].set_alpha(alpha) - elif self.imagedict[image]["fading"] is False and alpha > 0: - # print("%s Decrease alpha: %f" % (image, alpha)) - alpha -= alpha_step - self.imagedict[image]["canvas"].set_alpha(alpha) + for image, detail in self.imagedict.iteritems(): + alpha = detail["canvas"].alpha() + if detail["fading"] and alpha < 1: + alpha += ALPHA_STEP + detail["canvas"].set_alpha(alpha) + elif not detail["fading"] and alpha > 0: + alpha -= ALPHA_STEP + detail["canvas"].set_alpha(alpha) else: if alpha <= 0: - self.imagedict[image]["visible"] = False + detail["visible"] = False def draw(self): """ Draw the images on the screen """ @@ -386,11 +306,11 @@ def draw(self): first_image = None second_image = None - for image in self.imagedict: - if self.imagedict[image]["visible"] is True and self.imagedict[image]["fading"] is True: - first_image = self.imagedict[image]["canvas"] - elif self.imagedict[image]["visible"] is True: - second_image = self.imagedict[image]["canvas"] + for image, detail in self.imagedict.iteritems(): + if detail["visible"] and detail["fading"]: + first_image = detail["canvas"] + elif detail["visible"]: + second_image = detail["canvas"] first_image.draw() # If second image exists (fixes start up) @@ -398,22 +318,9 @@ def draw(self): second_image.draw() -if __name__ == "__main__": - process = "Main Process" - logging.info("Christie's Projector") - - # parse command line arguments - parser = argparse.ArgumentParser(description='Project images.') - parser.add_argument("-t", "--test", - help="Run projector in a window for testing", - action="store_true") - - args = parser.parse_args() - +def main(test): # Set up image queue IMAGEQ = multiprocessing.Queue() - - # Set up connections dict connections = multiprocessing.Manager().dict() logging.info("Start TCP Receiver") @@ -422,40 +329,31 @@ def draw(self): tcpprocess.start() logging.info("Start Projector process") - - if args.test: - # If testing, create a small display - DISPLAY = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), + if test: + display = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), frames_per_second=20, w=800, h=600) else: - DISPLAY = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), + display = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), frames_per_second=20) - - SHADER = pi3d.Shader("2d_flat") - - alpha_step = 0.025 crsl = Carousel() # Set up camera CAMERA = pi3d.Camera.instance() CAMERA.was_moved = False - - # Keyboard KEYBOARD = pi3d.Keyboard() - while DISPLAY.loop_running(): + while display.loop_running(): crsl.update() crsl.draw() - # Take keyboard events and check for quit k = KEYBOARD.read() if k > -1: if k == 27: KEYBOARD.close() - DISPLAY.stop() + display.stop() if len(connections) > 0: for connection in connections: - print("Close connnection %s" % connection) + logging.info("Close connnection %s" % connection) connection.shutdown(socket.SHUT_RDWR) connection.close() tcpprocess.terminate() @@ -465,3 +363,13 @@ def draw(self): new_image = IMAGEQ.get() logging.info("New image is: %s", new_image) crsl.pick(new_image) + + +if __name__ == "__main__": + logging.info("Christie's Projector") + parser = argparse.ArgumentParser(description='Project images.') + parser.add_argument("-t", "--test", + help="Run projector in a window for testing", + action="store_true") + args = parser.parse_args() + main(args.test) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..86de72b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pi3d +yaml +pil +flask \ No newline at end of file diff --git a/server.py b/server.py index fb57b94..3cf4991 100644 --- a/server.py +++ b/server.py @@ -1,630 +1,293 @@ #! /usr/bin/env python """ Web interface for the projector """ - -from __future__ import absolute_import, division, print_function, unicode_literals - -import web +import logging import os -from datetime import datetime -from PIL import Image import random -import yaml -import sys -import dbus -# Networking import socket -import pickle -import struct - -# Set up TCP socket port 5005 - -connections = {} - -# Set up URLS - -urls = ( - '/', 'Index', - '/upload', 'Upload', - '/settings', 'Settings', - '/delete', 'Delete', - '/shutdown', 'Shutdown', - '/rename', 'Rename', - '/videos', 'Videos', - '/displays', 'Displays', - '/display/(.+)', 'Index', - '/initnetwork', 'initNetwork' -) - -# Directories -IMAGEDIR = 'static/images/' -VIDEODIR = 'static/videos' -THUMBDIR = 'static/images/thumbs' - - -# Set up web.py app -app = web.application(urls, globals()) - -# Template Renderer -render = web.template.render('templates/', base="layout") - -db = web.database(dbn="sqlite", db="images.db") -# CREATE TABLE images(Id INTEGER PRIMARY KEY, filename TEXT, imagename TEXT, folder TEXT); - +from flask import Flask, render_template, request, redirect +from flask.views import MethodView +from PIL import Image +import settings +import services +from orm import db -# Image size maximums -WIDTH = 1920 -HEIGHT = 1080 -""" Functions """ +logging.basicConfig( + filename="server.log", + level=logging.INFO, + format='%(asctime)s %(thread)s %(levelname)-6s %(funcName)s:%(lineno)-5d %(message)s', +) -# Use class for settings? -def db_list_images(): - """ List images in database """ - output = db.select('images') +app = Flask(__name__) - return output +class Index(MethodView): + def get(self, display="local"): + current_image = None -def db_insert_image(filename): - """ Insert an image into the database """ - imagename, file_extension = os.path.splitext(filename) - filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for - i in range(16)) + # Check if the display exists + if display not in [f for f in services.PROJECTRS]: + logging.info("Display %s not found" % (display)) + return render_template("displaynotfound.html", + pagetitle="Display %s Not Found" % display, + displays=services.PROJECTRS, + message="") + + # Ask screen what is currently displayed + message = {"action": "whatsplaying"} + services.send_msg_to_display(display, message) + + # Wait for reply + try: + current_image = services.recv_msg(services.connections[display]) + logging.info("Received TCP data: %s from %s" % + (current_image, services.connections[display])) + except socket.timeout: + logging.info("No reply, socket timed out") + except KeyError as e: + logging.info("Display not found") + + images = services.db_list_images() + imagelist = [(x["filename"], x["imagename"]) for x in images] + + if current_image is None: + return render_template( + "index.html", + imagelist=imagelist, + pagetitle="Display - %s" % services.PROJECTRS[display]["name"], + current_image=current_image, + displays=services.PROJECTRS, + message="") + + return render_template( + "index.html", + imagelist=imagelist, + pagetitle="Display - %s" % services.PROJECTRS[display]["name"], + current_image=current_image[14:], + displays=services.PROJECTRS, + message="") + + def post(self, display="local"): + action = request.form["action"] + if action == "project": + prop1 = request.form["prop1"] # THE IMAGE + prop2 = request.form["prop2"] + logging.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) + message = {"action": "project", + "images": [os.path.join(settings.IMAGEDIR, prop1)]} + services.send_msg_to_display(display, message) + logging.info("Image to be projected is %s" % prop1) + services.PROJECTRS[display]["current"] = prop1 + return True + elif action == "slideshow": + # TODO: This doesn't work at all + imagelist = [v for k, v in request.args.iteritems() if k != "action"] + message = {"action": "slideshow", "images": imagelist} + services.send_msg_to_display(display, message) + return redirect('/') + else: + logging.info("Unknown Action %s" % action) +app.add_url_rule('/', view_func=Index.as_view('index')) +app.add_url_rule('/display/', view_func=Index.as_view('display')) + + +class initNetwork(MethodView): + def get(self): + services.init_network() + return redirect('/') +app.add_url_rule('/initnetwork', view_func=initNetwork.as_view('initnetwork')) + + +class Settings(MethodView): + def get(self): + settingsdict = services.read_settings() + return render_template("settings.html", + pagetitle="Settings", + settingsdict=settingsdict, + displays=services.PROJECTRS, + message="") + + def post(self): + settings = services.read_settings() + settings["slideshow"]["delay"] = int(request.args["slideshowlength"]) + services.write_settings(settings) + return redirect('/') +app.add_url_rule('/settings', view_func=Settings.as_view('settings')) + + +class Delete(MethodView): + def get(self): + image = request.args["image"] + return render_template("delete.html", + pagetitle="Delete", + displays=services.PROJECTRS, + image=image) + + def post(self): + image = request.args["image"] + try: + os.remove(os.path.join(settings.IMAGEDIR, image)) + logging.info("%s deleted" % image) + except OSError as e: + logging.exception("Deleting %s failed" % image) + db.delete('images', where="filename=$image", vars=locals()) + return redirect('/') +app.add_url_rule('/delete', view_func=Delete.as_view('delete')) - imageid = db.insert('images', filename=filename, - imagename=imagename, folder="") - print(imageid) +class Rename(MethodView): + def get(self): + image = request.args["image"] + imagename = db.select('images', where="filename=$image", + vars=locals())[0]["imagename"] + return render_template("rename.html", + pagetitle="Rename", + image=image, + displays=services.PROJECTRS, + message=imagename) + + def post(self): + image = request.form["image"] + newname = request.form["newname"] + db.update('images', where="filename=$image", + imagename=newname, vars=locals()) + return redirect('/') +app.add_url_rule('/rename', view_func=Rename.as_view('rename')) -def write_settings(data): - """Write the previous image to settings file""" - print("Writing settings...") - print(data) - with open('settings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) +class Upload(MethodView): + def get(self): + return render_template("upload.html", + pagetitle="Upload", + displays=services.PROJECTRS, + message="") -def read_settings(): - """ Read settings from YAML file""" - print("Read settings...") - try: - return yaml.load(open("settings.yml")) - except: - print("Could not read settings") - return None + def post(self): + if 'newimage' not in request.files: + return -def write_log(logdata, process=""): - """ Write to the log """ - if process == "": - with open("server.log", "a") as myfile: - output = "%s : %s" % (datetime.now().strftime('%Y/%m/%d %H:%M'), - logdata) - myfile.write(output) - print(output) + newimage = request.files["newimage"] + # replaces the windows-style slashes with linux ones. + filepath = newimage.filename.replace('\\', '/') + # splits the path and chooses the last part (the filename with extension) + imagename = filepath.split('/')[-1] + imagename, file_extension = os.path.splitext(imagename) - else: - with open("server.log", "a") as myfile: - output = "%s : %s : %s" % ( - datetime.now().strftime('%Y/%m/%d %H:%M'), - process, - logdata - ) - myfile.write(output) - print(output) + filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for + i in range(16)) + newimagepath = os.path.join(settings.IMAGEDIR, filename) + newimage.save(newimagepath) -def rename_image(filename): - """ Rename files with random string to ensure there are no clashes """ - randomstring = random.getrandbits(16) - filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] - print(filename) + logging.info("%s uploaded successfully" % imagename) + logging.info("Resizing %s" % imagename) + imageid = db.insert('images', filename=filename, + imagename=imagename, folder="") + logging.info("Added %s to the database as ID %d" % (imagename, imageid)) -def make_thumbnail(image): - """ Make thumnail for given image """ - # if thumbnail doesn't exist - if not os.path.exists(os.path.join(thumbdir, image)): - imagepath = image - print(get_logtime("%s: PIL - File to open is: %s" % (cur_process, - imagepath))) try: # open and convert to RGB - img = Image.open(imagepath).convert('RGB') + img = Image.open(newimagepath).convert('RGB') # find ratio of new height to old height - hpercent = (float(HEIGHT) / float(img.size[1])) + hpercent = (float(settings.HEIGHT) / float(img.size[1])) # apply ratio to create new width wsize = int(float(img.size[0]) * hpercent) # resize image with antialiasing - img = img.resize((int(wsize), int(HEIGHT)), Image.ANTIALIAS) + img = img.resize((int(wsize), int(settings.HEIGHT)), Image.ANTIALIAS) # save with quality of 80, optimise setting caused crash - img.save(imagepath, format='JPEG', quality=90) - write_log("Sucessfully resized: %s \n" % image) + # Delete original (in case it is a different filetype) + # This needs to be changed! + os.remove(newimagepath) + newimagepath = os.path.splitext(newimagepath)[0] + ".jpg" + img.save(newimagepath, format='JPEG', quality=90) + logging.info("Sucessfully resized: %s \n" % newimagepath) except IOError: - write_log( - "IO Error. %s will be deleted and " - "downloaded properly next sync" - % imagepath) - os.remove(imagepath) - else: - write_log("Thumbnail for %s exists \n" % image) - - -def list_files(directory, reverse=False): - """ Return list of files of specified type """ - output = [f for f in os.listdir(directory) if - os.path.isfile(os.path.join(directory, f)) and - f.endswith(('.jpg', '.jpeg', '.png'))] - - if reverse is True: - # Sort newFileList by date added(?) - output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) - output.reverse() # reverse image list so new files are first - else: - pass - - return output - - -def connect_display(display): - process = "connect_display" - """ Try and connect to a display - This is a bit of a clusterfuck - should probably re-engineer this """ - # Make a new socket - write_log("Connections: " % connections, process) - try: - write_log("Remove display socket from connections", process) - del connections[display] - except KeyError, e: - write_log("Display doesn't exist in connections", process) - - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - write_log("Socket made", process) - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - # set socket timeout high - connections[display].settimeout(20) - return True - except: - write_log("No displays found", process) - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - return False - - -def send_msg_display(display, msg): - process = "Send Message To Display" - """ Send a message to a display """ - write_log("Send message to display", process) - x = 0 - attempts = 1 - while x == 0 and attempts < 4: - try: - sock = connections[display] - x = 1 - except: - e = sys.exc_info()[0] - write_log("Display %s doesn't exist: %s" % (display, e), process) - - write_log("Reattach display, attempt %d" % attempts, process) - attempt = connect_display(display) - - if attempt is True: - write_log("Successfully reconnected display", process) - else: - write_log("Could not reattach display", process) - attempts += 1 - - if display in connections: - # Prefix each message with a 4-byte length (network byte order) - msg = struct.pack('>I', len(msg)) + msg - - # Try sending message, if broken pipe - # Try creating new socket - x = 0 - attempts = 0 - - while x < 1 and attempts < 3: - try: - write_log("Attempting to send message", process) - sock.sendall(msg) - write_log("Sent message %s" % msg) - # success, quit loop - x = 1 - except socket.error, e: - write_log("Socket error: %s" % e, process) - write_log("Attempting to reconnect to display %s" % sock, - process) - write_log(connections) - - # Make a new socket - del connections[display] - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - write_log("Socket made", process) - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - # set socket timeout high - connections[display].settimeout(20) - except: - write_log("Something fucked up", process) - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - attempts += 1 - else: - write_log("Could not send message, could not communicate with display", - process) - - -def recv_msg(sock): - # Read message length and unpack it into an integer - raw_msglen = recvall(sock, 4) - if not raw_msglen: - return None - msglen = struct.unpack('>I', raw_msglen)[0] - # Read the message data - return recvall(sock, msglen) - - -def recvall(sock, n): - """Helper function to recv n bytes or return None if EOF is hit""" - data = '' - - while len(data) < n: - packet = sock.recv(n - len(data)) - if not packet: - return None - data += packet - - return data - - -def init_network(connections): - process = "Network Initialisation" - write_log("Check display connections", process) - for display in PROJECTRS: - if PROJECTRS[display]["enabled"] is True: - if display not in connections: - write_log("Display not in existing connections", process) - write_log("Connecting to display %s" % display, process) - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - write_log("Socket made", process) - connections[display].settimeout(1) - - try: - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - connections[display].settimeout(20) - except: - write_log("Could not connect", process) - del connections[display] - else: - send_msg_display(display, "alive") - try: - check = recv_msg(connections[display]) - write_log("Already connected to %s" % display, process) - except: - write_log("Connection appears to be dead", process) - del connections[display] - write_log("Connecting to display %s" % display, process) - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - write_log("Socket made", process) - connections[display].settimeout(1) - - try: - connections[display].connect(display_address) - write_log("Connected to %s" % display, process) - connections[display].settimeout(20) - except: - write_log("Something fucked up", process) - del connections[display] - write_log("Connections: %s" % connections, process) - - -# Home Page -class Index(object): - def GET(self, display="local"): - - current_display = display - current_image = None + logging.info("IO Error. %s will be deleted and downloaded properly next sync" % newimagepath) + os.remove(newimagepath) - # Check if the display exists - if current_display in [f for f in PROJECTRS]: - # Ask screen what is currently displayed - message = pickle.dumps({"action": "whatsplaying"}) - send_msg_display(display, message) - - # Wait for reply - try: - current_image = recv_msg(connections[display]) - write_log("Received TCP data: %s from %s" % - (current_image, connections[display])) - except socket.timeout: - write_log("No reply, socket timed out") - except KeyError, e: - write_log("Display not found") - - images = db_list_images() - imagelist = [] - - for image in images: - imagelist.append((image["filename"], image["imagename"])) - - if current_image is None: - return render.index(imagelist, "Display - %s" % - PROJECTRS[current_display]["name"], - current_image, PROJECTRS, "") - else: - return render.index(imagelist, "Display - %s" % - PROJECTRS[current_display]["name"], - current_image[14:], PROJECTRS, "") - else: - write_log("Display %s not found" % (current_display)) - return render.displaynotfound("Display %s Not Found" % - current_display, PROJECTRS, "") + return redirect('/') +app.add_url_rule('/upload', view_func=Upload.as_view('upload')) - def POST(self, display="local"): - print("Index POST") - # Check the action - action = web.input().action # Project? +class Displays(MethodView): + def get(self): + logging.info(services.connections) + alive = [f for f in services.connections] - # If project, project image - if action == "project": - prop1 = web.input().prop1 # THE IMAGE - prop2 = web.input().prop2 - print("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) - message = pickle.dumps({"action": "project", - "images": [os.path.join(IMAGEDIR, prop1)]}) - send_msg_display(display, message) - print("Image to be projected is %s" % prop1) - PROJECTRS[display]["current"] = prop1 - return True - elif action == "slideshow": - # Get contents of the input - slideshow = web.input() + return render_template("displays.html", + pagetitle="Displays", + displays=services.PROJECTRS, + alive=alive, + message="") - imagelist = [] + def post(self): + display = request.args["prop1"] + message = {"action": "sync"} + services.send_msg_to_display(display, message) + logging.info("sync %s" % display) +app.add_url_rule('/displays', view_func=Displays.as_view('displays')) - # Get list of images - for image in slideshow: - if image != "action": - print(slideshow[image]) - imagelist.append(slideshow[image]) - write_log(slideshow) +class Shutdown(MethodView): + def get(self): + return render_template("shutdown.html", + pagetitle="Shutdown?", + displays=services.PROJECTRS, + message="") - message = pickle.dumps({"action": "slideshow", "images": "hello"}) - write_log(message) - send_msg_display(display, message) - raise web.seeother('/') - else: - print("Unknown Action %s" % action) + def post(self): + shutdown = request.form["shutdown"] + logging.info(shutdown) + if shutdown == "true": + logging.info("Shutdown") + message = {"action": "project", + "images": ['static/img/shutdown.jpg']} + # Should probably cycle through display and switch them off + services.send_msg_to_display("local", message) + os.system("poweroff") + else: + logging.info("Don't shutdown") + return redirect('/') +app.add_url_rule('/shutdown', view_func=Shutdown.as_view('shutdown')) -class Videos(object): - def GET(self): - # Get folders in users folders - # imagelist = list_files(IMAGEDIR) - videolist = [f for f in os.listdir(VIDEODIR) if - os.path.isfile(os.path.join(VIDEODIR, f)) and +class Videos(MethodView): + def get(self): + videolist = [f for f in os.listdir(settings.VIDEODIR) if + os.path.isfile(os.path.join(settings.VIDEODIR, f)) and f.endswith(('.mp4', '.webm'))] - write_log("User accessed index") - - return render.videos(videolist, "Home", PROJECTRS, "") - - def POST(self): - print("Index POST") + logging.info("User accessed index") + return render_template("videos.html", + videolist=videolist, + pagetitle="Home", + displays=services.PROJECTRS, + message="") + def post(self, display="local"): # Check the action - action = web.input().action # Project? + action = request.args["action"] # If project, project video if action == "project": - prop1 = web.input().prop1 # THE video - prop2 = web.input().prop2 - print("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) - message = pickle.dumps({"action": "project", - "video": [os.path.join(VIDEODIR, prop1)]}) - sock.sendto(message, (TCP_IP, TCP_PORT)) - print("video to be projected is %s" % prop1) + prop1 = request.args["prop1"] # THE video + prop2 = request.args["prop2"] + logging.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) + message = {"action": "project", + "video": [os.path.join(settings.VIDEODIR, prop1)]} + services.send_msg_to_display(display, message) + logging.info("video to be projected is %s" % prop1) return True else: - print("Unknown Action %s" % action) - - -class initNetwork(object): - def GET(self): - init_network(connections) - raise web.seeother('/') - - -class Settings(object): - def GET(self): - # Get settings - settingsdict = read_settings() - return render.settings("Settings", settingsdict, PROJECTRS, "") - - def POST(self): - newsettings = web.input() - # newsetting is - - settings = read_settings() - - settings["slideshow"]["delay"] = int(newsettings["slideshowlength"]) - write_settings(settings) - - raise web.seeother('/') - - -class Delete(object): - def GET(self): - image = web.input().image - - return render.delete("Delete", PROJECTRS, image) - - def POST(self): - image = web.input().image - try: - os.remove(os.path.join(IMAGEDIR, image)) - write_log("%s deleted" % image) - except OSError, e: - write_log("Deleting %s failed" % image) - write_log("Error: %s" % e) - - db.delete('images', where="filename=$image", vars=locals()) - - raise web.seeother('/') - - -class Rename(object): - def GET(self): - image = web.input().image - - # Get image name from database - imagename = db.select('images', where="filename=$image", - vars=locals())[0]["imagename"] - - return render.rename("Rename", image, PROJECTRS, imagename) - - def POST(self): - image = web.input().image - newname = web.input().newname - - # Update image in database with new name - db.update('images', where="filename=$image", - imagename=newname, vars=locals()) - - raise web.seeother('/') - - -class Upload(object): - def GET(self): - return render.upload("Upload", PROJECTRS, "") - - def POST(self): - image = web.input(newimage={}) - - if 'newimage' in image: # to check if the file-object is created - # replaces the windows-style slashes with linux ones. - filepath = image.newimage.filename.replace('\\', '/') - # splits the path and chooses the last part (the filename with extension) - imagename = filepath.split('/')[-1] - imagename, file_extension = os.path.splitext(imagename) - imagename = imagename - - filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for - i in range(16)) - - newimagepath = os.path.join(IMAGEDIR, filename) - - # creates the file where the uploaded file should be stored - fout = open(newimagepath, 'w') - # writes the uploaded file to the newly created file. - fout.write(image.newimage.file.read()) - fout.close() # closes the file, upload complete. - write_log("%s uploaded successfully" % imagename) - write_log("Resizing %s" % imagename) - - # Add image to database - imageid = db.insert('images', filename=filename, - imagename=imagename, folder="") - print("Added %s to the database as ID %d" % (imagename, imageid)) - - try: - # open and convert to RGB - img = Image.open(newimagepath).convert('RGB') - - # find ratio of new height to old height - hpercent = (float(HEIGHT) / float(img.size[1])) - # apply ratio to create new width - wsize = int(float(img.size[0]) * hpercent) - # resize image with antialiasing - img = img.resize((int(wsize), int(HEIGHT)), Image.ANTIALIAS) - # save with quality of 80, optimise setting caused crash - # Delete original (in case it is a different filetype) - # This needs to be changed! - os.remove(newimagepath) - newimagepath = os.path.splitext(newimagepath)[0] + ".jpg" - img.save(newimagepath, format='JPEG', quality=90) - write_log("Sucessfully resized: %s \n" % newimagepath) - except IOError: - write_log("IO Error. %s will be deleted and downloaded properly next sync" % newimagepath) - os.remove(newimagepath) - - raise web.seeother('/') - - -class Displays(object): - def GET(self): - displays = PROJECTRS - print(connections) - alive = [f for f in connections] - - return render.displays("Displays", displays, alive, "") - - def POST(self): - display = web.input().prop1 - message = pickle.dumps({"action": "sync"}) - - sock.sendto(message, (PROJECTRS[display]["ip"], - PROJECTRS[display]["port"])) - - print("sync %s" % display) - - -class Shutdown(object): - def GET(self): - return render.shutdown("Shutdown?", PROJECTRS, "") - - def POST(self): - shutdown = web.input().shutdown - print(shutdown) - - if shutdown == "true": - print("Shutdown") - message = pickle.dumps({"action": "project", - "images": ['static/img/shutdown.jpg']}) - # Should probably cycle through display and switch them off - send_msg_display("local", message) - os.system("poweroff") - else: - print("Don't shutdown") - raise web.seeother('/') + logging.info("Unknown Action %s" % action) +app.add_url_rule('/videos', view_func=Videos.as_view('videos')) -SETTINGS = read_settings() -PROJECTRS = SETTINGS["projectors"] - if __name__ == "__main__": - # Set up settings - - init_network(connections) - app.run() + services.init_network() + app.run(host="localhost", port=8000) diff --git a/services.py b/services.py new file mode 100644 index 0000000..71bd92c --- /dev/null +++ b/services.py @@ -0,0 +1,249 @@ +import logging +import os +import pickle +import random +import struct +import socket +import yaml +from PIL import Image +import settings +from orm import db + +connections = {} + + +def db_list_images(): + """ List images in database """ + return db.select('images') + + +def db_insert_image(filename): + """ Insert an image into the database """ + imagename, file_extension = os.path.splitext(filename) + filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for + i in range(16)) + + imageid = db.insert('images', filename=filename, + imagename=imagename, folder="") + logging.info(imageid) + + +def write_settings(data): + """Write the previous image to settings file""" + logging.info("Writing settings...") + logging.info(data) + with open('uisettings.yml', 'w') as outfile: + outfile.write(yaml.dump(data, default_flow_style=True)) + + +def read_settings(): + """ Read settings from YAML file""" + logging.info("Read settings...") + try: + return yaml.load(open("uisettings.yml")) + except (IOError, yaml.composer.ComposerError): + logging.exception("Could not read settings") + return None + + +def rename_image(filename): + """ Rename files with random string to ensure there are no clashes """ + randomstring = random.getrandbits(16) + filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] + logging.info(filename) + + +def make_thumbnail(imagepath): + """ Make thumnail for given image """ + # if thumbnail doesn't exist + if os.path.exists(os.path.join(settings.THUMBDIR, imagepath)): + logging.info("Thumbnail for %s exists \n" % imagepath) + return + + logging.info("PIL - File to open is: %s" % imagepath) + try: + # open and convert to RGB + img = Image.open(imagepath).convert('RGB') + + # find ratio of new height to old height + hpercent = (float(settings.HEIGHT) / float(img.size[1])) + # apply ratio to create new width + wsize = int(float(img.size[0]) * hpercent) + # resize image with antialiasing + img = img.resize((int(wsize), int(settings.HEIGHT)), Image.ANTIALIAS) + # save with quality of 80, optimise setting caused crash + img.save(imagepath, format='JPEG', quality=90) + logging.info("Sucessfully resized: %s \n" % imagepath) + except IOError: + logging.info( + "IO Error. %s will be deleted and " + "downloaded properly next sync" + % imagepath) + os.remove(imagepath) + + +def list_files(directory, reverse=False): + """ Return list of files of specified type """ + output = [f for f in os.listdir(directory) if + os.path.isfile(os.path.join(directory, f)) and + f.endswith(('.jpg', '.jpeg', '.png'))] + + if reverse: + # Sort newFileList by date added(?) + output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) + output.reverse() # reverse image list so new files are first + + return output + + +SETTINGS = read_settings() +print(SETTINGS) +PROJECTRS = SETTINGS["projectors"] + + +def connect_display(display): + """ Try and connect to a display + This is a bit of a clusterfuck + should probably re-engineer this """ + # Make a new socket + logging.info("Connections: " % connections) + try: + logging.info("Remove display socket from connections") + del connections[display] + except KeyError as e: + logging.exception("Display doesn't exist in connections") + + # Get display address + display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) + # Make new socket it and add to connections dict + connections[display] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + logging.info("Socket made") + # Set connections timeout low + connections[display].settimeout(1) + + try: + # try connecting to socket + connections[display].connect(display_address) + logging.info("Connected to %s" % display) + # set socket timeout high + connections[display].settimeout(20) + return True + except: + logging.info("No displays found") + # delete socket from dict, because it doesn't work + del connections[display] + # increment attempts + return False + + +def send_msg_to_display(display, msg): + """ Send a message to a display """ + logging.info("Send message to display") + msg = pickle.dumps(msg) + logging.info(msg) + sent = False + attempts = 1 + while not sent and attempts < 4: + try: + sock = connections[display] + sent = True + except KeyError: + logging.exception("Display %s doesn't exist" % display) + logging.info("Reattach display, attempt %d" % attempts) + display_connected = connect_display(display) + + if display_connected: + logging.info("Successfully reconnected display") + else: + logging.info("Could not reattach display") + attempts += 1 + + if display not in connections: + logging.info("Could not send message, could not communicate with display") + return + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + + # Try sending message, if broken pipe + # Try creating new socket + sent = False + attempts = 0 + + while not sent and attempts < 3: + try: + logging.info("Attempting to send message") + sock.sendall(msg) + logging.info("Sent message %s" % msg) + # success, quit loop + sent = True + except socket.error as e: + logging.info("Socket error: %s" % e) + logging.info("Attempting to reconnect to display %s" % sock) + logging.info(connections) + + # Make a new socket + del connections[display] + # Get display address + display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) + # Make new socket it and add to connections dict + connections[display] = socket.socket(socket.AF_INET, + socket.SOCK_STREAM) + logging.info("Socket made") + # Set connections timeout low + connections[display].settimeout(1) + + try: + # try connecting to socket + connections[display].connect(display_address) + logging.info("Connected to %s" % display) + # set socket timeout high + connections[display].settimeout(20) + except: + logging.info("Something went wrong") + # delete socket from dict, because it doesn't work + del connections[display] + # increment attempts + attempts += 1 + + +def recv_msg(sock): + # Read message length and unpack it into an integer + raw_msglen = recvall(sock, 4) + if not raw_msglen: + return None + msglen = struct.unpack('>I', raw_msglen)[0] + # Read the message data + return recvall(sock, msglen) + + +def recvall(sock, n): + """Helper function to recv n bytes or return None if EOF is hit""" + data = '' + + while len(data) < n: + packet = sock.recv(n - len(data)) + if not packet: + return None + data += packet + + return data + + +def init_network(): + logging.info("Check display connections") + for display in PROJECTRS: + if not PROJECTRS[display]["enabled"]: + continue + + if display not in connections: + logging.info("Display not in existing connections") + connect_display(display) + else: + send_msg_to_display(display, "alive") + try: + check = recv_msg(connections[display]) + logging.info("Already connected to %s" % display) + except: + logging.info("Connection appears to be dead") + connect_display(display) + logging.info("Connections: %s" % connections) diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..d876304 --- /dev/null +++ b/settings.py @@ -0,0 +1,8 @@ +# Directories +IMAGEDIR = 'static/images/' +VIDEODIR = 'static/videos' +THUMBDIR = 'static/images/thumbs' + +# Image size maximums +WIDTH = 1920 +HEIGHT = 1080 diff --git a/static/js/functions.js b/static/js/functions.js index 23e3838..dfa02cb 100644 --- a/static/js/functions.js +++ b/static/js/functions.js @@ -2,7 +2,6 @@ jQuery(document).ready(function() { jQuery(function( $ ) { $('#displayswitch').on('click', function(){ - if ($('#displayslist').css('display') === 'none'){ $('#displayslist').slideDown(); } else { diff --git a/static/js/functionsold.js b/static/js/functionsold.js deleted file mode 100644 index 11bef72..0000000 --- a/static/js/functionsold.js +++ /dev/null @@ -1,69 +0,0 @@ -jQuery(document).ready(function() { -jQuery(function( $ ) { - - $('.delete').on('click', function() { - - image = $(this).parents('.tile').data("image"); - //alert(absimage); - - - current_element = $(this).parents('.tile'); - - nothing = "nothing"; - - jQuery.ajax({ - type: "POST", - data: {action : "delete", prop1 : image, prop2 : nothing}, - success: function() { - current_element.css("display", "none"); - } - }); - return false; - }); - - $('.project').on('click', function() { - image = $(this).parents('.tile').data("image"); - - - nothing = "nothing"; - - jQuery.ajax({ - type:"POST", - data: {action: "project", prop1 : image, prop2: nothing}, - success: function() { - alert("Projecting " + image); - } - }); - - }); - - $('#menubutton').on('click', function(){ - - if ($('nav').css('display') === 'none'){ - $('nav').css("display", "block"); - } else { - $('nav').css("display", "none"); - } - - }); - -}); - -}); - -/* -$("id-" + image).attr("src", "../" + absimage + "?" + d.getTime()); - -jQuery(document).ready(function() { -jQuery(".button").click(function() { - var input_string = $$("input#textfield").val(); - jQuery.ajax({ - type: "POST", - data: {textfield : input_string}, - success: function(data) { - jQuery('#foo').html(data).hide().fadeIn(1500); - }, - }); - return false; - }); -});*/ \ No newline at end of file diff --git a/templates/delete.html b/templates/delete.html index 3fac0ab..7cb200a 100644 --- a/templates/delete.html +++ b/templates/delete.html @@ -1,9 +1,5 @@ -$def with (pagetitle, displays, image) - -$var pagetitle = pagetitle -$var displays = displays - - +{% extends 'layout.html' %} +{% block content %}
@@ -11,4 +7,5 @@

-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/displaynotfound.html b/templates/displaynotfound.html index 6db68a5..3a809b5 100644 --- a/templates/displaynotfound.html +++ b/templates/displaynotfound.html @@ -1,7 +1,6 @@ -$def with (pagetitle, displays, message) - -$var pagetitle = pagetitle -$var displays = displays +{% extends 'layout.html' %} +{% block content %}

DISPLAY NOT FOUND

-

Edit Displays

\ No newline at end of file +

Edit Displays

+{% endblock %} \ No newline at end of file diff --git a/templates/displays.html b/templates/displays.html index cfe2872..bd7e591 100644 --- a/templates/displays.html +++ b/templates/displays.html @@ -1,10 +1,5 @@ -$def with (pagetitle, displays, alive, message) - -$var pagetitle = pagetitle -$var displays = displays - -$alive - +{% extends 'layout.html' %} +{% block content %} New Static Display
@@ -17,35 +12,39 @@ Alive? Remove - $for display in displays: + {% for display_name, display in displays.items() %} - $displays[display]["name"] - $displays[display]["ip"] - O + {{display.name}} + {{display.ip}} + O - $if display in alive: + {%if display in alive %} Yes - $else: + {% else %} No + {% endif %} + {% endfor %}
-$for display in displays: - $if display in alive: +{% for display_name, display in displays.items() %} + {% if display in alive %}
- $else: + {% else %}
- - + {% endif %} + +
- $displays[display]['name']
- $displays[display]['ip'] + {{display_name}}
+ {{display.ip}}
-
-
\ No newline at end of file +{% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 4294cd6..a4e4c97 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,11 +1,5 @@ -$def with (imagelist, pagetitle, current_image, displays, message) - -$if current_image == "unknown": - $var pagetitle = "%s - X" % pagetitle -$else: - $var pagetitle = pagetitle -$var displays = displays - +{% extends 'layout.html' %} +{% block content %}
@@ -13,24 +7,25 @@
-$for image in imagelist: - $if image[0] == current_image: -
- $else: -
- - +{% for image in imagelist %} + {% if image[0] == current_image %} +
+ {% else %} +
+ {% endif %} +
-
Delete
-
Rename
+
Delete
+
Rename
Project
- +
- - - -
+ +
+{% endfor %}
-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/indexold.html b/templates/indexold.html deleted file mode 100644 index 836ed3c..0000000 --- a/templates/indexold.html +++ /dev/null @@ -1,28 +0,0 @@ -$def with (imagelist, pagetitle, message) - -$var pagetitle = pagetitle - -
-
- - - -
- -$for image in imagelist: -
- -
-
Delete
-
Rename
-
Project
- -
- - - -
- -
-
-
\ No newline at end of file diff --git a/templates/layout.html b/templates/layout.html index e6bc232..63548bf 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -1,4 +1,3 @@ -$def with (content) @@ -20,9 +19,8 @@
- - $:content + + {% block content %} + {% endblock %}
diff --git a/templates/layoutold.html b/templates/layoutold.html deleted file mode 100644 index 134393c..0000000 --- a/templates/layoutold.html +++ /dev/null @@ -1,41 +0,0 @@ -$def with (content) - - - - Projectr - - - - - - - - - -
- - -
- - $:content - -
-
- - \ No newline at end of file diff --git a/templates/rename.html b/templates/rename.html index 9e2b1dc..84ac417 100644 --- a/templates/rename.html +++ b/templates/rename.html @@ -1,16 +1,12 @@ -$def with (pagetitle, image, displays, imagename) - -$var pagetitle = pagetitle -$var displays = displays - - - +{% extends 'layout.html' %} +{% block content %}
- - Rename $imagename?

+ + Rename {{imagename}}?



-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html index eb47950..bc9f4c0 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -1,12 +1,10 @@ -$def with (pagetitle, settingsdict, displays, message) - -$var pagetitle = pagetitle -$var displays = displays - -
+{% extends 'layout.html' %} +{% block content %} +

Maximum slide time is 17 seconds




-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/shutdown.html b/templates/shutdown.html index 17c06dc..06e83c2 100644 --- a/templates/shutdown.html +++ b/templates/shutdown.html @@ -1,13 +1,10 @@ -$def with (pagetitle, displays, message) - -$var pagetitle = pagetitle -$var displays = displays - - +{% extends 'layout.html' %} +{% block content %}
-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/upload.html b/templates/upload.html index 0b402d9..a79cdd3 100644 --- a/templates/upload.html +++ b/templates/upload.html @@ -1,13 +1,10 @@ -$def with (pagetitle, displays, message) - -$var pagetitle = pagetitle -$var displays = displays - - +{% extends 'layout.html' %} +{% block content %}

Please avoid any files with special characters in their names.



-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/templates/videos.html b/templates/videos.html index 3027910..caa09c7 100644 --- a/templates/videos.html +++ b/templates/videos.html @@ -1,7 +1,5 @@ -$def with (videolist, pagetitle, message) - -$var pagetitle = pagetitle - +{% extends 'layout.html' %} +{% block content %}
@@ -24,4 +22,5 @@
-
\ No newline at end of file + +{% endblock %} \ No newline at end of file diff --git a/tests/client.py b/tests/client.py deleted file mode 100644 index fc8c8cb..0000000 --- a/tests/client.py +++ /dev/null @@ -1,43 +0,0 @@ -import socket -import time -import fcntl -import struct - - -PORT = 50000 -MAGIC = "sdf876sd" #to make sure we don't confuse or get confused by other programs - -foundclients = False - -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #create UDP socket -s.bind(('', 0)) -s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) #this is a broadcast socket - -def get_ip_address(ifname): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return socket.inet_ntoa(fcntl.ioctl( - s.fileno(), - 0x8915, # SIOCGIFADDR - struct.pack('256s', ifname[:15]) - )[20:24]) - -my_ip = get_ip_address('wlan0') - -while 1: - data = MAGIC+my_ip - s.sendto(data, ('', PORT)) - print "sent service announcement, waiting for reply" - - # wait for reply, timeout after 1 minute - data, addr = s.recvfrom(1024) - print data - - # when receive reply, break loop and continue with IP - if data == "hello": - print "Received acknowledgement from server" - break - -print "Escaped successfully" - - - diff --git a/tests/getip.py b/tests/getip.py deleted file mode 100644 index 0ff7a61..0000000 --- a/tests/getip.py +++ /dev/null @@ -1,12 +0,0 @@ -import socket -import time -import fcntl -import struct - -def get_ip_address(ifname): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - return socket.inet_ntoa(fcntl.ioctl( - s.fileno(), - 0x8915, # SIOCGIFADDR - struct.pack('256s', ifname[:15]) - )[20:24]) \ No newline at end of file diff --git a/tests/projector.py b/tests/projector.py deleted file mode 100644 index 834c6c6..0000000 --- a/tests/projector.py +++ /dev/null @@ -1,14 +0,0 @@ -import socket - -UDP_IP = "127.0.0.1" -UDP_PORT = 5005 - - -sock = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP -sock.bind((UDP_IP, UDP_PORT)) - -while True: - data, addr = sock.recvfrom(1024) # buffer size is 1024 bytes - print "received messages:", data - print "From address: ", addr \ No newline at end of file diff --git a/tests/randomname.py b/tests/randomname.py deleted file mode 100644 index 9c75cbc..0000000 --- a/tests/randomname.py +++ /dev/null @@ -1,8 +0,0 @@ -import random - -def rename_image(filename): - randomstring = random.getrandbits(16) - filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] - print filename - -rename_image('/christie/file.jpg') \ No newline at end of file diff --git a/tests/server.py b/tests/server.py deleted file mode 100644 index 190d9a8..0000000 --- a/tests/server.py +++ /dev/null @@ -1,21 +0,0 @@ -import socket - -PORT = 50000 -MAGIC = "sdf876sd" # make us unique - - -s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) #create UDP socket -s.bind(('', PORT)) - -clients = {} - -while 1: - print "Waiting for service announcement" - data, addr = s.recvfrom(1024) # wait for a packet - if data.startswith(MAGIC): - clientip = data[len(MAGIC):] - print clientip - print "got service announcement from", data[len(MAGIC):] - # Tell client we've heard - print "Sending reply" - s.sendto("hello", (clientip, PORT)) diff --git a/tests/show_logo.py b/tests/show_logo.py deleted file mode 100644 index 677f208..0000000 --- a/tests/show_logo.py +++ /dev/null @@ -1,76 +0,0 @@ -import os -import pygame -import time -import socket -import sys - -UDP_IP = "127.0.0.1" -UDP_PORT = 5005 - - -# Set up UDP -sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP -sock.bind((UDP_IP, UDP_PORT)) - - -# check if there is an X display -disp_no = os.getenv('DISPLAY') - -if disp_no: - print "I'm running under X display = {0}".format(disp_no) - -# List of possible Framebuffer drivers, directfb is preferable -drivers = ['directfb', 'fbcon', 'svgalib'] - -found = False -# attempt to use each driver with pygame -for driver in drivers: - if not os.getenv('SDL_VIDEODRIVER'): - os.putenv('SDL_VIDEODRIVER', driver) - print driver - try: - pygame.display.init() - except pygame.error: - print 'Driver: {0} failed.'.format(driver) - continue - found = True - break - -if not found: - raise Exception('No suitable video driver found!') - - -size = (pygame.display.Info().current_w, pygame.display.Info().current_h) -screen = pygame.display.set_mode(size, pygame.FULLSCREEN) - -IMG = pygame.image.load("/home/pi/server/static/images/logo.png") - -while True: - events = pygame.event.get() - for event in pygame.event.get(): - if event.type == QUIT: - pygame.quit() - sys.exit() - - x = (size[0] / 2) - (IMG.get_rect().size[0] / 2) - - pygame.mouse.set_visible(False) - screen.fill((0,0,0)) - screen.blit(IMG, (x, 0)) - pygame.display.flip() - - # Wait for udp - DATA, ADDR = sock.recvfrom(1024) # buffer size is 1024 bytes - try: - IMG = pygame.image.load("/home/pi/server/static/images/%s" % DATA) - except: - print "ERROR! Could not load image %s" % DATA - - # Check if new image is bigger than screen - if (IMG.get_rect().size[0] > size[0]) or IMG.get_rect().size[1] > size[1]: - IMG = pygame.transform.smoothscale(IMG, (size[0], IMG.get_rect().size[1] / (IMG.get_rect().size[0] / size[0]))) - # Check if image is still too tall after being resized - if IMG.get_rect().size[1] > size[1]: - IMG = pygame.transform.smoothscale(IMG, (IMG.get_rect().size[0] / (IMG.get_rect().size[1] / size[1]), size[1])) - else: - pass diff --git a/tests/udpsend.py b/tests/udpsend.py deleted file mode 100644 index 9746c62..0000000 --- a/tests/udpsend.py +++ /dev/null @@ -1,16 +0,0 @@ -import socket - -UDP_IP = "127.0.0.1" -UDP_PORT = 5005 - - -SOCK = socket.socket(socket.AF_INET, # Internet - socket.SOCK_DGRAM) # UDP - -#print "starting at: %d" % count - -print "Input filename of image to show:" -IMAGE = raw_input("> ") - -# send image name to projector -SOCK.sendto(IMAGE, (UDP_IP, UDP_PORT)) diff --git a/uisettings.yml b/uisettings.yml new file mode 100644 index 0000000..f1bddfc --- /dev/null +++ b/uisettings.yml @@ -0,0 +1,5 @@ +{!!python/unicode 'fadeduration': 2, !!python/unicode 'lastimage': !!python/unicode 'static/images/logo.jpg', + !!python/unicode 'projectors': {!!python/unicode 'local': {!!python/unicode 'current': !!python/unicode '', + !!python/unicode 'enabled': true, !!python/unicode 'ip': !!python/unicode '127.0.0.1', + !!python/unicode 'name': !!python/unicode 'Main', !!python/unicode 'port': 5006}}, + !!python/unicode 'slideshow': {!!python/unicode 'delay': 20, !!python/unicode 'loop': true}} From e8eb605c5aa33d4fa15d152ecd9b959d4f1d1b58 Mon Sep 17 00:00:00 2001 From: Christie Grinham Date: Wed, 24 Jun 2020 12:20:44 +0100 Subject: [PATCH 2/7] Create linting.yml --- .github/workflows/linting.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/linting.yml diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 0000000..16a9db2 --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,22 @@ +name: Code Quality + +on: + push: + paths: + - "**.py" + +jobs: + lint: + name: Python Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: "3.8" + - name: Run flake8 + uses: julianwachholz/flake8-action@v1 + with: + checkName: "Python Lint" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 0f823938b8c268a677b13a6494cc122bccbcbff8 Mon Sep 17 00:00:00 2001 From: christophski Date: Wed, 24 Jun 2020 14:04:43 +0100 Subject: [PATCH 3/7] Move code from handler to services.py --- server.py | 38 +++----------------------------------- services.py | 3 ++- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/server.py b/server.py index 3cf4991..68618ba 100644 --- a/server.py +++ b/server.py @@ -164,7 +164,6 @@ def get(self): message="") def post(self): - if 'newimage' not in request.files: return @@ -172,43 +171,12 @@ def post(self): # replaces the windows-style slashes with linux ones. filepath = newimage.filename.replace('\\', '/') # splits the path and chooses the last part (the filename with extension) - imagename = filepath.split('/')[-1] - imagename, file_extension = os.path.splitext(imagename) - - filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for - i in range(16)) + filename = filepath.split('/')[-1] + filename = services.db_insert_image(filename) newimagepath = os.path.join(settings.IMAGEDIR, filename) newimage.save(newimagepath) - - logging.info("%s uploaded successfully" % imagename) - logging.info("Resizing %s" % imagename) - - imageid = db.insert('images', filename=filename, - imagename=imagename, folder="") - logging.info("Added %s to the database as ID %d" % (imagename, imageid)) - - try: - # open and convert to RGB - img = Image.open(newimagepath).convert('RGB') - - # find ratio of new height to old height - hpercent = (float(settings.HEIGHT) / float(img.size[1])) - # apply ratio to create new width - wsize = int(float(img.size[0]) * hpercent) - # resize image with antialiasing - img = img.resize((int(wsize), int(settings.HEIGHT)), Image.ANTIALIAS) - # save with quality of 80, optimise setting caused crash - # Delete original (in case it is a different filetype) - # This needs to be changed! - os.remove(newimagepath) - newimagepath = os.path.splitext(newimagepath)[0] + ".jpg" - img.save(newimagepath, format='JPEG', quality=90) - logging.info("Sucessfully resized: %s \n" % newimagepath) - except IOError: - logging.info("IO Error. %s will be deleted and downloaded properly next sync" % newimagepath) - os.remove(newimagepath) - + services.make_thumbnail(newimagepath) return redirect('/') app.add_url_rule('/upload', view_func=Upload.as_view('upload')) diff --git a/services.py b/services.py index 71bd92c..2958d63 100644 --- a/services.py +++ b/services.py @@ -25,7 +25,8 @@ def db_insert_image(filename): imageid = db.insert('images', filename=filename, imagename=imagename, folder="") - logging.info(imageid) + logging.info("Added %s to the database as ID %d" % (imagename, imageid)) + return filename def write_settings(data): From 603d0259c68377a774cf54d0f5e5eead77d6611f Mon Sep 17 00:00:00 2001 From: christophski Date: Sat, 27 Jun 2020 00:56:52 +0100 Subject: [PATCH 4/7] Improve projectr code --- networking.py | 142 ++++++++++++++++++++++++++++ orm.py | 6 +- projector.py | 234 ++++++++++++++++++++--------------------------- requirements.txt | 7 +- server.py | 36 ++++---- services.py | 207 +++++++++-------------------------------- settings.json | 1 + 7 files changed, 306 insertions(+), 327 deletions(-) create mode 100644 settings.json diff --git a/networking.py b/networking.py index 3cb3efc..f4b3543 100644 --- a/networking.py +++ b/networking.py @@ -1,4 +1,12 @@ import struct +import socket +import logging +import pickle + +import services + +connections = {} + # SENDING AND RECEIVING def send_msg(sock, msg): @@ -29,3 +37,137 @@ def recvall(sock, n): data += packet return data + + +SETTINGS = services.read_settings(settings_file="settings.json") +print(SETTINGS) +if SETTINGS: + PROJECTRS = SETTINGS["projectors"] +else: + PROJECTRS = [] + + +def connect_display(display): + """ Try and connect to a display + This is a bit of a clusterfuck + should probably re-engineer this """ + # Make a new socket + logging.info("Connections: " % connections) + try: + logging.info("Remove display socket from connections") + del connections[display] + except KeyError: + logging.exception("Display doesn't exist in connections") + + # Get display address + display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) + # Make new socket it and add to connections dict + connections[display] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + logging.info("Socket made") + # Set connections timeout low + connections[display].settimeout(1) + + try: + # try connecting to socket + connections[display].connect(display_address) + logging.info("Connected to %s" % display) + # set socket timeout high + connections[display].settimeout(20) + return True + except: + logging.info("No displays found") + # delete socket from dict, because it doesn't work + del connections[display] + # increment attempts + return False + + +def send_msg_to_display(display, msg): + """ Send a message to a display """ + logging.info("Send message to display") + msg = pickle.dumps(msg) + logging.info(msg) + sent = False + attempts = 1 + while not sent and attempts < 4: + try: + sock = connections[display] + sent = True + except KeyError: + logging.exception("Display %s doesn't exist" % display) + logging.info("Reattach display, attempt %d" % attempts) + display_connected = connect_display(display) + + if display_connected: + logging.info("Successfully reconnected display") + else: + logging.info("Could not reattach display") + attempts += 1 + + if display not in connections: + logging.info("Could not send message, could not communicate with display") + return + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + + # Try sending message, if broken pipe + # Try creating new socket + sent = False + attempts = 0 + + while not sent and attempts < 3: + try: + logging.info("Attempting to send message") + sock.sendall(msg) + logging.info("Sent message %s" % msg) + # success, quit loop + sent = True + except socket.error as e: + logging.info("Socket error: %s" % e) + logging.info("Attempting to reconnect to display %s" % sock) + logging.info(connections) + + # Make a new socket + del connections[display] + # Get display address + display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) + # Make new socket it and add to connections dict + connections[display] = socket.socket(socket.AF_INET, + socket.SOCK_STREAM) + logging.info("Socket made") + # Set connections timeout low + connections[display].settimeout(1) + + try: + # try connecting to socket + connections[display].connect(display_address) + logging.info("Connected to %s" % display) + # set socket timeout high + connections[display].settimeout(20) + except Exception: + logging.exception("Something went wrong") + # delete socket from dict, because it doesn't work + del connections[display] + # increment attempts + attempts += 1 + + +def init_network(): + logging.info("Check display connections") + for display in PROJECTRS: + if not PROJECTRS[display]["enabled"]: + continue + + if display not in connections: + logging.info("Display not in existing connections") + connect_display(display) + else: + send_msg_to_display(display, "alive") + try: + # Try to receive from connection to see if it is alive + recv_msg(connections[display]) + logging.info("Already connected to %s" % display) + except Exception: + logging.exception("Connection appears to be dead") + connect_display(display) + logging.info("Connections: %s" % connections) diff --git a/orm.py b/orm.py index c3a53aa..d28a763 100644 --- a/orm.py +++ b/orm.py @@ -1,4 +1,6 @@ -import web +# import web +from flask_sqlalchemy import SQLAlchemy -db = web.database(dbn="sqlite", db="images.db") +db = None +# db = web.database(dbn="sqlite", db="images.db") # CREATE TABLE images(Id INTEGER PRIMARY KEY, filename TEXT, imagename TEXT, folder TEXT); diff --git a/projector.py b/projector.py index a5d5650..34cc8d4 100644 --- a/projector.py +++ b/projector.py @@ -13,57 +13,44 @@ import time import argparse import multiprocessing -import yaml # Networking -import thread +import threading import socket import pickle -from . import networking +import networking +import services logging.basicConfig( filename="projector.log", level=logging.INFO, format='%(asctime)s %(thread)s %(levelname)-6s %(funcName)s:%(lineno)-5d %(message)s', ) +client_logger = logging.getLogger('client_handler') +projectr_logger = logging.getLogger('projectr') IMAGEDIR = 'static/images/' ALPHA_STEP = 0.025 - - -# Use class for settings? -def write_settings(process, data): - """Write the previous image to settings file""" - logging.info("Writing settings...") - with open('settings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) - - -def read_settings(process): - logging.info("Read settings...") - try: - return yaml.load(open("settings.yml")) - except: - logging.exception("Could not read settings") - logging.info("Writing default settings file") - data = { - 'slideshow': {'delay': 20, 'loop': True}, - 'fadeduration': 2, - 'lastimage': u'static/images/logo.jpg', - 'projectors': { - 'local': { - 'ip': '127.0.0.1', - 'port': 5006, - 'enabled': True, - 'name': 'Main', - 'current': '', - } - } - } - with open('settings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) - return data +# TCP config +TCP_IP = "127.0.0.1" # local only +TCP_PORT = 5006 + + +DEFAULT_SETTINGS = { + 'slideshow': {'delay': 20, 'loop': True}, + 'fadeduration': 2, + 'lastimage': u'static/images/logo.jpg', + 'projectors': { + 'local': { + 'ip': TCP_IP, + 'port': TCP_PORT, + 'enabled': True, + 'name': 'Main', + 'current': '', + } + } +} def fit_image(input_texture, display): @@ -71,30 +58,18 @@ def fit_image(input_texture, display): # Ripped this from demo, think I understand it # Pretty sure this bit resizes textureture to display size # Get ratio of display to textureture - x_ratio = display.width/input_texture.ix - y_ratio = display.height/input_texture.iy + x_ratio = display.width / input_texture.ix + y_ratio = display.height / input_texture.iy if y_ratio < x_ratio: # if y ratio is smaller than x ratio x_ratio = y_ratio # make the ratios the same width, height = input_texture.ix * x_ratio, input_texture.iy * x_ratio # width, height = tex.ix, tex.iy - x_position = (display.width - width)/2 - y_position = (display.height - height)/2 + x_position = (display.width - width) / 2 + y_position = (display.height - height) / 2 return width, height, x_position, y_position -def list_files(directory, reverse=False): - """ Return list of files of specified type """ - output = [f for f in os.listdir(directory) if - os.path.isfile(os.path.join(directory, f)) and - f.endswith(('.jpg', '.jpeg', '.png'))] - - if reverse is True: - output.sort(key=lambda x: os.stat(os.path.join(directory, x)).st_mtime) - output.reverse() # reverse image list so new files are first - return output - - """ PROCESSES """ @@ -102,7 +77,7 @@ def slideshow(imagelist, cur_queue, killslideshowq): """ run slideshow """ process = "Slideshow Process" - settings = read_settings(process) + settings = services.read_settings('settings.json', default_settings=DEFAULT_SETTINGS) logging.info("Starting slideshow...") working = True while working: @@ -136,7 +111,7 @@ def client_handler(connection, client_address, cur_queue): try: data = pickle.loads(data) logging.debug(data) - except TypeError as e: + except TypeError: logging.exception("Failed to unpickle data") logging.info("Received TCP data: %s from %s" % (data, client_address)) @@ -158,19 +133,19 @@ def client_handler(connection, client_address, cur_queue): logging.info("Nothing to project") elif data["action"] == "slideshow": logging.info("Start Slideshow Process") - ssproc = multiprocessing.Process(target=slideshow, - args=(data["images"], - cur_queue, - killslideshowq)) + ssproc = multiprocessing.Process( + target=slideshow, args=(data["images"], + cur_queue, + killslideshowq)) ssproc.start() slideshowon = True elif data["action"] == "stopslideshow": ssproc.terminate() elif data["action"] == "sync": - images = list_files(IMAGEDIR) + images = services.list_files(IMAGEDIR) print(images) elif data["action"] == "whatsplaying": - settings = read_settings(process) + settings = services.read_settings('settings.json', default_settings=DEFAULT_SETTINGS) networking.send_msg(connection, settings["lastimage"]) else: logging.info("Unkown action: %s", data["action"]) @@ -178,18 +153,12 @@ def client_handler(connection, client_address, cur_queue): def tcp_receiver(cur_queue, connections): """ Receive images via TCP """ - # TCP config - tcp_ip = "127.0.0.1" # local only - tcp_port = 5006 - # Set up TCP # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - logging.info('Starting up on %s port %s' % (tcp_ip, tcp_port)) - + logging.info('Starting up on %s port %s' % (TCP_IP, TCP_PORT)) # bind server to address - sock.bind((tcp_ip, tcp_port)) + sock.bind((TCP_IP, TCP_PORT)) sock.listen(1) cons = {} @@ -198,45 +167,52 @@ def tcp_receiver(cur_queue, connections): logging.info("Waiting for a connection...") # Not sure if this is really necessary however, # It gives the connection a unique number - # and adds it to the connactions dictionary + # and adds it to the connections dictionary con_number = str(len(connections) + 1) logging.debug(con_number) cons[con_number], client_address = sock.accept() logging.info("Starting a new thread for client %s", str(client_address)) - thread.start_new_thread(client_handler, (cons[con_number], - client_address, cur_queue)) - + threading.Thread( + target=client_handler, + args=(cons[con_number], client_address, cur_queue) + ) class Carousel(object): """ The main object """ + display_settings = { + "background": (0.0, 0.0, 0.0, 1.0), "frames_per_second": 20} - def __init__(self, display): + def __init__(self, test=False): self.imagedict = {} self.process = "Carousel" + if test: + self.display = pi3d.Display.create( + w=800, h=600, **self.display_settings) + else: + self.display = pi3d.Display.create(**self.display_settings) self.shader = pi3d.Shader("2d_flat") - self.display = display - # Load the last image used - try: - # If there is a settings file, load most recent image - settings = read_settings(self.process) - # Add the image to the queue - logging.info("Last image: %s" % settings["lastimage"]) - starting_image = settings["lastimage"] - except: - logging.exception("Failed to load settings file") - logging.info("Loading default image") - # Write new image to settings - settings = read_settings(self.process) - settings["lastimage"] = "static/images/logo.jpg" - write_settings(self.process, settings) - settings = read_settings(self.process) - # Load default image into queue - starting_image = settings["lastimage"] - + starting_image = self.get_starting_image() self.set_up_image(starting_image, 1, 0.1) self.focus = starting_image + # Set up camera + self.camera = pi3d.Camera.instance() + self.camera.was_moved = False + self.keyboard = pi3d.Keyboard() + self.image_queue = multiprocessing.Queue() + + def get_starting_image(self): + # Load the last image used + # If there is a settings file, load most recent image + settings = services.read_settings( + settings_file='settings.json', default_settings=DEFAULT_SETTINGS) + if not settings: + services.write_settings(DEFAULT_SETTINGS, settings_file='settings.json') + settings = services.read_settings( + settings_file='settings.json', default_settings=DEFAULT_SETTINGS) + logging.info("Last image: %s" % settings["lastimage"]) + return settings["lastimage"] def set_up_image(self, image, alpha, z_position): texture = pi3d.Texture(image, blend=True, mipmap=True) @@ -281,14 +257,11 @@ def pick(self, new_image): self.focus = new_image # Change the focused image # Write new image to settings - settings = read_settings(self.process) - settings["lastimage"] = new_image - write_settings(self.process, settings) - settings = read_settings(self.process) + services.update_setting('lastimage', new_image, file_name='settings.json') def update(self): """ Update image alphas """ - for image, detail in self.imagedict.iteritems(): + for image, detail in self.imagedict.items(): alpha = detail["canvas"].alpha() if detail["fading"] and alpha < 1: alpha += ALPHA_STEP @@ -306,7 +279,7 @@ def draw(self): first_image = None second_image = None - for image, detail in self.imagedict.iteritems(): + for image, detail in self.imagedict.items(): if detail["visible"] and detail["fading"]: first_image = detail["canvas"] elif detail["visible"]: @@ -317,52 +290,41 @@ def draw(self): if second_image: second_image.draw() + def loop(self, tcpprocess, connections): + logging.info("Start Projector process") + while self.display.loop_running(): + self.update() + self.draw() + + k = self.keyboard.read() + if k > -1: + if k == 27: + self.keyboard.close() + self.display.stop() + for connection in connections: + logging.info("Close connnection %s" % connection) + connection.shutdown(socket.SHUT_RDWR) + connection.close() + tcpprocess.terminate() + + # Check if there is a new image to be displayed + if not self.image_queue.empty(): + new_image = self.image_queue.get() + logging.info("New image is: %s", new_image) + self.pick(new_image) + def main(test): # Set up image queue - IMAGEQ = multiprocessing.Queue() connections = multiprocessing.Manager().dict() + crsl = Carousel(test) logging.info("Start TCP Receiver") tcpprocess = multiprocessing.Process(target=tcp_receiver, - args=(IMAGEQ, connections)) + args=(crsl.image_queue, connections)) tcpprocess.start() - logging.info("Start Projector process") - if test: - display = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), - frames_per_second=20, w=800, h=600) - else: - display = pi3d.Display.create(background=(0.0, 0.0, 0.0, 1.0), - frames_per_second=20) - crsl = Carousel() - - # Set up camera - CAMERA = pi3d.Camera.instance() - CAMERA.was_moved = False - KEYBOARD = pi3d.Keyboard() - - while display.loop_running(): - crsl.update() - crsl.draw() - - k = KEYBOARD.read() - if k > -1: - if k == 27: - KEYBOARD.close() - display.stop() - if len(connections) > 0: - for connection in connections: - logging.info("Close connnection %s" % connection) - connection.shutdown(socket.SHUT_RDWR) - connection.close() - tcpprocess.terminate() - - # Check if there is a new image to be displayed - if not IMAGEQ.empty(): - new_image = IMAGEQ.get() - logging.info("New image is: %s", new_image) - crsl.pick(new_image) + crsl.loop(tcpprocess, connections) if __name__ == "__main__": diff --git a/requirements.txt b/requirements.txt index 86de72b..6651ee3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ +numpy pi3d -yaml -pil -flask \ No newline at end of file +Pillow +flask +flask_sqlalchemy diff --git a/server.py b/server.py index 68618ba..6ee9c23 100644 --- a/server.py +++ b/server.py @@ -2,13 +2,12 @@ """ Web interface for the projector """ import logging import os -import random import socket from flask import Flask, render_template, request, redirect from flask.views import MethodView -from PIL import Image import settings import services +from . import networking from orm import db @@ -36,16 +35,16 @@ def get(self, display="local"): # Ask screen what is currently displayed message = {"action": "whatsplaying"} - services.send_msg_to_display(display, message) + networking.send_msg_to_display(display, message) # Wait for reply try: - current_image = services.recv_msg(services.connections[display]) + current_image = networking.recv_msg(services.connections[display]) logging.info("Received TCP data: %s from %s" % (current_image, services.connections[display])) except socket.timeout: logging.info("No reply, socket timed out") - except KeyError as e: + except KeyError: logging.info("Display not found") images = services.db_list_images() @@ -76,7 +75,7 @@ def post(self, display="local"): logging.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) message = {"action": "project", "images": [os.path.join(settings.IMAGEDIR, prop1)]} - services.send_msg_to_display(display, message) + networking.send_msg_to_display(display, message) logging.info("Image to be projected is %s" % prop1) services.PROJECTRS[display]["current"] = prop1 return True @@ -84,7 +83,7 @@ def post(self, display="local"): # TODO: This doesn't work at all imagelist = [v for k, v in request.args.iteritems() if k != "action"] message = {"action": "slideshow", "images": imagelist} - services.send_msg_to_display(display, message) + networking.send_msg_to_display(display, message) return redirect('/') else: logging.info("Unknown Action %s" % action) @@ -129,21 +128,21 @@ def post(self): try: os.remove(os.path.join(settings.IMAGEDIR, image)) logging.info("%s deleted" % image) - except OSError as e: + except OSError: logging.exception("Deleting %s failed" % image) - db.delete('images', where="filename=$image", vars=locals()) + else: + services.db_delete_image(image) return redirect('/') app.add_url_rule('/delete', view_func=Delete.as_view('delete')) class Rename(MethodView): def get(self): - image = request.args["image"] - imagename = db.select('images', where="filename=$image", - vars=locals())[0]["imagename"] + filename = request.args["image"] + imagename = services.db_get_image(filename)[0]["imagename"] return render_template("rename.html", pagetitle="Rename", - image=image, + image=filename, displays=services.PROJECTRS, message=imagename) @@ -195,7 +194,7 @@ def get(self): def post(self): display = request.args["prop1"] message = {"action": "sync"} - services.send_msg_to_display(display, message) + networking.send_msg_to_display(display, message) logging.info("sync %s" % display) app.add_url_rule('/displays', view_func=Displays.as_view('displays')) @@ -216,7 +215,7 @@ def post(self): message = {"action": "project", "images": ['static/img/shutdown.jpg']} # Should probably cycle through display and switch them off - services.send_msg_to_display("local", message) + networking.send_msg_to_display("local", message) os.system("poweroff") else: logging.info("Don't shutdown") @@ -226,10 +225,7 @@ def post(self): class Videos(MethodView): def get(self): - videolist = [f for f in os.listdir(settings.VIDEODIR) if - os.path.isfile(os.path.join(settings.VIDEODIR, f)) and - f.endswith(('.mp4', '.webm'))] - + videolist = services.list_files(settings.VIDEODIR, video=True) logging.info("User accessed index") return render_template("videos.html", videolist=videolist, @@ -248,7 +244,7 @@ def post(self, display="local"): logging.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) message = {"action": "project", "video": [os.path.join(settings.VIDEODIR, prop1)]} - services.send_msg_to_display(display, message) + networking.send_msg_to_display(display, message) logging.info("video to be projected is %s" % prop1) return True else: diff --git a/services.py b/services.py index 2958d63..474b774 100644 --- a/services.py +++ b/services.py @@ -1,15 +1,22 @@ import logging import os -import pickle import random -import struct -import socket -import yaml + +import json from PIL import Image import settings from orm import db -connections = {} + +def db_get_image(filename): + return db.select( + 'images', + where="filename=$filename", + vars=locals()) + + +def db_delete_image(filename): + db.delete('images', where="filename=$filename", vars=locals()) def db_list_images(): @@ -29,24 +36,44 @@ def db_insert_image(filename): return filename -def write_settings(data): +def write_settings(data, settings_file='uisettings.json'): """Write the previous image to settings file""" logging.info("Writing settings...") logging.info(data) - with open('uisettings.yml', 'w') as outfile: - outfile.write(yaml.dump(data, default_flow_style=True)) + with open(settings_file, 'w') as outfile: + json.dump(data, outfile) -def read_settings(): +def read_settings(settings_file='uisettings.json', default_settings=None): """ Read settings from YAML file""" logging.info("Read settings...") try: - return yaml.load(open("uisettings.yml")) - except (IOError, yaml.composer.ComposerError): + with open(settings_file) as infile: + data = json.load(infile) + return data + except IOError: + logging.exception("Could not read settings") + if default_settings: + logging.info("Writing default settings file") + with open(settings_file, 'w') as outfile: + json.dump(default_settings, outfile) + return default_settings + except ValueError: logging.exception("Could not read settings") return None +def update_setting(setting_name, value, file_name='uisettings.json'): + settings = read_settings(settings_file=file_name) + settings[setting_name] = value + write_settings(settings, settings_file=file_name) + + +def get_setting(setting_name, file_name='uisettings.json'): + settings = read_settings(settings_file=file_name) + return settings[setting_name] + + def rename_image(filename): """ Rename files with random string to ensure there are no clashes """ randomstring = random.getrandbits(16) @@ -83,11 +110,12 @@ def make_thumbnail(imagepath): os.remove(imagepath) -def list_files(directory, reverse=False): +def list_files(directory, reverse=False, video=False): """ Return list of files of specified type """ + filetypes = ('.mp4', '.webm') if video else ('.jpg', '.jpeg', '.png') output = [f for f in os.listdir(directory) if os.path.isfile(os.path.join(directory, f)) and - f.endswith(('.jpg', '.jpeg', '.png'))] + f.endswith(filetypes)] if reverse: # Sort newFileList by date added(?) @@ -95,156 +123,3 @@ def list_files(directory, reverse=False): output.reverse() # reverse image list so new files are first return output - - -SETTINGS = read_settings() -print(SETTINGS) -PROJECTRS = SETTINGS["projectors"] - - -def connect_display(display): - """ Try and connect to a display - This is a bit of a clusterfuck - should probably re-engineer this """ - # Make a new socket - logging.info("Connections: " % connections) - try: - logging.info("Remove display socket from connections") - del connections[display] - except KeyError as e: - logging.exception("Display doesn't exist in connections") - - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - logging.info("Socket made") - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - logging.info("Connected to %s" % display) - # set socket timeout high - connections[display].settimeout(20) - return True - except: - logging.info("No displays found") - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - return False - - -def send_msg_to_display(display, msg): - """ Send a message to a display """ - logging.info("Send message to display") - msg = pickle.dumps(msg) - logging.info(msg) - sent = False - attempts = 1 - while not sent and attempts < 4: - try: - sock = connections[display] - sent = True - except KeyError: - logging.exception("Display %s doesn't exist" % display) - logging.info("Reattach display, attempt %d" % attempts) - display_connected = connect_display(display) - - if display_connected: - logging.info("Successfully reconnected display") - else: - logging.info("Could not reattach display") - attempts += 1 - - if display not in connections: - logging.info("Could not send message, could not communicate with display") - return - # Prefix each message with a 4-byte length (network byte order) - msg = struct.pack('>I', len(msg)) + msg - - # Try sending message, if broken pipe - # Try creating new socket - sent = False - attempts = 0 - - while not sent and attempts < 3: - try: - logging.info("Attempting to send message") - sock.sendall(msg) - logging.info("Sent message %s" % msg) - # success, quit loop - sent = True - except socket.error as e: - logging.info("Socket error: %s" % e) - logging.info("Attempting to reconnect to display %s" % sock) - logging.info(connections) - - # Make a new socket - del connections[display] - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - logging.info("Socket made") - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - logging.info("Connected to %s" % display) - # set socket timeout high - connections[display].settimeout(20) - except: - logging.info("Something went wrong") - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - attempts += 1 - - -def recv_msg(sock): - # Read message length and unpack it into an integer - raw_msglen = recvall(sock, 4) - if not raw_msglen: - return None - msglen = struct.unpack('>I', raw_msglen)[0] - # Read the message data - return recvall(sock, msglen) - - -def recvall(sock, n): - """Helper function to recv n bytes or return None if EOF is hit""" - data = '' - - while len(data) < n: - packet = sock.recv(n - len(data)) - if not packet: - return None - data += packet - - return data - - -def init_network(): - logging.info("Check display connections") - for display in PROJECTRS: - if not PROJECTRS[display]["enabled"]: - continue - - if display not in connections: - logging.info("Display not in existing connections") - connect_display(display) - else: - send_msg_to_display(display, "alive") - try: - check = recv_msg(connections[display]) - logging.info("Already connected to %s" % display) - except: - logging.info("Connection appears to be dead") - connect_display(display) - logging.info("Connections: %s" % connections) diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..6de4557 --- /dev/null +++ b/settings.json @@ -0,0 +1 @@ +{"slideshow": {"delay": 20, "loop": true}, "fadeduration": 2, "lastimage": "static/images/logo.jpg", "projectors": {"local": {"ip": "127.0.0.1", "port": 5006, "enabled": true, "name": "Main", "current": ""}}} \ No newline at end of file From f88e8cb40509d58496475877f9f1bd4e1278faf2 Mon Sep 17 00:00:00 2001 From: christophski Date: Sun, 28 Jun 2020 10:19:59 +0100 Subject: [PATCH 5/7] Further changes --- networking.py | 260 +++++++++++++++++++++--------------------- orm.py | 11 +- projector.py | 56 +++++---- server.py | 136 ++++++++++------------ services.py | 28 ++--- templates/delete.html | 6 +- 6 files changed, 244 insertions(+), 253 deletions(-) diff --git a/networking.py b/networking.py index f4b3543..95466ba 100644 --- a/networking.py +++ b/networking.py @@ -1,12 +1,12 @@ import struct +import os import socket import logging import pickle +import settings import services -connections = {} - # SENDING AND RECEIVING def send_msg(sock, msg): @@ -39,135 +39,141 @@ def recvall(sock, n): return data -SETTINGS = services.read_settings(settings_file="settings.json") -print(SETTINGS) -if SETTINGS: - PROJECTRS = SETTINGS["projectors"] -else: - PROJECTRS = [] - - -def connect_display(display): - """ Try and connect to a display - This is a bit of a clusterfuck - should probably re-engineer this """ - # Make a new socket - logging.info("Connections: " % connections) - try: - logging.info("Remove display socket from connections") - del connections[display] - except KeyError: - logging.exception("Display doesn't exist in connections") - - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - logging.info("Socket made") - # Set connections timeout low - connections[display].settimeout(1) - - try: - # try connecting to socket - connections[display].connect(display_address) - logging.info("Connected to %s" % display) - # set socket timeout high - connections[display].settimeout(20) - return True - except: - logging.info("No displays found") - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - return False - - -def send_msg_to_display(display, msg): - """ Send a message to a display """ - logging.info("Send message to display") - msg = pickle.dumps(msg) - logging.info(msg) - sent = False - attempts = 1 - while not sent and attempts < 4: +class Connection: + def __init__(self): + SETTINGS = services.read_settings(settings_file="settings.json") + print(SETTINGS) + if SETTINGS: + self.PROJECTRS = SETTINGS["projectors"] + else: + self.PROJECTRS = [] + self.connections = {} + + @staticmethod + def get_display_addr(display): + return (display["ip"], display["port"]) + + def init_network(self): + logging.info("Check display connections") + for display in self.PROJECTRS: + if not self.PROJECTRS[display]["enabled"]: + continue + + if display not in self.connections: + logging.info("Display not in existing connections") + self.connect_display(display) + else: + self.send_msg_to_display(display, "alive") + try: + # Try to receive from connection to see if it is alive + recv_msg(self.connections[display]) + logging.info("Already connected to %s" % display) + except Exception: + logging.exception("Connection appears to be dead") + self.connect_display(display) + logging.info("Connections: %s" % self.connections) + + def connect_display(self, display): + """ Try and connect to a display + This is a bit of a clusterfuck + should probably re-engineer this """ + # Make a new socket + logging.info("Connections: " % self.connections) try: - sock = connections[display] - sent = True + logging.info("Remove display socket from connections") + del self.connections[display] except KeyError: - logging.exception("Display %s doesn't exist" % display) - logging.info("Reattach display, attempt %d" % attempts) - display_connected = connect_display(display) + logging.info("Display doesn't exist in connections") - if display_connected: - logging.info("Successfully reconnected display") - else: - logging.info("Could not reattach display") - attempts += 1 - - if display not in connections: - logging.info("Could not send message, could not communicate with display") - return - # Prefix each message with a 4-byte length (network byte order) - msg = struct.pack('>I', len(msg)) + msg + # Make new socket it and add to connections dict + self.connections[display] = socket.socket(socket.AF_INET, + socket.SOCK_STREAM) + logging.info("Socket made") + # Set connections timeout low + self.connections[display].settimeout(1) - # Try sending message, if broken pipe - # Try creating new socket - sent = False - attempts = 0 - - while not sent and attempts < 3: try: - logging.info("Attempting to send message") - sock.sendall(msg) - logging.info("Sent message %s" % msg) - # success, quit loop - sent = True - except socket.error as e: - logging.info("Socket error: %s" % e) - logging.info("Attempting to reconnect to display %s" % sock) - logging.info(connections) - - # Make a new socket - del connections[display] - # Get display address - display_address = (PROJECTRS[display]["ip"], PROJECTRS[display]["port"]) - # Make new socket it and add to connections dict - connections[display] = socket.socket(socket.AF_INET, - socket.SOCK_STREAM) - logging.info("Socket made") - # Set connections timeout low - connections[display].settimeout(1) - + # try connecting to socket + self.connections[display].connect( + self.get_display_addr(self.PROJECTRS[display])) + logging.info("Connected to %s" % display) + # set socket timeout high + self.connections[display].settimeout(20) + return True + except: + logging.info("No displays found") + # delete socket from dict, because it doesn't work + del self.connections[display] + # increment attempts + return False + + def send_msg_to_display(self, display, msg): + """ Send a message to a display """ + logging.info("Send message to display") + msg = pickle.dumps(msg) + logging.info(msg) + sent = False + attempts = 1 + while not sent and attempts < 4: try: - # try connecting to socket - connections[display].connect(display_address) - logging.info("Connected to %s" % display) - # set socket timeout high - connections[display].settimeout(20) - except Exception: - logging.exception("Something went wrong") - # delete socket from dict, because it doesn't work - del connections[display] - # increment attempts - attempts += 1 - - -def init_network(): - logging.info("Check display connections") - for display in PROJECTRS: - if not PROJECTRS[display]["enabled"]: - continue - - if display not in connections: - logging.info("Display not in existing connections") - connect_display(display) - else: - send_msg_to_display(display, "alive") + sock = self.connections[display] + sent = True + except KeyError: + logging.exception("Display %s doesn't exist" % display) + logging.info("Reattach display, attempt %d" % attempts) + display_connected = self.connect_display(display) + + if display_connected: + logging.info("Successfully reconnected display") + else: + logging.info("Could not reattach display") + attempts += 1 + + if display not in self.connections: + logging.info("Could not send message, could not communicate with display") + return + # Prefix each message with a 4-byte length (network byte order) + msg = struct.pack('>I', len(msg)) + msg + + # Try sending message, if broken pipe + # Try creating new socket + attempts = 0 + while attempts < 3: try: - # Try to receive from connection to see if it is alive - recv_msg(connections[display]) - logging.info("Already connected to %s" % display) - except Exception: - logging.exception("Connection appears to be dead") - connect_display(display) - logging.info("Connections: %s" % connections) + logging.info("Attempting to send message") + sock.sendall(msg) + logging.info("Sent message %s" % msg) + break + except socket.error as e: + logging.info("Socket error: %s" % e) + logging.info("Attempting to reconnect to display %s" % sock) + logging.info(self.connections) + connected = self.connect_display(display) + if not connected: + attempts += 1 + + def whatsplaying(self, display): + message = {"action": "whatsplaying"} + current_image = None + self.send_msg_to_display(display, message) + # Wait for reply + try: + current_image = recv_msg(self.connections[display]) + logging.info("Received TCP data: %s from %s" % + (current_image, self.connections[display])) + except socket.timeout: + logging.info("No reply, socket timed out") + except KeyError: + logging.info("Display not found") + return current_image + + def project(self, display, image): + message = {"action": "project", + "images": [os.path.join(settings.IMAGEDIR, image)]} + self.send_msg_to_display(display, message) + logging.info("Image to be projected is %s" % image) + self.PROJECTRS[display]["current"] = image + + def slideshow(self, display, imagelist): + message = {"action": "slideshow", "images": imagelist} + self.send_msg_to_display(display, message) diff --git a/orm.py b/orm.py index d28a763..db0ff30 100644 --- a/orm.py +++ b/orm.py @@ -1,6 +1,11 @@ -# import web from flask_sqlalchemy import SQLAlchemy -db = None -# db = web.database(dbn="sqlite", db="images.db") +db = SQLAlchemy() + + # CREATE TABLE images(Id INTEGER PRIMARY KEY, filename TEXT, imagename TEXT, folder TEXT); +class Image(db.Model): + Id = db.Column(db.Integer, primary_key=True) + filename = db.Column(db.String()) + imagename = db.Column(db.String()) + folder = db.Column(db.String()) diff --git a/projector.py b/projector.py index 34cc8d4..422450a 100644 --- a/projector.py +++ b/projector.py @@ -24,11 +24,17 @@ logging.basicConfig( filename="projector.log", - level=logging.INFO, format='%(asctime)s %(thread)s %(levelname)-6s %(funcName)s:%(lineno)-5d %(message)s', ) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) client_logger = logging.getLogger('client_handler') +client_logger.setLevel(logging.DEBUG) +client_logger.addHandler(logging.StreamHandler()) projectr_logger = logging.getLogger('projectr') +projectr_logger.setLevel(logging.DEBUG) +projectr_logger.addHandler(logging.StreamHandler()) IMAGEDIR = 'static/images/' ALPHA_STEP = 0.025 @@ -78,7 +84,7 @@ def slideshow(imagelist, cur_queue, killslideshowq): process = "Slideshow Process" settings = services.read_settings('settings.json', default_settings=DEFAULT_SETTINGS) - logging.info("Starting slideshow...") + client_logger.info("Starting slideshow...") working = True while working: for image in imagelist: @@ -86,10 +92,10 @@ def slideshow(imagelist, cur_queue, killslideshowq): if not killslideshowq.empty(): emptyq = killslideshowq.get() if emptyq == "die": - logging.info("Killing slideshow") + client_logger.info("Killing slideshow") working = False # Make sure while loop breaks break # break out of for loop - logging.info("Slideshow: %s", image) + client_logger.info("Slideshow: %s", image) cur_queue.put(image) time.sleep(settings["slideshow"]["delay"] + settings["fadeduration"]) @@ -99,26 +105,26 @@ def client_handler(connection, client_address, cur_queue): # Is there a slideshow running? slideshowon = False killslideshowq = multiprocessing.Queue() - logging.info("Connected to Master") + client_logger.info("Connected to Master") while True: # buffer size is 1024 bytes data = networking.recv_msg(connection) if data == "alive": - logging.info("Alive?") + client_logger.info("Alive?") networking.send_msg(connection, "alive") else: try: data = pickle.loads(data) - logging.debug(data) + client_logger.debug(data) except TypeError: - logging.exception("Failed to unpickle data") + client_logger.exception("Failed to unpickle data") - logging.info("Received TCP data: %s from %s" % (data, client_address)) + client_logger.info("Received TCP data: %s from %s" % (data, client_address)) if slideshowon: # If slideshow is running, kill it - logging.info("Tell Slideshow process to die") + client_logger.info("Tell Slideshow process to die") # Tell slideshow to die killslideshowq.put("die") slideshowon = False @@ -128,11 +134,11 @@ def client_handler(connection, client_address, cur_queue): cur_queue.put(data["images"][0]) slideshowon = False elif "video" in data: - logging.info("Video to project is %s", data["video"]) + client_logger.info("Video to project is %s", data["video"]) else: - logging.info("Nothing to project") + client_logger.info("Nothing to project") elif data["action"] == "slideshow": - logging.info("Start Slideshow Process") + client_logger.info("Start Slideshow Process") ssproc = multiprocessing.Process( target=slideshow, args=(data["images"], cur_queue, @@ -148,7 +154,7 @@ def client_handler(connection, client_address, cur_queue): settings = services.read_settings('settings.json', default_settings=DEFAULT_SETTINGS) networking.send_msg(connection, settings["lastimage"]) else: - logging.info("Unkown action: %s", data["action"]) + client_logger.info("Unkown action: %s", data["action"]) def tcp_receiver(cur_queue, connections): @@ -156,7 +162,7 @@ def tcp_receiver(cur_queue, connections): # Set up TCP # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - logging.info('Starting up on %s port %s' % (TCP_IP, TCP_PORT)) + client_logger.info('Starting up on %s port %s' % (TCP_IP, TCP_PORT)) # bind server to address sock.bind((TCP_IP, TCP_PORT)) sock.listen(1) @@ -164,15 +170,15 @@ def tcp_receiver(cur_queue, connections): cons = {} while True: - logging.info("Waiting for a connection...") + client_logger.info("Waiting for a connection...") # Not sure if this is really necessary however, # It gives the connection a unique number # and adds it to the connections dictionary con_number = str(len(connections) + 1) - logging.debug(con_number) + client_logger.debug(con_number) cons[con_number], client_address = sock.accept() - logging.info("Starting a new thread for client %s", str(client_address)) + client_logger.info("Starting a new thread for client %s", str(client_address)) threading.Thread( target=client_handler, args=(cons[con_number], client_address, cur_queue) @@ -211,7 +217,7 @@ def get_starting_image(self): services.write_settings(DEFAULT_SETTINGS, settings_file='settings.json') settings = services.read_settings( settings_file='settings.json', default_settings=DEFAULT_SETTINGS) - logging.info("Last image: %s" % settings["lastimage"]) + projectr_logger.info("Last image: %s" % settings["lastimage"]) return settings["lastimage"] def set_up_image(self, image, alpha, z_position): @@ -229,7 +235,7 @@ def set_up_image(self, image, alpha, z_position): def pick(self, new_image): """ Pick an image by URL """ if self.focus == new_image: - logging.warning("Image already projected") + projectr_logger.warning("Image already projected") return # Check to see if image already in dictionary @@ -291,7 +297,7 @@ def draw(self): second_image.draw() def loop(self, tcpprocess, connections): - logging.info("Start Projector process") + projectr_logger.info("Start Projector process") while self.display.loop_running(): self.update() self.draw() @@ -302,7 +308,7 @@ def loop(self, tcpprocess, connections): self.keyboard.close() self.display.stop() for connection in connections: - logging.info("Close connnection %s" % connection) + projectr_logger.info("Close connnection %s" % connection) connection.shutdown(socket.SHUT_RDWR) connection.close() tcpprocess.terminate() @@ -310,7 +316,7 @@ def loop(self, tcpprocess, connections): # Check if there is a new image to be displayed if not self.image_queue.empty(): new_image = self.image_queue.get() - logging.info("New image is: %s", new_image) + projectr_logger.info("New image is: %s", new_image) self.pick(new_image) @@ -319,7 +325,7 @@ def main(test): connections = multiprocessing.Manager().dict() crsl = Carousel(test) - logging.info("Start TCP Receiver") + logger.info("Start TCP Receiver") tcpprocess = multiprocessing.Process(target=tcp_receiver, args=(crsl.image_queue, connections)) tcpprocess.start() @@ -328,7 +334,7 @@ def main(test): if __name__ == "__main__": - logging.info("Christie's Projector") + logger.info("Christie's Projector") parser = argparse.ArgumentParser(description='Project images.') parser.add_argument("-t", "--test", help="Run projector in a window for testing", diff --git a/server.py b/server.py index 6ee9c23..7dd2ea8 100644 --- a/server.py +++ b/server.py @@ -2,98 +2,75 @@ """ Web interface for the projector """ import logging import os -import socket from flask import Flask, render_template, request, redirect from flask.views import MethodView import settings import services -from . import networking -from orm import db - - +import networking +import orm +db = orm.db logging.basicConfig( filename="server.log", level=logging.INFO, format='%(asctime)s %(thread)s %(levelname)-6s %(funcName)s:%(lineno)-5d %(message)s', ) +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' +db.init_app(app) + +CONNECTION = networking.Connection() class Index(MethodView): def get(self, display="local"): - current_image = None - - # Check if the display exists - if display not in [f for f in services.PROJECTRS]: + if display not in CONNECTION.PROJECTRS.keys(): logging.info("Display %s not found" % (display)) return render_template("displaynotfound.html", pagetitle="Display %s Not Found" % display, - displays=services.PROJECTRS, + displays=CONNECTION.PROJECTRS, message="") - # Ask screen what is currently displayed - message = {"action": "whatsplaying"} - networking.send_msg_to_display(display, message) - - # Wait for reply - try: - current_image = networking.recv_msg(services.connections[display]) - logging.info("Received TCP data: %s from %s" % - (current_image, services.connections[display])) - except socket.timeout: - logging.info("No reply, socket timed out") - except KeyError: - logging.info("Display not found") - - images = services.db_list_images() - imagelist = [(x["filename"], x["imagename"]) for x in images] - - if current_image is None: - return render_template( - "index.html", - imagelist=imagelist, - pagetitle="Display - %s" % services.PROJECTRS[display]["name"], - current_image=current_image, - displays=services.PROJECTRS, - message="") + # current_image = CONNECTION.whatsplaying("local") # FIXME + current_image = None + images = orm.Image.query.all() + imagelist = [(x.filename, x.imagename) for x in images] return render_template( "index.html", imagelist=imagelist, - pagetitle="Display - %s" % services.PROJECTRS[display]["name"], - current_image=current_image[14:], - displays=services.PROJECTRS, + pagetitle="Display - %s" % CONNECTION.PROJECTRS[display]["name"], + current_image=current_image[14:] if current_image else current_image, + displays=CONNECTION.PROJECTRS, message="") def post(self, display="local"): action = request.form["action"] if action == "project": - prop1 = request.form["prop1"] # THE IMAGE - prop2 = request.form["prop2"] - logging.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) - message = {"action": "project", - "images": [os.path.join(settings.IMAGEDIR, prop1)]} - networking.send_msg_to_display(display, message) - logging.info("Image to be projected is %s" % prop1) - services.PROJECTRS[display]["current"] = prop1 + image = request.form["prop1"] + logger.info( + "action: %s, image: %s" % (action, image)) + CONNECTION.project("local", image) return True elif action == "slideshow": - # TODO: This doesn't work at all - imagelist = [v for k, v in request.args.iteritems() if k != "action"] - message = {"action": "slideshow", "images": imagelist} - networking.send_msg_to_display(display, message) + # TODO: Does this work? + imagelist = [ + v for k, v in request.args.iteritems() if k != "action"] + CONNECTION.slideshow("local", imagelist) return redirect('/') else: - logging.info("Unknown Action %s" % action) + logger.info("Unknown Action %s" % action) app.add_url_rule('/', view_func=Index.as_view('index')) app.add_url_rule('/display/', view_func=Index.as_view('display')) class initNetwork(MethodView): def get(self): - services.init_network() + networking.init_network() return redirect('/') app.add_url_rule('/initnetwork', view_func=initNetwork.as_view('initnetwork')) @@ -104,7 +81,7 @@ def get(self): return render_template("settings.html", pagetitle="Settings", settingsdict=settingsdict, - displays=services.PROJECTRS, + displays=networking.PROJECTRS, message="") def post(self): @@ -120,18 +97,23 @@ def get(self): image = request.args["image"] return render_template("delete.html", pagetitle="Delete", - displays=services.PROJECTRS, + displays=CONNECTION.PROJECTRS, image=image) def post(self): - image = request.args["image"] + print("---------") + print(request.form) + print("---------") + image = request.form["image"] try: os.remove(os.path.join(settings.IMAGEDIR, image)) - logging.info("%s deleted" % image) + logger.info("%s deleted" % image) except OSError: - logging.exception("Deleting %s failed" % image) + logger.exception("Deleting %s failed" % image) else: - services.db_delete_image(image) + img = db.Image.query.filter_by(imagename=image).one_or_none() + db.session.delete(img) + db.session.commit() return redirect('/') app.add_url_rule('/delete', view_func=Delete.as_view('delete')) @@ -143,7 +125,7 @@ def get(self): return render_template("rename.html", pagetitle="Rename", image=filename, - displays=services.PROJECTRS, + displays=networking.PROJECTRS, message=imagename) def post(self): @@ -159,7 +141,7 @@ class Upload(MethodView): def get(self): return render_template("upload.html", pagetitle="Upload", - displays=services.PROJECTRS, + displays=CONNECTION.PROJECTRS, message="") def post(self): @@ -171,7 +153,11 @@ def post(self): filepath = newimage.filename.replace('\\', '/') # splits the path and chooses the last part (the filename with extension) filename = filepath.split('/')[-1] - filename = services.db_insert_image(filename) + imagename, file_extension = os.path.splitext(filename) + filename = services.rename_image(filename) + image = orm.Image(filename=filename, imagename=imagename) + db.session.add(image) + db.session.commit() newimagepath = os.path.join(settings.IMAGEDIR, filename) newimage.save(newimagepath) @@ -182,12 +168,12 @@ def post(self): class Displays(MethodView): def get(self): - logging.info(services.connections) - alive = [f for f in services.connections] + logger.info(networking.connections) + alive = [f for f in networking.connections] return render_template("displays.html", pagetitle="Displays", - displays=services.PROJECTRS, + displays=networking.PROJECTRS, alive=alive, message="") @@ -195,7 +181,7 @@ def post(self): display = request.args["prop1"] message = {"action": "sync"} networking.send_msg_to_display(display, message) - logging.info("sync %s" % display) + logger.info("sync %s" % display) app.add_url_rule('/displays', view_func=Displays.as_view('displays')) @@ -203,22 +189,22 @@ class Shutdown(MethodView): def get(self): return render_template("shutdown.html", pagetitle="Shutdown?", - displays=services.PROJECTRS, + displays=networking.PROJECTRS, message="") def post(self): shutdown = request.form["shutdown"] - logging.info(shutdown) + logger.info(shutdown) if shutdown == "true": - logging.info("Shutdown") + logger.info("Shutdown") message = {"action": "project", "images": ['static/img/shutdown.jpg']} # Should probably cycle through display and switch them off networking.send_msg_to_display("local", message) os.system("poweroff") else: - logging.info("Don't shutdown") + logger.info("Don't shutdown") return redirect('/') app.add_url_rule('/shutdown', view_func=Shutdown.as_view('shutdown')) @@ -226,11 +212,11 @@ def post(self): class Videos(MethodView): def get(self): videolist = services.list_files(settings.VIDEODIR, video=True) - logging.info("User accessed index") + logger.info("User accessed index") return render_template("videos.html", videolist=videolist, pagetitle="Home", - displays=services.PROJECTRS, + displays=networking.PROJECTRS, message="") def post(self, display="local"): @@ -241,17 +227,17 @@ def post(self, display="local"): if action == "project": prop1 = request.args["prop1"] # THE video prop2 = request.args["prop2"] - logging.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) + logger.info("action: %s, prop1: %s, prop2: %s" % (action, prop1, prop2)) message = {"action": "project", "video": [os.path.join(settings.VIDEODIR, prop1)]} networking.send_msg_to_display(display, message) - logging.info("video to be projected is %s" % prop1) + logger.info("video to be projected is %s" % prop1) return True else: - logging.info("Unknown Action %s" % action) + logger.info("Unknown Action %s" % action) app.add_url_rule('/videos', view_func=Videos.as_view('videos')) if __name__ == "__main__": - services.init_network() + CONNECTION.init_network() app.run(host="localhost", port=8000) diff --git a/services.py b/services.py index 474b774..d6f52fa 100644 --- a/services.py +++ b/services.py @@ -5,34 +5,29 @@ import json from PIL import Image import settings -from orm import db -def db_get_image(filename): +def db_get_image(db, filename): return db.select( 'images', where="filename=$filename", vars=locals()) -def db_delete_image(filename): +def db_delete_image(db, filename): db.delete('images', where="filename=$filename", vars=locals()) -def db_list_images(): +def db_list_images(db, ): """ List images in database """ return db.select('images') -def db_insert_image(filename): - """ Insert an image into the database """ - imagename, file_extension = os.path.splitext(filename) - filename = "%s.jpg" % ''.join(random.choice('0123456789abcdef') for - i in range(16)) - - imageid = db.insert('images', filename=filename, - imagename=imagename, folder="") - logging.info("Added %s to the database as ID %d" % (imagename, imageid)) +def rename_image(filename): + """ Rename files with random string to ensure there are no clashes """ + randomstring = random.getrandbits(16) + filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] + logging.info(filename) return filename @@ -74,13 +69,6 @@ def get_setting(setting_name, file_name='uisettings.json'): return settings[setting_name] -def rename_image(filename): - """ Rename files with random string to ensure there are no clashes """ - randomstring = random.getrandbits(16) - filename = filename[:-4] + '_' + str(randomstring) + filename[-4:] - logging.info(filename) - - def make_thumbnail(imagepath): """ Make thumnail for given image """ # if thumbnail doesn't exist diff --git a/templates/delete.html b/templates/delete.html index 7cb200a..92f371d 100644 --- a/templates/delete.html +++ b/templates/delete.html @@ -2,10 +2,10 @@ {% block content %}
- - Delete $image? + + Delete {{image}}?

-{% endblock %} \ No newline at end of file +{% endblock %} From 1ba854a7f6e0f6a1535eea2ae11fbdb78e76c9cc Mon Sep 17 00:00:00 2001 From: christophski Date: Thu, 2 Jul 2020 13:32:17 +0100 Subject: [PATCH 6/7] Add flake8 config --- .flake8 | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7da1f96 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 From dbb12f221c433725b9162642abbba025b05c3907 Mon Sep 17 00:00:00 2001 From: christophski Date: Thu, 2 Jul 2020 13:37:13 +0100 Subject: [PATCH 7/7] Fix --- services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services.py b/services.py index d6f52fa..0e2e4fd 100644 --- a/services.py +++ b/services.py @@ -18,7 +18,7 @@ def db_delete_image(db, filename): db.delete('images', where="filename=$filename", vars=locals()) -def db_list_images(db, ): +def db_list_images(db, filename): """ List images in database """ return db.select('images')