changes needed to adapt to new environment

This commit is contained in:
lounge
2026-01-27 22:42:37 +01:00
parent 690b4ebd3c
commit a714bd9d76
7 changed files with 318 additions and 32 deletions

View File

@@ -24,7 +24,7 @@ ScreenY = 40
DefaultBrightness = 0.6 DefaultBrightness = 0.6
Serial = "/dev/ttyACM0" Serial = "/dev/ttyTeensy"
# kills app after some seconds if it sends no data # kills app after some seconds if it sends no data
NoDataTimeout = 40 NoDataTimeout = 40
@@ -47,6 +47,7 @@ Apps = [
AppConfig(guiname="Digi Clock", name="digiclock", cmd="./digi_clock.py"), AppConfig(guiname="Digi Clock", name="digiclock", cmd="./digi_clock.py"),
AppConfig(guiname="Text Scroller MQTT", name="textscroll", cmd="./textscroll.py"), AppConfig(guiname="Text Scroller MQTT", name="textscroll", cmd="./textscroll.py"),
AppConfig(guiname="Spot", name="spot", cmd="./spot.py", white=True), AppConfig(guiname="Spot", name="spot", cmd="./spot.py", white=True),
AppConfig(guiname="Fireworks", name="fireworks", cmd="./fireworks.py", white=False),
AppConfig(guiname="Flicker", name="flicker", cmd="./flicker"), AppConfig(guiname="Flicker", name="flicker", cmd="./flicker"),
AppConfig(guiname="Pixelflut", name="pixelflut", cmd="./pixelflut", persistent=True), AppConfig(guiname="Pixelflut", name="pixelflut", cmd="./pixelflut", persistent=True),
@@ -60,17 +61,17 @@ Apps = [
AppConfig(guiname="Wget Video/Gif/Images", name="wget", cmd="./wget.sh"), AppConfig(guiname="Wget Video/Gif/Images", name="wget", cmd="./wget.sh"),
# juergen/pixelfoo # juergen/pixelfoo
AppConfig(guiname="Congress noise", name="cnoise", cmd="./cnoise", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Congress noise", name="cnoise", cmd="./cnoise"),
AppConfig(guiname="Color code", name="colorcode", cmd="./colorcode", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Color code", name="colorcode", cmd="./colorcode"),
AppConfig(guiname="Game of Life", name="life", cmd="./life", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Game of Life", name="life", cmd="./life"),
AppConfig(guiname="Matrix Code", name="matrix-code", cmd="./matrix-code", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Matrix Code", name="matrix-code", cmd="./matrix-code"),
AppConfig(guiname="Lorenz Attractor", name="lorenz", cmd="./lorenz", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Lorenz Attractor", name="lorenz", cmd="./lorenz"),
AppConfig(guiname="Primes", name="primes", cmd="./primes", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Primes", name="primes", cmd="./primes"),
AppConfig(guiname="Alien Message", name="alien-message", cmd="./alien-message", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Alien Message", name="alien-message", cmd="./alien-message"),
AppConfig(guiname="Dual Moodlight", name="bimood", cmd="./bimood", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Dual Moodlight", name="bimood", cmd="./bimood"),
AppConfig(guiname="Maze", name="maze", cmd="./maze", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Maze", name="maze", cmd="./maze"),
AppConfig(guiname="Dual Maze", name="dualmaze", cmd="./dualmaze", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Dual Maze", name="dualmaze", cmd="./dualmaze"),
AppConfig(guiname="Predator & Prey", name="predprey", cmd="./predprey", path="pixelfoo-apps/target/release/"), AppConfig(guiname="Predator & Prey", name="predprey", cmd="./predprey"),
# App(guiname="Beat Saber Ceiling", name="beatsaberceiling", cmd="./beatsaberceiling.py", path="beatsaberceiling"), # App(guiname="Beat Saber Ceiling", name="beatsaberceiling", cmd="./beatsaberceiling.py", path="beatsaberceiling"),

8
configs/pixelthudconf.py Normal file
View File

@@ -0,0 +1,8 @@
Apps = [
# pixelthud
{"guiname": "Fading Pixels", "name": "fadingpxls", "cmd": "apps/fading_pixels.py", "persistent": False},
{"guiname": "Plane Wave", "name": "planewave", "cmd": "apps/plane_wave.py", "persistent": False},
{"guiname": "Rock-paper-scissors-spock-lizard", "name": "rps", "cmd": "apps/rps.py", "persistent": False},
{"guiname": "Doom Fire", "name": "doomfire", "cmd": "apps/doom_fire_psx2.py", "persistent": False},
{"guiname": "Maxwell FDTD", "name": "fdtd", "cmd": "apps/fdtd.py", "persistent": False},
]

View File

@@ -2,6 +2,9 @@
Description=Deckensteuerung Server Description=Deckensteuerung Server
[Service] [Service]
User=lounge
Group=uucp
Type=exec Type=exec
ExecStart=/usr/bin/python3 -u /home/ds/pixelserver2/main.py ExecStart=/usr/bin/python3 -u /home/ds/pixelserver2/main.py
WorkingDirectory=/home/ds/pixelserver2 WorkingDirectory=/home/ds/pixelserver2

View File

@@ -13,8 +13,10 @@
--header-color: #1a202c; --header-color: #1a202c;
--subheader-color: #718096; --subheader-color: #718096;
--toolbar-bg: #ffffff; --toolbar-bg: #ffffff;
--button-bg: #e53e3e; --button-red-bg: #e53e3e;
--button-hover-bg: #c53030; --button-red-hover-bg: #c53030;
--button-blue-bg: #3b82f6;
--button-blue-hover-bg: #3167bf;
--swatch-border: #e2e8f0; --swatch-border: #e2e8f0;
--active-swatch-border: #3b82f6; --active-swatch-border: #3b82f6;
--input-bg: #f7fafc; --input-bg: #f7fafc;
@@ -154,9 +156,8 @@
color: var(--text-color); color: var(--text-color);
} }
#clearCanvas { .control-group button {
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
background-color: var(--button-bg);
color: white; color: white;
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
@@ -165,8 +166,20 @@
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
} }
#clearCanvas {
background-color: var(--button-red-bg);
}
#clearCanvas:hover { #clearCanvas:hover {
background-color: var(--button-hover-bg); background-color: var(--button-red-hover-bg);
}
#saveCanvas {
background-color: var(--button-blue-bg);
}
#saveCanvas:hover {
background-color: var(--button-blue-hover-bg);
} }
#grid-container { #grid-container {
@@ -191,7 +204,7 @@
<div class="main-container"> <div class="main-container">
<header> <header>
<h1>Pixel Art Canvas</h1> <h1>Pixel Art Canvas</h1>
<p>Click and drag to paint. Your changes are live.</p> <p>Click and drag to paint. Your changes are live. <a href="gallery.html">Gallery</a></p>
</header> </header>
<div class="toolbar"> <div class="toolbar">
@@ -210,6 +223,7 @@
</div> </div>
<div class="control-group"> <div class="control-group">
<button id="clearCanvas">Clear</button> <button id="clearCanvas">Clear</button>
<button id="saveCanvas">Save</button>
</div> </div>
</div> </div>
@@ -224,14 +238,16 @@
const paletteContainer = document.getElementById('color-palette'); const paletteContainer = document.getElementById('color-palette');
const brushSizeSelect = document.getElementById('brushSize'); const brushSizeSelect = document.getElementById('brushSize');
const clearCanvasBtn = document.getElementById('clearCanvas'); const clearCanvasBtn = document.getElementById('clearCanvas');
const saveCanvasBtn = document.getElementById('saveCanvas');
// --- Grid Configuration --- // --- Grid Configuration ---
const GRID_ROWS = 40; const GRID_ROWS = 40;
const GRID_COLS = 80; const GRID_COLS = 80;
const DEFAULT_COLOR = '#ffffff'; const DEFAULT_COLOR = '#ffffff';
const PRESET_COLORS = [ const PRESET_COLORS = [
'#ffffff', '#c0c0c0', '#ff0000', '#ffa500', '#ffff00', '#ffffff', '#c0c0c0', '#ff0000', '#ffa500',
'#008000', '#0000ff', '#4b0082', '#ee82ee', '#000000' '#ffff00', '#008000', '#0000ff', '#4b0082',
'#ee82ee', '#b5651d', '#000000',
]; ];
// --- State Management --- // --- State Management ---
@@ -414,6 +430,12 @@
})) }))
}); });
saveCanvasBtn.addEventListener('click', async () => {
await fetch("/save_frame").then((res) => {
// if (res.ok) ...
});
});
// --- Initial Load --- // --- Initial Load ---
initializePalette(); initializePalette();
initializeGrid(); initializeGrid();

191
html/gallery.html Normal file
View File

@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pixel Art Gallery</title>
<style>
/* --- CSS styles are identical to the previous version --- */
:root {
--bg-color: #f7fafc;
--text-color: #2d3748;
--grid-border: #cbd5e0;
--header-color: #1a202c;
--subheader-color: #718096;
--card-bg: #ffffff;
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a202c;
--text-color: #e2e8f0;
--grid-border: #4a5568;
--header-color: #ffffff;
--subheader-color: #a0aec0;
--card-bg: #2d3748;
}
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 1rem;
box-sizing: border-box;
}
.main-container {
width: 100%;
max-width: 1200px;
display: flex;
flex-direction: column;
align-items: center;
}
header {
text-align: center;
margin-bottom: 2rem;
}
h1 {
font-size: 2.25rem;
font-weight: 700;
color: var(--header-color);
margin: 0;
}
header p {
margin-top: 0.25rem;
color: var(--subheader-color);
}
header p a {
color: var(--subheader-color);
font-weight: 500;
}
#gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 1.5rem;
width: 100%;
}
.gallery-item {
background-color: var(--card-bg);
border-radius: 0.5rem;
box-shadow: var(--shadow-md);
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
color: var(--text-color);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.gallery-item:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
cursor: pointer;
}
.gallery-item img {
width: 100%;
height: auto;
border: 1px solid var(--grid-border);
border-radius: 0.25rem;
image-rendering: pixelated;
image-rendering: -moz-crisp-edges;
image-rendering: crisp-edges;
}
.gallery-item .caption {
margin-top: 0.75rem;
font-weight: 500;
font-size: 0.9rem;
text-align: center;
}
.gallery-message {
grid-column: 1 / -1;
text-align: center;
color: var(--subheader-color);
}
</style>
</head>
<body>
<div class="main-container">
<header>
<h1>Pixel Art Gallery</h1>
<p><a href="./draw.html">Back to Drawing.</a></p>
</header>
<div id="gallery-grid"></div>
</div>
<template id="gallery-item-template">
<a href="#" class="gallery-item">
<img src="" alt="">
<span class="caption"></span>
</a>
</template>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const grid = document.getElementById('gallery-grid');
const template = document.getElementById('gallery-item-template');
grid.innerHTML = `<p class="gallery-message">Loading frames ...</p>`;
const response = await fetch('/gallery_frames');
if (!response.ok) {
grid.innerHTML = `<p class="gallery-message">Could not load frames</p>`;
return;
}
const images = await response.json();
if (images.length === 0) {
grid.innerHTML = `<p class="gallery-message">No frames found</p>`;
return;
}
grid.innerHTML = '';
images.forEach(image => {
const clone = document.importNode(template.content, true);
const link = clone.querySelector('a');
const img = clone.querySelector('img');
const caption = clone.querySelector('.caption');
link.href = '#';
link.addEventListener('click', async () => {
await fetch('/load_frame', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({"src": image.src})
});
});
img.src = image.src;
img.alt = image.alt || image.caption || image.dt;
caption.textContent = image.caption || image.dt;
// Add the populated clone to the grid
grid.appendChild(clone);
});
});
</script>
</body>
</html>

69
main.py
View File

@@ -1,5 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import base64
import datetime
import io
import json import json
import logging import logging
import math import math
@@ -8,15 +11,18 @@ import subprocess
import threading import threading
import time import time
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path
import PIL.Image
import bottle import bottle
import numpy as np
import serial
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import bottle.ext.websocket as bottle_ws import bottle.ext.websocket as bottle_ws
import geventwebsocket.websocket
import numpy as np
import urllib.request
import serial
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from bottle.ext.websocket import GeventWebSocketServer from bottle.ext.websocket import GeventWebSocketServer
import geventwebsocket.websocket
import config import config
import filters import filters
@@ -507,7 +513,7 @@ def pixel(x, y, r, g, b, w):
data.buffer[y][x][2] = b data.buffer[y][x][2] = b
if data.channels == 4: if data.channels == 4:
data.buffer[y][x][3] = w data.buffer[y][x][3] = w
runner.datasource.pushData(data) runner.app.datasource.pushData(data)
return "ok" return "ok"
@@ -558,6 +564,61 @@ def frame_ws(ws: geventwebsocket.websocket.WebSocket):
runner.app.datasource.pushData(data) runner.app.datasource.pushData(data)
@bottle.route("/save_frame")
def save_frame():
screenshot_dir = Path("./screenshots")
screenshot_dir.mkdir(parents=True, exist_ok=True)
data = runner.datasource.getData()
PIL.Image.fromarray(data.buffer.copy().astype("u1")).save(screenshot_dir.joinpath(f"frame-{datetime.datetime.now().astimezone().replace(tzinfo=None).isoformat()}.png"))
return "ok"
@bottle.post("/load_frame")
def load_frame():
src = bottle.request.json["src"]
buf = np.array(PIL.Image.open(io.BytesIO(urllib.request.urlopen(src).read())), dtype=np.uint8)
if buf.shape == (80, 40, 3) or buf.shape == (80, 40, 4):
buf = buf.swapaxes(0, 1)
elif buf.shape == (40, 80, 3) or buf.shape == (40, 80, 4):
pass
else:
return 415
if runner.app.name != "pixelcanvas":
startApp("pixelcanvas")
for _ in range(200):
if runner.requestedApp is None:
break
time.sleep(0.01)
else:
return 500
runner.app.datasource.pushData(Frame(buf, buf.shape[2]))
return "ok"
@bottle.route("/gallery_frames")
def gallery_frames():
screenshot_dir = Path("./screenshots")
if not screenshot_dir.exists():
return []
frames = []
for p in screenshot_dir.glob("frame-*.png"):
try:
dt = datetime.datetime.fromisoformat(p.stem.removeprefix("frame-")).astimezone().astimezone(datetime.timezone.utc)
i = "data:image/png;base64," + base64.b64encode(p.read_bytes()).decode()
frames.append({
"dt": dt.isoformat(),
"caption": dt.strftime("%d.%m.%Y %H:%M"),
"src": i,
})
except:
continue
frames.sort(key=lambda f: f["dt"], reverse=True)
return json.dumps(frames)
@bottle.route("/") @bottle.route("/")
def index(): def index():
return bottle.static_file("index.html", root='html') return bottle.static_file("index.html", root='html')

View File

@@ -23,20 +23,20 @@ if hash cargo 2>/dev/null; then
# get juergens apps # get juergens apps
mkdir -p external mkdir -p external
pushd external pushd external
if [ ! -d "pixelfoo" ]; then if [ ! -d "pixelfoo-apps" ]; then
git clone https://git.chaospott.de/juergen/pixelfoo.git git clone https://git.chaospott.de/starblue/pixelfoo-apps
fi fi
pushd pixelfoo pushd pixelfoo-apps
git pull git pull
cargo build --release cargo build --release
popd popd
popd popd
cp -f external/pixelfoo/target/release/cnoise apps/ cp -f external/pixelfoo-apps/target/release/cnoise apps/
cp -f external/pixelfoo/target/release/bimood apps/ cp -f external/pixelfoo-apps/target/release/bimood apps/
cp -f external/pixelfoo/target/release/predprey apps/ cp -f external/pixelfoo-apps/target/release/predprey apps/
cp -f external/pixelfoo/target/release/maze apps/ cp -f external/pixelfoo-apps/target/release/maze apps/
cp -f external/pixelfoo/target/release/dualmaze apps/ cp -f external/pixelfoo-apps/target/release/dualmaze apps/
fi fi
#echo "Getting mathpixel" #echo "Getting mathpixel"