Live Painting Mode

This commit is contained in:
T
2025-07-23 09:36:20 +02:00
parent 5b2f7e4b5e
commit 6eb6cec170
4 changed files with 584 additions and 9 deletions

123
main.py
View File

@@ -12,6 +12,11 @@ from collections import OrderedDict
import bottle
import numpy as np
import serial
# noinspection PyUnresolvedReferences
import bottle.ext.websocket as bottle_ws
# noinspection PyUnresolvedReferences
from bottle.ext.websocket import GeventWebSocketServer
import geventwebsocket.websocket
import config
import filters
@@ -27,7 +32,7 @@ class DataSource:
self.data = initial
self.listeners = []
def getData(self):
def getData(self) -> "Frame":
return self.data
def addListener(self, listener):
@@ -89,7 +94,7 @@ class LogReader(threading.Thread):
class Frame:
def __init__(self, buffer, channels=3):
self.buffer = buffer
self.buffer: np.ndarray = buffer
self.created = time.time()
self.channels = channels
@@ -223,13 +228,14 @@ class SerialWriter(threading.Thread):
# App #
########################################################################
class App(threading.Thread):
def __init__(self, cmd, param, listener, is_persistent, is_white=False, path="."):
def __init__(self, name, cmd, param, listener, is_persistent, is_white=False, path="."):
super().__init__(daemon=True)
# start app
self.name = name
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.cv = threading.Condition()
self.watchdog = WatchDog(lambda: self.isAppTimedOut(), lambda: self.terminateApp())
self.watchdog.start()
self.logreader = LogReader(self)
@@ -253,8 +259,8 @@ class App(threading.Thread):
frame = Frame(buffer, channels=channels)
self.last_update = time.time()
self.datasource.pushData(frame)
except:
logging.debug("Exception in App.run")
except Exception as ex:
logging.debug(f"Exception in App.run: {ex}")
with self.listener:
self.listener.notify_all()
self.watchdog.stop()
@@ -288,6 +294,35 @@ class App(threading.Thread):
return time.time() - self.last_update > config.NoDataTimeout
class PixelCanvas(App):
# noinspection PyMissingConstructor
def __init__(self):
threading.Thread.__init__(self, daemon=True)
self.name = "pixelcanvas"
self.running = True
self.is_persistent = True
self.datasource = DataSource(Frame(np.zeros((config.ScreenY, config.ScreenX, 3))))
def run(self):
while running and self.running:
time.sleep(1)
def alive(self):
return True
def stop(self):
pass
def getLog(self):
return ""
def terminateApp(self):
pass
def isAppTimedOut(self):
return False
########################################################################
# Main #
########################################################################
@@ -322,7 +357,10 @@ class AppRunner(threading.Thread):
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)
if app.name == "pixelcanvas":
newapp = PixelCanvas()
else:
newapp = App(app.name, 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:
@@ -451,6 +489,75 @@ def apps_running():
return config.Apps[runner.currentApp].name
@bottle.route("/frame")
def frame():
data = runner.datasource.getData()
return {"data": data.buffer.flatten().tolist(), "channels": data.channels}
@bottle.route("/pixel/<x:int>/<y:int>/<r:int>/<g:int>/<b:int>/<w:int>")
def pixel(x, y, r, g, b, w):
if runner.app.name != "pixelcanvas":
startApp("pixelcanvas")
data = runner.app.datasource.getData().clone()
data.created = time.time()
data.buffer[y][x][0] = r
data.buffer[y][x][1] = g
data.buffer[y][x][2] = b
if data.channels == 4:
data.buffer[y][x][3] = w
runner.datasource.pushData(data)
return "ok"
@bottle.get("/frame_ws", apply=[bottle_ws.websocket])
def frame_ws(ws: geventwebsocket.websocket.WebSocket):
while not ws.closed:
msg = json.loads(ws.receive())
if msg["ty"] == "frame":
data = runner.datasource.getData()
if msg.get("time", None) == str(data.created):
ws.send(json.dumps({
"type": "frame_unchanged",
}, separators=(',', ':')))
else:
ws.send(json.dumps({
"ty": "frame",
"data": data.buffer.flatten().tolist(),
"channels": data.channels,
"time": str(data.created),
}, separators=(',', ':')))
elif msg["ty"] == "pixel":
if runner.app.name != "pixelcanvas":
startApp("pixelcanvas")
x = msg["x"]
y = msg["y"]
data = runner.app.datasource.getData().clone()
data.created = time.time()
data.buffer[y, x, 0] = msg["r"]
data.buffer[y, x, 1] = msg["g"]
data.buffer[y, x, 2] = msg["b"]
if data.channels == 4 and "w" in msg:
data.buffer[y, x, 3] = msg["w"]
runner.app.datasource.pushData(data)
elif msg["ty"] == "fill":
if runner.app.name != "pixelcanvas":
startApp("pixelcanvas")
data = runner.app.datasource.getData().clone()
data.created = time.time()
data.buffer[..., 0] = msg["r"]
data.buffer[..., 1] = msg["g"]
data.buffer[..., 2] = msg["b"]
if data.channels == 4 and "w" in msg:
data.buffer[..., 3] = msg["w"]
runner.app.datasource.pushData(data)
@bottle.route("/")
def index():
return bottle.static_file("index.html", root='html')
@@ -523,7 +630,7 @@ def main():
runner.start()
# runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x"))
bottle.run(host=config.WebHost, port=config.WebPort)
bottle.run(host=config.WebHost, port=config.WebPort, server=GeventWebSocketServer)
########################################################################
# Shutdown #