2018-10-21 13:19:51 +00:00
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
import config
|
|
|
|
import subprocess
|
|
|
|
import os
|
|
|
|
import serial
|
|
|
|
import threading
|
|
|
|
import json
|
2018-08-26 20:07:06 +00:00
|
|
|
import bottle
|
|
|
|
from bottle import route, run, request, post
|
2018-08-22 16:04:04 +00:00
|
|
|
import time
|
2018-08-22 17:43:16 +00:00
|
|
|
import sys
|
|
|
|
import signal
|
2018-08-22 18:48:26 +00:00
|
|
|
import logging
|
2018-10-21 13:03:56 +00:00
|
|
|
import math
|
2018-08-22 18:48:26 +00:00
|
|
|
|
|
|
|
logging.basicConfig(filename='pixelserver.log',level=config.LogLevel)
|
2018-08-22 17:43:16 +00:00
|
|
|
|
|
|
|
running = True
|
|
|
|
|
2018-08-25 17:01:09 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
def pushData(self, data):
|
|
|
|
self.data = data
|
|
|
|
for listener in self.listeners:
|
|
|
|
with listener:
|
|
|
|
listener.notify_all()
|
|
|
|
|
2018-08-22 17:23:36 +00:00
|
|
|
if config.UseGui:
|
|
|
|
import pygame
|
|
|
|
|
|
|
|
class Gui(threading.Thread):
|
2018-08-25 17:01:09 +00:00
|
|
|
def __init__(self, datasource):
|
2018-08-22 17:23:36 +00:00
|
|
|
super().__init__()
|
2018-08-25 17:01:09 +00:00
|
|
|
self.datasource = datasource
|
|
|
|
self.cv = threading.Condition()
|
|
|
|
self.datasource.addListener(self.cv)
|
2018-08-22 17:23:36 +00:00
|
|
|
|
|
|
|
def run(self):
|
2018-08-22 17:43:16 +00:00
|
|
|
global running
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Starting GUI")
|
2018-08-22 17:23:36 +00:00
|
|
|
sf = config.GuiScaleFactor
|
|
|
|
screen = pygame.display.set_mode((sf*config.ScreenX, sf*config.ScreenY))
|
|
|
|
pygame.display.set_caption("Pixelserver - GUI Vis")
|
2018-08-22 17:43:16 +00:00
|
|
|
while running:
|
2018-08-22 17:23:36 +00:00
|
|
|
for event in pygame.event.get():
|
2018-08-25 17:01:09 +00:00
|
|
|
pass
|
|
|
|
with self.cv:
|
|
|
|
self.cv.wait()
|
|
|
|
data = self.datasource.getData()
|
2018-08-22 17:23:36 +00:00
|
|
|
screen.fill((0, 255, 0))
|
2018-08-25 17:01:09 +00:00
|
|
|
try:
|
|
|
|
for x in range(config.ScreenX):
|
|
|
|
for y in range(config.ScreenY):
|
|
|
|
i = x+y*config.ScreenX
|
|
|
|
r = (data[i*3+0])
|
|
|
|
g = (data[i*3+1])
|
|
|
|
b = (data[i*3+2])
|
|
|
|
|
|
|
|
pygame.draw.rect(screen, (r, g, b), pygame.Rect(sf*x, sf*y, sf, sf))
|
|
|
|
except:
|
|
|
|
pass
|
|
|
|
raise
|
2018-08-22 17:23:36 +00:00
|
|
|
pygame.display.flip()
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Closing GUI")
|
2018-08-25 17:01:09 +00:00
|
|
|
def join(self):
|
|
|
|
with self.cv:
|
|
|
|
self.cv.notify_all()
|
|
|
|
super().join()
|
2018-08-22 16:04:04 +00:00
|
|
|
|
|
|
|
class SerialWriter(threading.Thread):
|
2018-08-25 17:01:09 +00:00
|
|
|
def __init__(self, datasource):
|
2018-08-22 16:04:04 +00:00
|
|
|
super().__init__()
|
2018-08-25 17:01:09 +00:00
|
|
|
self.cv = threading.Condition()
|
|
|
|
self.datasource = datasource
|
|
|
|
self.datasource.addListener(self.cv)
|
2018-10-21 13:03:56 +00:00
|
|
|
self.updateGamma = False
|
|
|
|
|
2018-08-22 16:04:04 +00:00
|
|
|
def run(self):
|
2018-08-22 16:18:43 +00:00
|
|
|
should_connect = True
|
|
|
|
ser = None
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Starting SerialWriter")
|
2018-08-22 17:43:16 +00:00
|
|
|
while running:
|
2018-08-22 16:18:43 +00:00
|
|
|
try:
|
|
|
|
if should_connect:
|
|
|
|
ser = serial.Serial(config.Serial)
|
|
|
|
should_connect = False
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Serial Opened")
|
2018-08-25 17:01:09 +00:00
|
|
|
with self.cv:
|
|
|
|
self.cv.wait(timeout = 1/30)
|
|
|
|
data = self.datasource.getData()
|
2018-10-21 13:03:56 +00:00
|
|
|
with self.cv:
|
|
|
|
if self.updateGamma:
|
2018-10-21 13:55:12 +00:00
|
|
|
buf = bytearray(b"\x00")*3*256
|
|
|
|
self.updateGamma = False
|
2018-10-21 13:03:56 +00:00
|
|
|
r = self.r
|
|
|
|
g = self.g
|
|
|
|
b = self.b
|
|
|
|
for i in range(256):
|
|
|
|
gr = int(math.pow(i/255, r)*255)
|
|
|
|
gg = int(math.pow(i/255, g)*255)
|
|
|
|
gb = int(math.pow(i/255, b)*255)
|
2018-12-28 14:32:16 +00:00
|
|
|
buf[i] = max(0, min(255, gr))
|
|
|
|
buf[i+256] = max(0, min(255, gg))
|
|
|
|
buf[i+512] = max(0, min(255, gb))
|
2018-10-21 13:03:56 +00:00
|
|
|
ser.write(b"\x02")
|
|
|
|
ser.write(buf)
|
|
|
|
self.updateGamma = False
|
2018-08-25 17:01:09 +00:00
|
|
|
ser.write(b"\01")
|
|
|
|
ser.write(data)
|
2018-10-21 13:19:51 +00:00
|
|
|
ser.flush()
|
2018-08-22 18:48:26 +00:00
|
|
|
except Exception as e:
|
2018-08-22 16:18:43 +00:00
|
|
|
if ser != None:
|
|
|
|
ser.close()
|
|
|
|
ser = None
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.warning("Serial was close because: "+str(e))
|
2018-08-22 16:18:43 +00:00
|
|
|
should_connect = True
|
2018-08-25 17:01:09 +00:00
|
|
|
time.sleep(5)
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Closing SerialWriter")
|
2018-08-25 17:01:09 +00:00
|
|
|
def joint(self):
|
|
|
|
self.cv.notify_all()
|
|
|
|
super().join()
|
2018-08-19 18:47:08 +00:00
|
|
|
|
2018-10-21 13:03:56 +00:00
|
|
|
def setGamma(self, r, g, b):
|
|
|
|
with self.cv:
|
|
|
|
self.r = r
|
|
|
|
self.g = g
|
|
|
|
self.b = b
|
|
|
|
self.updateGamma = True
|
|
|
|
self.cv.notify_all()
|
|
|
|
|
2018-08-22 16:48:43 +00:00
|
|
|
class WatchDog(threading.Thread):
|
|
|
|
def __init__(self, check, action):
|
|
|
|
super().__init__()
|
|
|
|
self.check = check
|
|
|
|
self.action = action
|
2018-08-25 17:01:09 +00:00
|
|
|
self.running = True
|
|
|
|
|
2018-08-22 16:48:43 +00:00
|
|
|
def run(self):
|
2018-08-25 17:01:09 +00:00
|
|
|
while running and self.running:
|
2018-08-22 16:48:43 +00:00
|
|
|
if self.check():
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.error("Watchdog: Executed")
|
2018-08-22 16:48:43 +00:00
|
|
|
self.action()
|
|
|
|
time.sleep(1)
|
2018-08-25 17:01:09 +00:00
|
|
|
def stop(self):
|
|
|
|
self.running = False
|
2018-08-19 18:47:08 +00:00
|
|
|
|
2018-08-22 18:05:57 +00:00
|
|
|
class LogReader(threading.Thread):
|
|
|
|
def __init__(self, runner):
|
|
|
|
super().__init__()
|
|
|
|
self.runner = runner
|
|
|
|
self.log = ""
|
2018-08-25 17:01:09 +00:00
|
|
|
self.running = True
|
2018-08-22 18:05:57 +00:00
|
|
|
def clear(self):
|
|
|
|
self.log = ""
|
|
|
|
|
2018-08-25 17:01:09 +00:00
|
|
|
def getLog(self):
|
|
|
|
return self.log
|
|
|
|
|
2018-08-22 18:05:57 +00:00
|
|
|
def run(self):
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("LogReader started")
|
2018-08-25 17:01:09 +00:00
|
|
|
while running and self.running:
|
2018-08-22 18:05:57 +00:00
|
|
|
try:
|
2018-10-21 13:19:51 +00:00
|
|
|
self.log += self.runner.app.stderr.read(1).decode("utf-8")
|
2018-08-22 18:05:57 +00:00
|
|
|
except Exception as e:
|
2018-10-21 13:19:51 +00:00
|
|
|
print(e)
|
2018-08-25 17:01:09 +00:00
|
|
|
time.sleep(1)
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("LogReader closed")
|
2018-08-25 17:01:09 +00:00
|
|
|
def stop(self):
|
|
|
|
self.running = False
|
|
|
|
|
|
|
|
class App(threading.Thread):
|
2018-08-26 18:31:48 +00:00
|
|
|
def __init__(self, cmd, param, listener, is_persistent):
|
2018-08-25 17:01:09 +00:00
|
|
|
super().__init__()
|
|
|
|
#start app
|
|
|
|
if type(cmd) != list:
|
|
|
|
cmd = [cmd,]
|
|
|
|
args = cmd+[str(config.ScreenX), str(config.ScreenY), param]
|
|
|
|
self.app = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
|
|
|
|
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(b"\x00"*config.ScreenX*config.ScreenY*3)
|
|
|
|
self.running = running
|
|
|
|
self.listener = listener
|
2018-08-26 18:31:48 +00:00
|
|
|
self.is_persistent = is_persistent
|
2018-08-25 17:01:09 +00:00
|
|
|
|
|
|
|
def run(self):
|
|
|
|
while running and self.running and self.alive():
|
|
|
|
oshandle = self.app.stdout.fileno()
|
|
|
|
try:
|
|
|
|
data = os.read(oshandle, config.ScreenX*config.ScreenY*3)
|
|
|
|
assert len(data) == config.ScreenX*config.ScreenY*3
|
|
|
|
self.last_update = time.time()
|
|
|
|
self.datasource.pushData(data)
|
|
|
|
except Exception as e:
|
|
|
|
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()
|
2018-08-22 18:05:57 +00:00
|
|
|
|
|
|
|
|
2018-08-25 17:01:09 +00:00
|
|
|
def alive(self):
|
|
|
|
return self.app.poll() == None
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.running = False
|
|
|
|
self.app.kill()
|
|
|
|
self.app.stdout.close()
|
|
|
|
self.app.stderr.close()
|
|
|
|
self.watchdog.stop()
|
|
|
|
self.logreader.stop()
|
|
|
|
|
|
|
|
def getLog(self):
|
|
|
|
return self.logreader.getLog()
|
|
|
|
|
2018-08-26 18:31:48 +00:00
|
|
|
def isPersistent(self):
|
|
|
|
return self.is_persistent
|
|
|
|
|
2018-08-25 17:01:09 +00:00
|
|
|
def terminateApp(self):
|
|
|
|
logging.error("Terminate app!")
|
|
|
|
self.stop()
|
|
|
|
|
|
|
|
|
|
|
|
def isAppTimedOut(self):
|
|
|
|
return time.time()-self.last_update > config.NoDataTimeout
|
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
class AppRunner(threading.Thread):
|
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
|
|
|
self.currentApp = -1
|
|
|
|
self.requestedApp = 0
|
|
|
|
self.app = None
|
2018-08-25 17:01:09 +00:00
|
|
|
self.cv = threading.Condition()
|
2018-08-19 18:47:08 +00:00
|
|
|
self.param = ""
|
2018-08-25 17:01:09 +00:00
|
|
|
self.datasource = DataSource(b"\x00"*config.ScreenX*config.ScreenY*3)
|
|
|
|
self.serial = SerialWriter(self.datasource)
|
2018-08-22 16:04:04 +00:00
|
|
|
self.serial.start()
|
2018-08-26 18:31:48 +00:00
|
|
|
self.persistent_apps = {}
|
2018-12-30 14:55:59 +00:00
|
|
|
#start persistent apps
|
|
|
|
for app, i in zip(config.Apps, range(len(config.Apps))):
|
|
|
|
if app["persistent"]:
|
|
|
|
newapp = App(app["cmd"], "", self.cv, is_persistent=True)
|
|
|
|
newapp.datasource.addListener(self.cv)
|
|
|
|
newapp.start()
|
|
|
|
self.persistent_apps[i] = newapp
|
|
|
|
|
2018-08-22 17:23:36 +00:00
|
|
|
if config.UseGui:
|
2018-08-25 17:01:09 +00:00
|
|
|
self.gui = Gui(self.datasource)
|
2018-08-22 17:23:36 +00:00
|
|
|
self.gui.start()
|
2018-08-19 18:47:08 +00:00
|
|
|
|
|
|
|
def requestApp(self, app, param=""):
|
2018-08-25 17:01:09 +00:00
|
|
|
with self.cv:
|
2018-08-19 18:47:08 +00:00
|
|
|
self.requestedApp = app
|
|
|
|
self.param = param
|
2018-08-25 17:01:09 +00:00
|
|
|
self.cv.notify_all()
|
|
|
|
logging.info("Requesting app: "+str(app))
|
2018-08-22 16:48:43 +00:00
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
def updateApp(self):
|
2018-08-26 18:31:48 +00:00
|
|
|
if self.app != None and not self.app.isPersistent():
|
2018-08-25 17:01:09 +00:00
|
|
|
self.app.stop()
|
2018-08-26 18:31:48 +00:00
|
|
|
|
|
|
|
self.currentApp = self.requestedApp
|
|
|
|
if self.currentApp in self.persistent_apps.keys():
|
|
|
|
self.app = self.persistent_apps[self.currentApp]
|
|
|
|
else:
|
2018-08-26 20:07:06 +00:00
|
|
|
persistent = config.Apps[self.requestedApp]["persistent"]
|
2018-08-26 18:31:48 +00:00
|
|
|
|
|
|
|
logging.info("Starting app "+config.Apps[self.requestedApp]["name"])
|
|
|
|
cmd = config.Apps[self.requestedApp]["cmd"]
|
|
|
|
logging.debug(str(cmd))
|
|
|
|
self.app = App(cmd, self.param, self.cv, is_persistent=persistent)
|
|
|
|
self.app.datasource.addListener(self.cv)
|
|
|
|
self.app.start()
|
|
|
|
if persistent:
|
|
|
|
self.persistent_apps[self.currentApp] = self.app
|
2018-08-19 18:47:08 +00:00
|
|
|
|
|
|
|
def run(self):
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Starting Apprunner")
|
2018-08-22 17:43:16 +00:00
|
|
|
while running:
|
2018-08-25 17:01:09 +00:00
|
|
|
with self.cv:
|
|
|
|
if self.app == None or not self.app.alive():
|
2018-08-19 18:47:08 +00:00
|
|
|
self.requestedApp = 0
|
2018-08-22 16:21:59 +00:00
|
|
|
if self.requestedApp != None:
|
2018-08-26 18:31:48 +00:00
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
self.updateApp()
|
2018-08-22 16:21:59 +00:00
|
|
|
self.requestedApp = None
|
2018-08-25 17:01:09 +00:00
|
|
|
|
|
|
|
self.cv.wait()
|
|
|
|
if self.app != None:
|
|
|
|
data = self.app.datasource.getData()
|
|
|
|
self.datasource.pushData(data)
|
2018-08-22 17:43:16 +00:00
|
|
|
self.serial.join()
|
|
|
|
if config.UseGui:
|
|
|
|
self.gui.join()
|
2018-08-22 18:48:26 +00:00
|
|
|
logging.info("Close Apprunner")
|
|
|
|
|
2018-08-22 18:05:57 +00:00
|
|
|
def getLog(self):
|
2018-08-25 17:01:09 +00:00
|
|
|
if self.app == None:
|
|
|
|
return ""
|
|
|
|
return self.app.getLog()
|
2018-10-21 13:03:56 +00:00
|
|
|
def setGamma(self, r, g, b):
|
|
|
|
self.serial.setGamma(r, g, b)
|
2018-08-25 17:01:09 +00:00
|
|
|
|
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
|
2018-08-26 20:07:06 +00:00
|
|
|
|
|
|
|
#normalize config
|
|
|
|
for app in config.Apps:
|
|
|
|
if "persistent" not in app.keys():
|
|
|
|
app["persistent"] = False
|
|
|
|
if "guiname" not in app.keys():
|
|
|
|
app["guiname"] = app["name"]
|
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
runner = AppRunner()
|
|
|
|
runner.start()
|
|
|
|
|
2018-10-21 14:09:48 +00:00
|
|
|
@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'
|
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
@route("/apps/list")
|
|
|
|
def apps_list():
|
|
|
|
s = []
|
|
|
|
for app in config.Apps:
|
2018-08-26 20:07:06 +00:00
|
|
|
s.append({
|
|
|
|
"name": app["name"],
|
|
|
|
"guiname": app["guiname"],
|
|
|
|
"persistent": app["persistent"],
|
|
|
|
})
|
2018-08-19 18:47:08 +00:00
|
|
|
return json.dumps(s)
|
|
|
|
|
|
|
|
@route("/apps/start/<name>")
|
2018-08-26 20:07:06 +00:00
|
|
|
def apps_start_param(name):
|
2018-08-19 18:47:08 +00:00
|
|
|
for i in range(len(config.Apps)):
|
|
|
|
if config.Apps[i]["name"] == name:
|
|
|
|
runner.requestApp(i)
|
|
|
|
return "ok"
|
|
|
|
return "not_found"
|
|
|
|
|
2018-08-26 20:07:06 +00:00
|
|
|
@post("/apps/start/<name>")
|
|
|
|
def apps_start_post(name):
|
|
|
|
param = request.forms.get('param')
|
|
|
|
for i in range(len(config.Apps)):
|
|
|
|
if config.Apps[i]["name"] == name:
|
|
|
|
runner.requestApp(i, param)
|
|
|
|
return "ok"
|
|
|
|
return "not_found"
|
|
|
|
|
|
|
|
|
2018-08-19 18:47:08 +00:00
|
|
|
@route("/apps/start/<name>/<param>")
|
|
|
|
def apps_start(name, param):
|
|
|
|
for i in range(len(config.Apps)):
|
|
|
|
|
|
|
|
if config.Apps[i]["name"] == name:
|
|
|
|
runner.requestApp(i, param)
|
|
|
|
return "ok"
|
|
|
|
return "not_found"
|
|
|
|
|
2018-08-22 18:05:57 +00:00
|
|
|
@route("/apps/log")
|
|
|
|
def apps_log():
|
|
|
|
return runner.getLog()
|
2018-08-19 18:47:08 +00:00
|
|
|
|
|
|
|
@route("/apps/running")
|
|
|
|
def apps_running():
|
|
|
|
i = runner.currentApp
|
|
|
|
return config.Apps[i]["name"]
|
|
|
|
|
2018-08-22 19:15:32 +00:00
|
|
|
@route("/")
|
|
|
|
def index():
|
2018-08-26 20:07:06 +00:00
|
|
|
return bottle.static_file("index.html", root='html')
|
|
|
|
#return open("html/index.html").read()
|
2018-10-21 13:03:56 +00:00
|
|
|
@route("/setgamma/<r>/<g>/<b>")
|
|
|
|
def setGamma(r, g, b):
|
|
|
|
r = float(r)
|
|
|
|
g = float(g)
|
|
|
|
b = float(b)
|
|
|
|
runner.setGamma(r, g, b)
|
2018-10-21 14:09:48 +00:00
|
|
|
return "ok"
|
2018-08-22 17:43:16 +00:00
|
|
|
|
2018-08-26 20:07:06 +00:00
|
|
|
run(host=config.WebHost, port=config.WebPort)
|
2018-08-22 18:48:26 +00:00
|
|
|
running = False
|
2018-08-22 19:15:32 +00:00
|
|
|
runner.join()
|