#!/usr/bin/env python3 import json import logging import math import os import string import subprocess import threading import time from collections import OrderedDict import bottle import numpy as np import scipy.misc import serial import config logging.basicConfig(filename='pixelserver.log', level=config.LogLevel) running = True ######################################################################## # Utils # ######################################################################## class DataSource: def __init__(self, initial): self.data = initial self.listeners = [] def getData(self): return self.data def addListener(self, listener): self.listeners.append(listener) return self def pushData(self, data): self.data = data for listener in self.listeners: with listener: listener.notify_all() class WatchDog(threading.Thread): def __init__(self, check, action): super().__init__(daemon=True) self.check = check self.action = action self.running = True def run(self): while running and self.running: if self.check(): logging.error("Watchdog: Executed") self.action() time.sleep(1) def stop(self): self.running = False class LogReader(threading.Thread): def __init__(self, runner): super().__init__(daemon=True) self.runner = runner self.log = "" self.running = True def clear(self): self.log = "" def getLog(self): return self.log def run(self): logging.info("LogReader started") while running and self.running: try: self.log += self.runner.app.stderr.read(1).decode("utf-8") except Exception as e: print(e) logging.error(str(e)) time.sleep(1) logging.info("LogReader closed") def stop(self): self.running = False class Frame: def __init__(self, buffer, channels=3): self.buffer = buffer self.created = time.time() self.channels = channels def clone(self): f = Frame(self.buffer + 0, self.channels) f.created = self.created return f ######################################################################## # GUI # ######################################################################## if config.UseGui: import pygame class Gui(threading.Thread): def __init__(self, datasource): super().__init__(daemon=True) self.cv = threading.Condition() self.datasource = datasource.addListener(self.cv) def run(self): last_frame = time.time() logging.info("Starting GUI") sf = config.GuiScaleFactor pygame.init() screen = pygame.display.set_mode((sf * config.ScreenX, sf * config.ScreenY)) pygame.display.set_caption("Pixelserver - GUI Vis") while running: for event in pygame.event.get(): pass with self.cv: self.cv.wait() frame = self.datasource.getData() screen.fill((0, 0, 0)) if frame.channels == 3: for x in range(config.ScreenX): for y in range(config.ScreenY): color = (frame.buffer[y, x, 0], frame.buffer[y, x, 1], frame.buffer[y, x, 2]) pygame.draw.rect(screen, color, pygame.Rect(sf * x, sf * y, sf, sf)) elif frame.channels == 4: for x in range(config.ScreenX): for y in range(config.ScreenY): w = frame.buffer[y, x, 3] // 2 color = (frame.buffer[y, x, 0] // 2 + w, frame.buffer[y, x, 1] // 2 + w, frame.buffer[y, x, 2] // 2 + w) pygame.draw.rect(screen, color, pygame.Rect(sf * x, sf * y, sf, sf)) # logging.debug("Time to gui: " + str(time.time() - frame.created)) pygame.display.flip() if time.time() < last_frame + 1 / config.GuiFPS: time.sleep(max(0.01, time.time() - (last_frame + 1 / config.GuiFPS))) # time.sleep(0.01) last_frame = time.time() logging.info("Closing GUI") def join(self, **kwargs): with self.cv: self.cv.notify_all() super().join() ######################################################################## # Serial # ######################################################################## class SerialWriter(threading.Thread): def __init__(self, datasource): super().__init__(daemon=True) self.cv = threading.Condition() self.datasource = datasource.addListener(self.cv) self.updateGamma = False def run(self): should_connect = True ser = None logging.info("Starting SerialWriter") while running: try: if should_connect: ser = serial.Serial(config.Serial) should_connect = False logging.info("Serial Opened") with self.cv: self.cv.wait(timeout=1 / 30) frame = self.datasource.getData() data = frame.buffer.reshape((config.ScreenX * config.ScreenY * frame.channels,)).astype(np.uint8).tobytes() if self.updateGamma: buf = bytearray(b"\x00") * 4 * 256 for i in range(256): apply = lambda x, g: max(0, min(255, int(math.pow(x / 255, g) * 255))) buf[i] = apply(i, self.r) buf[i + 256] = apply(i, self.g) buf[i + 512] = apply(i, self.b) buf[i + 512 + 256] = apply(i, self.w) ser.write(b"\x02") ser.write(buf) self.updateGamma = False if frame.channels == 3: ser.write(b"\01") ser.write(data) elif frame.channels == 4: ser.write(b"\03") ser.write(data) logging.debug("Time to gui: " + str(time.time() - frame.created)) ser.flush() except Exception as e: if ser is not None: ser.close() ser = None logging.warning(f"Serial was close because: {e}") should_connect = True time.sleep(5) logging.info("Closing SerialWriter") def join(self, **kwargs): with self.cv: self.cv.notify_all() super().join() def setGamma(self, r, g, b, w): with self.cv: self.r, self.g, self.b, self.w = r, g, b, w self.updateGamma = True self.cv.notify_all() ######################################################################## # App # ######################################################################## class App(threading.Thread): def __init__(self, cmd, param, listener, is_persistent, is_white=False, path="."): super().__init__(daemon=True) # start app args = cmd + [str(config.ScreenX), str(config.ScreenY), param] self.app = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, cwd=path) self.last_update = time.time() self.cv = threading.Condition() self.watchdog = WatchDog(lambda: self.isAppTimedOut(), lambda: self.terminateApp()) self.watchdog.start() self.logreader = LogReader(self) self.logreader.start() self.datasource = DataSource(Frame(np.zeros((config.ScreenY, config.ScreenX, 3)))) self.running = True self.listener = listener self.is_persistent = is_persistent self.is_white = is_white def run(self): while running and self.running and self.alive(): oshandle = self.app.stdout.fileno() try: channels = 4 if self.is_white else 3 data = os.read(oshandle, config.ScreenX * config.ScreenY * channels) assert len(data) == config.ScreenX * config.ScreenY * channels buffer = np.frombuffer(data, dtype=np.uint8, count=config.ScreenX * config.ScreenY * channels) buffer = buffer.reshape((config.ScreenY, config.ScreenX, channels)) frame = Frame(buffer, channels=channels) self.last_update = time.time() self.datasource.pushData(frame) except: logging.debug("Exception in App.run") with self.listener: self.listener.notify_all() self.watchdog.stop() self.logreader.stop() self.watchdog.join() self.logreader.join() self.app.wait() logging.debug("App stopped") def alive(self): return self.app.poll() is None def stop(self): self.running = False self.app.kill() self.app.stdout.close() self.app.stderr.close() self.watchdog.stop() self.logreader.stop() self.app.wait() logging.debug("App stopped") def getLog(self): return self.logreader.getLog() def terminateApp(self): logging.error("Terminate app!") self.stop() def isAppTimedOut(self): return time.time() - self.last_update > config.NoDataTimeout ######################################################################## # Main # ######################################################################## class AppRunner(threading.Thread): def __init__(self): super().__init__(daemon=True) self.last_crashlog = "" self.currentApp = -1 self.requestedApp = 0 self.app = None self.cv = threading.Condition() self.param = "" self.datasource = DataSource(Frame(np.zeros((config.ScreenY, config.ScreenX, 3)))) self.serial = SerialWriter(self.datasource) self.serial.start() self.persistent_apps = {} self.filters = OrderedDict() # start persistent apps for i, app in enumerate(config.Apps): if app.persistent: self.startApp(i) if config.UseGui: self.gui = Gui(self.datasource) self.gui.start() def requestApp(self, app, param=""): with self.cv: self.requestedApp = app self.param = param self.cv.notify_all() logging.info("Requesting app: " + str(app)) def startApp(self, i, param=""): app = config.Apps[i] newapp = App(app.cmd, param, self.cv, is_persistent=app.persistent, is_white=app.white, path=app.path) newapp.datasource.addListener(self.cv) newapp.start() if app.persistent: self.persistent_apps[self.currentApp] = newapp return newapp def updateApp(self): try: if self.app is not None and not self.app.is_persistent: self.app.stop() self.currentApp = self.requestedApp if self.currentApp in self.persistent_apps.keys() and self.persistent_apps[self.currentApp].alive(): self.app = self.persistent_apps[self.currentApp] else: self.app = self.startApp(self.requestedApp, self.param) except FileNotFoundError as e: print(e) def run(self): logging.info("Starting Apprunner") while running: with self.cv: if self.app is None or not self.app.alive(): if self.app is not None: self.last_crashlog = self.app.getLog() self.requestedApp = 0 if self.requestedApp is not None: self.updateApp() self.requestedApp = None if self.app is not None: frame = self.app.datasource.getData().clone() # logging.debug("Runner in time: " + str(time.time() - frame.created)) for _, f in self.filters.items(): frame.buffer = f(frame.buffer) # logging.debug("Runner out time: " + str(time.time() - frame.created)) self.datasource.pushData(frame) self.cv.wait() self.serial.join() if config.UseGui: self.gui.join() logging.info("Close Apprunner") def getLog(self): if self.app is None: return "" return self.app.getLog() def setGamma(self, r, g, b, w): self.serial.setGamma(r, g, b, w) def setFilter(self, name, filter): self.filters[name] = filter def removeFilter(self, name): if name in self.filters.keys(): del self.filters[name] ######################################################################## # Filter Api # ######################################################################## def MakeBrightnessFilter(intensity): def filter(img): return (img * intensity).astype(np.uint8) return filter def FlipXFilter(intensity): return intensity[:, ::-1, :] def FlipYFilter(intensity): return intensity[::-1, :, :] def MakeBrightnessImageFilter(name): img = scipy.misc.imread("filter/" + name + ".png", flatten=True) / 255 # img = np.transpose(img) def filter(intensity): intensity = intensity.astype(float) for i in range(intensity.shape[2]): intensity[:, :, i] *= img return intensity.astype(np.uint8) return filter def strings(s): allowed_chars = string.ascii_letters + string.digits + "+-*/()." i = 0 outlist = [] while i != len(s): if s[i] not in allowed_chars: raise Exception("Unexpected char " + s[i]) if s[i] not in string.ascii_letters: i += 1 continue out = "" while i != len(s) and s[i] in string.ascii_letters + string.digits: out += s[i] i += 1 outlist.append(out) return outlist def eval_safer(expr, x, y, t): symbols = {"x": x, "y": y, "t": t, "sin": np.sin, "cos": np.cos, "exp": np.exp, "tan": np.tan} strs = strings(expr) for s in strs: if s not in symbols.keys(): raise Exception("unexpected symbol: " + s) return eval(expr, {}, symbols) def MakeBrightnessExprFilter(expr): t0 = time.time() x, y = np.meshgrid(np.arange(config.ScreenX), np.arange(config.ScreenY)) eval_safer(expr, 0, 0, 0) def filter(intensity): t = time.time() - t0 intensity = intensity.astype(float) filter = 0 * x + eval_safer(expr, x, y, t) filter = np.clip(np.nan_to_num(filter), 0, 1) for i in range(intensity.shape[2]): intensity[:, :, i] *= filter return intensity.astype(np.uint8) return filter ######################################################################## # Web Api # ######################################################################## @bottle.route('/<:re:.*>', method='OPTIONS') def enable_cors_generic_route(): add_cors_headers() @bottle.hook('after_request') def enable_cors_after_request_hook(): add_cors_headers() def add_cors_headers(): bottle.response.headers['Access-Control-Allow-Origin'] = '*' bottle.response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS' bottle.response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' def startApp(name, param=""): for i, app in enumerate(config.Apps): if app.name == name: runner.requestApp(i, param) return "ok" return "not_found" @bottle.route("/apps/list") def apps_list(): return json.dumps([ { "name": app.name, "guiname": app.guiname, "persistent": app.persistent, } for app in config.Apps ]) @bottle.route("/apps/start/") def apps_start_param(name): return startApp(name) @bottle.post("/apps/start/") def apps_start_post(name): param = bottle.request.forms.get('param') return startApp(name, param) @bottle.route("/apps/start//") def apps_start(name, param): return startApp(name, param) @bottle.route("/apps/log") def apps_log(): return runner.getLog() @bottle.route("/apps/crashlog") def apps_log(): return runner.last_crashlog @bottle.route("/apps/running") def apps_running(): return config.Apps[runner.currentApp].name @bottle.route("/") def index(): return bottle.static_file("index.html", root='html') @bottle.route("/") def serve_static(filepath): return bottle.static_file(filepath, root='html') @bottle.route("/setgamma////") def setGamma(r, g, b, w): r = float(r) g = float(g) b = float(b) w = float(w) runner.setGamma(r, g, b, w) return "ok" @bottle.route("/setbrightness/") def setIntensity(i): i = float(i) if not 0 <= i <= 1: return "bad_value" runner.setFilter("0_intensity", MakeBrightnessFilter(i)) return "ok" @bottle.route("/filter/flipx/") def flipx(do): if do == "true": runner.setFilter("1_flipx", FlipXFilter) else: runner.removeFilter("1_flipx") return "ok" @bottle.route("/filter/flipy/") def flipy(do): if do == "true": runner.setFilter("1_flipy", FlipYFilter) else: runner.removeFilter("1_flipy") return "ok" @bottle.route("/filter/img/") def setfilter(name): if name == "none": runner.removeFilter("3_imgfilter") else: runner.setFilter("3_imgfilter", MakeBrightnessImageFilter(name)) return "ok" @bottle.post("/filter/expr/") def filter_expr(): expr = bottle.request.forms.get('expr') if expr == "" or expr == "none": runner.removeFilter("5_brightnessfunction") else: runner.setFilter("5_brightnessfunction", MakeBrightnessExprFilter(expr)) return "ok" ######################################################################## # Startup # ######################################################################## runner = AppRunner() runner.start() # runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x")) bottle.run(host=config.WebHost, port=config.WebPort) ######################################################################## # Shutdown # ######################################################################## running = False runner.join()