sync with current state

This commit is contained in:
T
2024-10-28 18:10:03 +01:00
parent 895a744873
commit 8dc0480c18
38 changed files with 1675 additions and 104 deletions

252
main.py
View File

@@ -12,7 +12,10 @@ import time
import sys
import logging
import math
import numpy as np
import string
from collections import OrderedDict
import scipy.misc
logging.basicConfig(filename='pixelserver.log', level=config.LogLevel)
running = True
@@ -71,6 +74,16 @@ class LogReader(threading.Thread):
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 #
@@ -84,6 +97,7 @@ if config.UseGui:
self.cv = threading.Condition()
self.datasource = datasource.addListener(self.cv)
def run(self):
last_frame = time.time()
logging.info("Starting GUI")
sf = config.GuiScaleFactor
screen = pygame.display.set_mode((sf*config.ScreenX, sf*config.ScreenY))
@@ -93,14 +107,26 @@ if config.UseGui:
pass
with self.cv:
self.cv.wait()
data = self.datasource.getData()
screen.fill((0, 255, 0))
for x in range(config.ScreenX):
for y in range(config.ScreenY):
i = x+y*config.ScreenX
color = (data[i*3+0], data[i*3+1], data[i*3+2])
pygame.draw.rect(screen, color, pygame.Rect(sf*x, sf*y, sf, sf))
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(time.time()-(last_frame+1/config.GuiFPS))
#time.sleep(0.01)
last_frame = time.time()
logging.info("Closing GUI")
def join(self):
with self.cv:
@@ -128,19 +154,26 @@ class SerialWriter(threading.Thread):
logging.info("Serial Opened")
with self.cv:
self.cv.wait(timeout = 1/30)
data = self.datasource.getData()
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")*3*256
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
ser.write(b"\01")
ser.write(data)
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 != None:
@@ -154,9 +187,9 @@ class SerialWriter(threading.Thread):
with self.cv:
self.cv.notify_all()
super().join()
def setGamma(self, r, g, b):
def setGamma(self, r, g, b, w):
with self.cv:
self.r, self.g, self.b = r, g, b
self.r, self.g, self.b, self.w = r, g, b, w
self.updateGamma = True
self.cv.notify_all()
@@ -164,31 +197,37 @@ class SerialWriter(threading.Thread):
# App #
########################################################################
class App(threading.Thread):
def __init__(self, cmd, param, listener, is_persistent):
def __init__(self, cmd, param, listener, is_persistent, is_white=False, path="."):
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.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(b"\x00"*config.ScreenX*config.ScreenY*3)
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:
data = os.read(oshandle, config.ScreenX*config.ScreenY*3)
assert len(data) == config.ScreenX*config.ScreenY*3
bytes = 4 if self.is_white else 3
data = os.read(oshandle, config.ScreenX*config.ScreenY*bytes)
assert len(data) == config.ScreenX*config.ScreenY*bytes
buffer = np.frombuffer(data, dtype=np.uint8, count=config.ScreenX*config.ScreenY*bytes)
buffer = buffer.reshape((config.ScreenY, config.ScreenX, bytes))
frame = Frame(buffer, channels=bytes)
self.last_update = time.time()
self.datasource.pushData(data)
self.datasource.pushData(frame)
except Exception as e:
logging.debug("Exception in App.run")
with self.listener:
@@ -197,6 +236,8 @@ class App(threading.Thread):
self.logreader.stop()
self.watchdog.join()
self.logreader.join()
self.app.wait()
logging.debug("App stopped")
def alive(self):
return self.app.poll() == None
def stop(self):
@@ -206,6 +247,8 @@ class App(threading.Thread):
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):
@@ -220,16 +263,17 @@ class App(threading.Thread):
class AppRunner(threading.Thread):
def __init__(self):
super().__init__()
self.last_crashlog = ""
self.currentApp = -1
self.requestedApp = 0
self.intensity = config.DefaultBrightness
self.app = None
self.cv = threading.Condition()
self.param = ""
self.datasource = DataSource(b"\x00"*config.ScreenX*config.ScreenY*3)
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 app, i in zip(config.Apps, range(len(config.Apps))):
if app["persistent"]:
@@ -245,34 +289,41 @@ class AppRunner(threading.Thread):
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"])
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
self.persistent_apps[self.currentApp] = newapp
return newapp
def updateApp(self):
if self.app != None and not self.app.is_persistent:
try:
if self.app != 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.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 == None or not self.app.alive():
if self.app != None:
self.last_crashlog = self.app.getLog()
self.requestedApp = 0
if self.requestedApp != None:
self.updateApp()
self.requestedApp = None
data = bytearray(self.app.datasource.getData())
for i in range(len(data)):
data[i] = int(data[i]*self.intensity)
self.datasource.pushData(data)
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:
@@ -282,8 +333,77 @@ class AppRunner(threading.Thread):
if self.app == None:
return ""
return self.app.getLog()
def setGamma(self, r, g, b):
self.serial.setGamma(r, g, b)
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(str):
allowed_chars = string.ascii_letters+string.digits+"+-*/()."
i = 0
outlist = []
while i != len(str):
if str[i] not in allowed_chars:
raise Exception("Unexpected char "+str[i])
if str[i] not in string.ascii_letters:
i+=1
continue
out = ""
while i != len(str) and str[i] in string.ascii_letters+string.digits:
out += str[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 #
@@ -336,6 +456,10 @@ def apps_start(name, param):
def apps_log():
return runner.getLog()
@route("/apps/crashlog")
def apps_log():
return runner.last_crashlog
@route("/apps/running")
def apps_running():
return config.Apps[runner.currentApp]["name"]
@@ -344,22 +468,56 @@ def apps_running():
def index():
return bottle.static_file("index.html", root='html')
@route("/setgamma/<r>/<g>/<b>")
def setGamma(r, g, b):
@route("/setgamma/<r>/<g>/<b>/<w>")
def setGamma(r, g, b, w):
r = float(r)
g = float(g)
b = float(b)
runner.setGamma(r, g, b)
w = float(w)
runner.setGamma(r, g, b, w)
return "ok"
@route("/setbrightness/<i>")
def setGamma(i):
def setIntensity(i):
i = float(i)
if i < 0 or i > 1:
return "bad_value"
runner.intensity = i
runner.setFilter("0_intensity", MakeBrightnessFilter(i))
return "ok"
@route("/filter/flipx/<do>")
def flipx(do):
if do == "true":
runner.setFilter("1_flipx", FlipXFilter)
else:
runner.removeFilter("1_flipx")
return "ok"
@route("/filter/flipy/<do>")
def flipy(do):
if do == "true":
runner.setFilter("1_flipy", FlipYFilter)
else:
runner.removeFilter("1_flipy")
return "ok"
@route("/filter/img/<name>")
def setfilter(name):
if name == "none":
runner.removeFilter("3_imgfilter")
else:
runner.setFilter("3_imgfilter", MakeBrightnessImageFilter(name))
return "ok"
@post("/filter/expr/")
def filter_expr():
expr = request.forms.get('expr')
if expr == "" or expr == "none":
runner.removeFilter("5_brightnessfunction")
else:
runner.setFilter("5_brightnessfunction", MakeBrightnessExprFilter(expr))
return "ok"
########################################################################
# Startup #
########################################################################
@@ -369,15 +527,21 @@ for app in config.Apps:
app["persistent"] = False
if "guiname" not in app.keys():
app["guiname"] = app["name"]
if "white" not in app.keys():
app["white"] = False
if "path" not in app.keys():
app["path"] = "."
cmd = app["cmd"]
#remove non existing apps
if type(cmd) == str and not os.path.isfile(cmd):
config.Apps.remove(app)
logging.warning("Removed app "+app["name"])
#if type(cmd) == str and not os.path.isfile(cmd):
#config.Apps.remove(app)
#logging.warning("Removed app "+app["name"])
runner = AppRunner()
runner.start()
#runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x"))
run(host=config.WebHost, port=config.WebPort)
########################################################################