http static serve, minify, http set config, http state

This commit is contained in:
Patrick Haßel 2025-01-27 14:52:49 +01:00
parent 978075d2de
commit ad91b7ce86
24 changed files with 486 additions and 488 deletions

8
.gitignore vendored
View File

@ -1,5 +1,5 @@
.pio/ /.pio/
img/ /.idea/
/data/http
cmake-build-*/
CMakeListsPrivate.txt CMakeListsPrivate.txt
cmake-build-*/
/.idea

View File

@ -1,41 +0,0 @@
class Canvas {
width;
height;
canvas;
ctx;
constructor(canvasElementId, width, height, mouseMove) {
this.width = width;
this.height = height;
this.canvas = document.getElementById(canvasElementId);
this.canvas.width = width;
this.canvas.height = height;
this.canvas.addEventListener('contextmenu', event => event.preventDefault());
this.canvas.onmousemove = mouseMove;
this.canvas.onmousedown = mouseMove;
this.canvas.onmouseup = mouseMove;
this.ctx = this.canvas.getContext("2d");
}
getPixelColor(event) {
const colorArray = this.ctx.getImageData(event.offsetX, event.offsetY, 1, 1).data;
const colorInt = (colorArray[0] << 16) | (colorArray[1] << 8) | colorArray[2];
const colorHex = colorInt.toString(16);
return "#" + colorHex.padStart(6, "0");
}
fillRect(x, y, color) {
this.ctx.beginPath();
this.ctx.fillStyle = color;
this.ctx.rect(x * this.size, y * this.size, this.size, this.size);
this.ctx.fill();
}
}

View File

@ -1,87 +0,0 @@
class Drawing extends Canvas {
mouseRasterX = -1;
mouseRasterY = -1;
color0 = "#00F";
color1 = "#FFF";
constructor(rasterCountWidth, rasterCountHeight, size, canvasElementId) {
super(canvasElementId, rasterCountWidth * size, rasterCountHeight * size, (event) => this.mouseEvent(event));
this.size = size;
this.matrix = new Array(rasterCountHeight).fill(0).map(() => new Array(rasterCountWidth).fill(undefined));
this.draw();
}
mouseEvent(event) {
this.updateCursor(event);
this.mouseDraw(event);
this.draw();
}
mouseDraw(event) {
let color;
if (Boolean(event.buttons & 1)) {
color = this.color0;
} else if (Boolean(event.buttons & 2)) {
color = undefined;
} else {
return;
}
this.matrix[this.mouseRasterY][this.mouseRasterX] = color;
}
updateCursor(event) {
this.mouseRasterX = Math.floor(event.offsetX / this.size);
this.mouseRasterY = Math.floor(event.offsetY / this.size);
}
draw() {
this.ctx.fillStyle = "#fff";
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawMatrix();
this.drawRaster(0, 0, this.canvas.width, this.canvas.height, this.size, this.size);
}
drawMatrix() {
for (let y = 0; y < this.matrix.length; y++) {
const row = this.matrix[y];
for (let x = 0; x < row.length; x++) {
const color = row[x];
if (color === undefined) {
continue;
}
this.ctx.beginPath();
this.ctx.fillStyle = color;
this.ctx.rect(x * this.size, y * this.size, this.size, this.size);
this.ctx.fill();
}
}
}
drawRaster(xBgn, yBgn, w, h, rx, ry) {
const xEnd = xBgn + w;
const yEnd = yBgn + h;
this.ctx.beginPath();
for (let x = xBgn; x <= xEnd; x += rx) {
this.ctx.moveTo(x, yBgn);
this.ctx.lineTo(x, yEnd);
}
for (let y = yBgn; y <= yEnd; y += ry) {
this.ctx.moveTo(xBgn, y);
this.ctx.lineTo(xEnd, y);
}
this.ctx.strokeStyle = "#aaa";
this.ctx.stroke();
if (this.mouseRasterX >= 0) {
this.ctx.beginPath();
this.ctx.strokeStyle = "blue";
this.ctx.rect(this.mouseRasterX * this.size, this.mouseRasterY * this.size, this.size, this.size);
this.ctx.stroke();
}
}
}

View File

@ -1,48 +0,0 @@
const STEPS = 8;
class Picker extends Canvas {
setColor0;
setColor1;
constructor(size, canvasElementId, setColor0, setColor1) {
super(canvasElementId, (STEPS + 1) * size, 7 * size, (event) => this.mouseEvent(event));
this.setColor0 = setColor0;
this.setColor1 = setColor1;
this.size = size;
this.draw();
}
mouseEvent(event) {
const color = this.getPixelColor(event);
if (Boolean(event.buttons & 1)) {
this.setColor0(color);
} else if (Boolean(event.buttons & 2)) {
this.setColor1(color);
}
}
draw() {
this.ctx.fillStyle = "#fff";
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
let y = 0;
this.drawMatrix(true, false, false, y++);
this.drawMatrix(true, true, false, y++);
this.drawMatrix(false, true, false, y++);
this.drawMatrix(false, true, true, y++);
this.drawMatrix(false, false, true, y++);
this.drawMatrix(true, false, true, y++);
this.drawMatrix(true, true, true, y++);
}
drawMatrix(dr, dg, db, y) {
for (let x = 0; x <= STEPS; x++) {
const r = x * (dr ? 256 / STEPS : 0) - 1;
const g = x * (dg ? 256 / STEPS : 0) - 1;
const b = x * (db ? 256 / STEPS : 0) - 1;
this.fillRect(x, y, `rgb(${r},${g},${b})`);
}
}
}

View File

@ -1,3 +0,0 @@
body {
margin: 0;
}

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>RGBMatrixDisplay</title>
<link rel="stylesheet" href="./index.css">
<script src="Canvas.js"></script>
<script src="Drawing.js"></script>
<script src="Picker.js"></script>
<script src="./index.js"></script>
</head>
<body>
<canvas id="canvas"></canvas><canvas id="picker"></canvas>
</body>
</html>

View File

@ -1,9 +0,0 @@
window.onload = function () {
const drawing = new Drawing(8, 8, 60, "canvas");
new Picker(
60,
"picker",
(color => drawing.color0 = color),
(color => drawing.color1 = color),
);
};

7
http/favicon.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M11.082 10H11V6h4v2.187a7.868 7.868 0 0 0-3.72 1.623 3.642 3.642 0 0 0-.198.19zM20 1h-4v4h4zm0 10.423l-.055.002a13.903 13.903 0 0 0-2.06.287 3.01 3.01 0 0 0-.744.288l.009.076a2.686 2.686 0 0 0 .267.312L19.623 15H20zM7.866 16.647a4.766 4.766 0 0 1-.45-.647H6v4h4v-2.09a2.986 2.986 0 0 1-2.134-1.263zM1 5h4V1H1zm0 10h4v-4H1z" opacity=".25"/>
<path d="M16 8.052V6h4v2.266a18.371 18.371 0 0 0-3.106-.253c-.301 0-.6.015-.894.039zM1 20h4v-4H1zm15-1.335V20h.92q-.491-.724-.92-1.335zM10 1H6v4h4z" opacity=".5"/>
<path
d="M21 11.385v5.245L19.623 15H20v-3.577zm-8.142 4.106a2.952 2.952 0 0 1-.19.509h1.314c-.074-.083-.153-.176-.218-.243-.115-.116-.444-.448-.547-.455a2.09 2.09 0 0 0-.359.19zM17.592 21H0V0h21v8.921l-.293-.316a1 1 0 0 0-.502-.293l-.15-.035L20 8.266V6h-4v2.052c-.34.028-.673.076-1 .135V6h-4v4h.082a3.274 3.274 0 0 0-1.005 1.928l-.051.038c-.01 0-.016.004-.026.004V11H6v4h1.036a3.013 3.013 0 0 0 .38 1H6v4h4v-2.09c.029 0 .057.011.085.011a1.951 1.951 0 0 0 .915-.225V20h4v-2.724c.298.4.633.866 1 1.389V20h.92c.215.317.44.651.672 1zM16 5h4V1h-4zm-5 0h4V1h-4zM6 5h4V1H6zm0 5h4V6H6zm-1 6H1v4h4zm0-5H1v4h4zm0-5H1v4h4zm0-5H1v4h4zm17.575 20.99l-.822.753a1.308 1.308 0 0 1-.882.343 1.383 1.383 0 0 1-.167-.01 1.307 1.307 0 0 1-.932-.585 74.561 74.561 0 0 0-5.288-7.428c-.454-.458-.79-.761-1.27-.761a2.326 2.326 0 0 0-1.262.603 2.36 2.36 0 0 1-1.306 1.84c-.267.187-.997.493-2.009-.734-1.01-1.23-.57-1.888-.333-2.114a2.358 2.358 0 0 1 2.06-.926c.087-.073.175-.14.262-.204.394-.298.483-.395.453-.671-.075-.671.837-1.513.846-1.521a7.907 7.907 0 0 1 4.969-1.562 17.494 17.494 0 0 1 2.932.237l.148.036 1.02 1.098-1.087.042a14.724 14.724 0 0 0-2.246.312 4.385 4.385 0 0 0-1.635.797l.016.06a4.093 4.093 0 0 1 .13.765 2.322 2.322 0 0 0 .541.739l5.979 7.084a1.303 1.303 0 0 1-.117 1.808zm-7.844-8.063a5.606 5.606 0 0 1 .837-.63 1.8 1.8 0 0 1-.393-.865 3.211 3.211 0 0 0-.103-.591.872.872 0 0 1 .215-.996 5.678 5.678 0 0 1 1.374-.83 6.687 6.687 0 0 0-4.08 1.315 2.255 2.255 0 0 0-.508.706 1.607 1.607 0 0 1-.845 1.529c-.091.068-.185.138-.274.216a.781.781 0 0 1-.585.193c-.6-.05-.733.034-1.374.646a1.479 1.479 0 0 0 .414.756 1.587 1.587 0 0 0 .674.547c.711-.506.82-.62.886-1.219a.784.784 0 0 1 .302-.537 3.354 3.354 0 0 1 1.943-.865 2.27 2.27 0 0 1 1.517.625zm7.197 6.9l-5.705-6.762a5.388 5.388 0 0 0-.781.564 83.715 83.715 0 0 1 5.169 7.316.308.308 0 0 0 .467.06l.821-.752a.306.306 0 0 0 .029-.425z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

82
http/index.htm Normal file
View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<!--suppress HtmlFormInputWithoutLabel -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="main.css">
<title>RGBMatrixDisplay</title>
<link rel="icon" href="favicon.svg">
</head>
<body>
<div class="paragraph">
<a href="player.htm?index=0">Player 0</a><br>
<a href="player.htm?index=1">Player 1</a><br>
</div>
<div class="paragraph">
<a class="mode" id="mode0" onclick="sm(0)">NONE</a><br>
<a class="mode" id="mode1" onclick="sm(1)">BORDER</a><br>
<a class="mode" id="mode2" onclick="sm(2)">CLOCK</a><br>
<a class="mode" id="mode3" onclick="sm(3)">GAME_OF_LIFE_BLACK_WHITE</a><br>
<a class="mode" id="mode4" onclick="sm(4)">GAME_OF_LIFE_GRAYSCALE</a><br>
<a class="mode" id="mode5" onclick="sm(5)">GAME_OF_LIFE_COLOR_FADE</a><br>
<a class="mode" id="mode6" onclick="sm(6)">GAME_OF_LIFE_RANDOM_COLOR</a><br>
<a class="mode" id="mode7" onclick="sm(7)">PONG</a><br>
<a class="mode" id="mode8" onclick="sm(8)">SPACE_INVADERS</a><br>
<a class="mode" id="mode9" onclick="sm(9)">COUNT_DOWN</a><br>
<a class="mode" id="mode10" onclick="sm(10)">COUNT_DOWN_BARS</a><br>
<a class="mode" id="mode11" onclick="sm(11)">COUNT_DOWN_SLEEP</a><br>
<a class="mode" id="mode12" onclick="sm(12)">STARFIELD</a><br>
<a class="mode" id="mode13" onclick="sm(13)">MATRIX</a><br>
<a class="mode" id="mode14" onclick="sm(14)">POWER</a><br>
<a class="mode" id="mode15" onclick="sm(15)">ENERGY</a><br>
<a class="mode" id="mode16" onclick="sm(16)">TIMER</a><br>
</div>
<div class="paragraph">
<div>
<a onclick="br(-10)">Dunkler</a>/
<a onclick="br(+10)">Heller</a>
<span id="brightness">?</span>
</div>
<div>
<a onclick="sp(0.9)">Langsamer</a>/
<a onclick="sp(1.1)">Schneller</a>
<span id="speed">?</span>
</div>
</div>
<div class="paragraph">
<div>
<input type="number" min="2025" max="3000" step="1" id="dlYe">
<input type="number" min="1" max="12" step="1" id="dlMo">
<input type="number" min="1" max="31" step="1" id="dlDa">
</div>
<div>
<input type="number" min="0" max="23" step="1" id="dlHo">
<input type="number" min="0" max="59" step="1" id="dlMi">
<input type="number" min="0" max="59" step="1" id="dlSe">
</div>
<div>
<button onclick="dl()">Datum setzen</button>
</div>
</div>
<div class="paragraph">
<input type="number" min="1" max="999" step="1" id="tmDa">
<input type="number" min="0" max="23" step="1" id="tmHo">
<input type="number" min="0" max="59" step="1" id="tmMi">
<input type="number" min="0" max="59" step="1" id="tmSe">
<button onclick="tm()">Datum setzen</button>
</div>
<div class="paragraph">
<button onclick="gf('/config/save');">Speichern erzwingen</button>
</div>
<script src="main.js"></script>
</body>
</html>

47
http/main.css Normal file
View File

@ -0,0 +1,47 @@
body {
font-family: sans-serif;
font-size: 7vw;
margin: 0;
}
button.player {
width: 33vmin;
height: 33vmin;
font-size: 9vw;
}
a {
text-decoration: none;
}
div {
box-sizing: border-box;
}
input, select, textarea, button {
font-family: inherit;
font-size: inherit;
}
table {
border-collapse: collapse;
}
td {
text-align: center;
}
.paragraph {
margin-top: 1em;
margin-bottom: 1em;
}
.modeActive {
background-color: lightgreen;
}
@media (min-width: 1000px) {
body {
font-size: 16px;
}
}

109
http/main.js Normal file
View File

@ -0,0 +1,109 @@
const index = parseInt(new URLSearchParams(window.location.search).get('index')) || 0;
const dlYe = document.getElementById("dlYe");
const dlMo = document.getElementById("dlMo");
const dlDa = document.getElementById("dlDa");
const dlHo = document.getElementById("dlHo");
const dlMi = document.getElementById("dlMi");
const dlSe = document.getElementById("dlSe");
const tmDa = document.getElementById("tmDa");
const tmHo = document.getElementById("tmHo");
const tmMi = document.getElementById("tmMi");
const tmSe = document.getElementById("tmSe");
const brightness = document.getElementById("brightness")
const speed = document.getElementById("speed")
let interval = undefined;
function dl() {
const y = parseInt(dlYe.value);
const M = parseInt(dlMo.value) - 1;
const d = parseInt(dlDa.value);
const h = parseInt(dlHo.value) || 0;
const m = parseInt(dlMi.value) || 0;
const s = parseInt(dlSe.value) || 0;
const deadlineEpoch = (new Date(y, M, d, h, m, s).getTime() / 1000).toFixed(0);
set("deadlineEpoch", deadlineEpoch);
}
function tm() {
const d = parseInt(tmDa.value) || 0;
const h = parseInt(tmHo.value) || 0;
const m = parseInt(tmMi.value) || 0;
const s = parseInt(tmSe.value) || 0;
const timerMillis = (((d * 24 + h) * 60 + m) * 60 + s) * 1000;
set("timerMillis", timerMillis);
}
const sm = (v) => set('mode', v);
const br = (v) => set('brightness', v);
const sp = (v) => set('speed', v);
const set = (n, v) => gf(`/set?n=${n}&v=${v}`)
const fetch = () => {
// if (interval !== undefined) {
// clearInterval(interval);
// interval = undefined;
// }
// interval = setInterval(fetch, 2000);
get("/state", showState());
}
const gf = (path) => {
get(path, fetch);
}
function get(path, cb = null) {
const r = new XMLHttpRequest();
r.onreadystatechange = () => !!cb && r.readyState === 4 && r.status === 200 && cb(r);
r.open("GET", path, true);
r.send();
}
function showState() {
return function (r) {
const json = JSON.parse(r.responseText);
// noinspection JSUnresolvedReference
const d = new Date(parseInt(json.config.deadlineEpoch || 0) * 1000);
dlYe.value = "" + d.getFullYear();
dlMo.value = "" + d.getMonth() + 1;
dlDa.value = "" + d.getDate();
dlHo.value = "" + d.getHours();
dlMi.value = "" + d.getMinutes();
dlSe.value = "" + d.getSeconds();
// noinspection JSUnresolvedReference
const s = parseInt(json.config.timerMillis || 0) / 1000;
const m = Math.floor(s / 60);
const h = Math.floor(m / 60);
tmDa.value = "" + Math.floor(h / 24);
tmHo.value = "" + h % 24;
tmMi.value = "" + m % 60;
tmSe.value = "" + s % 60;
const id = parseInt(json.config.mode);
for (const mode of document.getElementsByClassName("mode")) {
if (mode.id === "mode" + id) {
if (!mode.classList.contains("modeActive")) {
mode.classList.add("modeActive");
}
} else {
mode.classList.remove("modeActive");
}
}
// noinspection JSUnresolvedReference
brightness.innerText = (parseInt(json.config.brightness) / 2.56).toFixed(0) + "%";
speed.innerText = parseInt(json.config.speed).toFixed(5) + "x";
};
}
fetch();

46
http/player.htm Normal file
View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" href="main.css">
<script src="main.js"></script>
<title>RGBMatrixDisplay</title>
<link rel="icon" href="favicon.svg">
</head>
<body>
<table>
<tr>
<td><a href='/'>&larr;</a></td>
<td>
<button class="player" onclick="get(`/player/move?index=${index}&x=0&y=-1`);">&uarr;</button>
<br>
</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>
<button class="player" onclick="get(`/player/move?index=${index}&x=-1&y=0`);">&larr;</button>
<br>
</td>
<td>
<button class="player" onclick="get(`/player/fire?index=${index}`);">X</button>
<br>
</td>
<td>
<button class="player" onclick="get(`/player/move?index=${index}&x=+1&y=0`);">&rarr;</button>
<br>
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>
<button class="player" onclick="get(`/player/move?index=${index}&x=0&y=+1`);">&darr;</button>
<br>
</td>
<td>&nbsp;</td>
</tr>
</table>
</body>
</html>

View File

@ -3,6 +3,7 @@ platform = espressif32
board = esp32dev board = esp32dev
framework = arduino framework = arduino
board_build.filesystem = littlefs board_build.filesystem = littlefs
extra_scripts = pre:./scripts/prepare_http.py
lib_deps = ../Patrix lib_deps = ../Patrix
build_flags = -DWIFI_SSID=\"HappyNet\" -DWIFI_PKEY=\"1Grausame!Sackratte7\" -DWIFI_HOST=\"RGBMatrixDisplay\" build_flags = -DWIFI_SSID=\"HappyNet\" -DWIFI_PKEY=\"1Grausame!Sackratte7\" -DWIFI_HOST=\"RGBMatrixDisplay\"
monitor_port = /dev/ttyUSB0 monitor_port = /dev/ttyUSB0
@ -14,6 +15,7 @@ platform = ${basic.platform}
board = ${basic.board} board = ${basic.board}
framework = ${basic.framework} framework = ${basic.framework}
board_build.filesystem = ${basic.board_build.filesystem} board_build.filesystem = ${basic.board_build.filesystem}
extra_scripts = ${basic.extra_scripts}
lib_deps = ${basic.lib_deps} lib_deps = ${basic.lib_deps}
build_flags = ${basic.build_flags} build_flags = ${basic.build_flags}
monitor_port = ${basic.monitor_port} monitor_port = ${basic.monitor_port}
@ -27,6 +29,7 @@ platform = ${basic.platform}
board = ${basic.board} board = ${basic.board}
framework = ${basic.framework} framework = ${basic.framework}
board_build.filesystem = ${basic.board_build.filesystem} board_build.filesystem = ${basic.board_build.filesystem}
extra_scripts = ${basic.extra_scripts}
lib_deps = ${basic.lib_deps} lib_deps = ${basic.lib_deps}
build_flags = ${basic.build_flags} build_flags = ${basic.build_flags}
monitor_port = ${basic.monitor_port} monitor_port = ${basic.monitor_port}

3
scripts/prepare_http.py Normal file
View File

@ -0,0 +1,3 @@
import subprocess
subprocess.run(["./scripts/prepare_http.sh"], check=True)

33
scripts/prepare_http.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
SOURCE_DIR="./http"
DESTINATION_DIR="./data/http"
echo
echo "+----------------------+"
echo "| minifying http files |"
echo "+----------------------+"
cd "$(dirname "$0")/../" || exit 1
if [ -e "$DESTINATION_DIR" ]; then
rm -r "$DESTINATION_DIR"
fi
mkdir -p "$DESTINATION_DIR"
find "$SOURCE_DIR" -type f | while read -r src; do
dst="$DESTINATION_DIR/$(basename "$src").gz"
echo "source: $(du -sb --apparent-size "$src")"
minify "$src" | gzip > "$dst"
echo "destination: $(du -sb --apparent-size "$dst")"
echo
done
du -sh --apparent-size "$SOURCE_DIR"
du -sh --apparent-size "$DESTINATION_DIR"
echo "+--------------------+"
echo "| minifying COMPLETE |"
echo "+--------------------+"
echo

20
src/Display.cpp Normal file
View File

@ -0,0 +1,20 @@
#include "Display.h"
#include <mode.h>
DisplayMatrix<32, 8> display(13);
void displaySetup() {
display.setup(config.get("brightness", 10));
}
void displayLoop() {
display.loop();
}
uint8_t setBrightness(const int brightness) {
uint8_t result = display.setBrightness(display.getBrightness() + brightness);
config.set("brightness", result);
debug("brightness = %d", result);
return result;
}

14
src/Display.h Normal file
View File

@ -0,0 +1,14 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#include <patrix/display/DisplayMatrix.h>
extern DisplayMatrix<32, 8> display;
void displaySetup();
void displayLoop();
uint8_t setBrightness(int brightness);
#endif

View File

@ -1,258 +1,11 @@
#ifndef NODE_H #ifndef NODE_H
#define NODE_H #define NODE_H
#include <patrix/display/DisplayMatrix.h>
#include <patrix/node/PatrixNode.h> #include <patrix/node/PatrixNode.h>
#include <Display.h>
#include <http.h>
#include <mode.h> #include <mode.h>
#include <patrix/core/http.h>
DisplayMatrix<32, 8> display(13);
static const auto style = R"(
<style>
body {
font-family: sans-serif;
font-size: 8vw;
margin: 0;
}
button.player{
width: 33vmin;
height: 33vmin;
font-size: 9vw;
}
table{
border-collapse: collapse;
}
td{
text-align: center;
}
</style>
)";
static const auto script = R"(
<script>
function get(path){
var r = new XMLHttpRequest();
r.open("GET", path, true);
r.send();
}
function configDate(){
const year = document.getElementById('year').value;
const month = document.getElementById('month').value - 1;
const day = document.getElementById('day').value;
const hour = document.getElementById('hour').value;
const minute = document.getElementById('minute').value;
const second = document.getElementById('second').value;
const targetEpochSeconds = (new Date(year, month, day, hour, minute, second).getTime() / 1000).toFixed(0);
get('/config/date?targetEpochSeconds=' + targetEpochSeconds);
}
</script>
)";
inline void httpMode(AsyncWebServerRequest *request) {
if (!request->hasParam("mode")) {
request->send(400);
}
setMode(static_cast<ModeId>(request->getParam("mode")->value().toInt()));
request->send(200);
}
inline void httpIndex(AsyncWebServerRequest *request) {
auto *response = request->beginResponseStream("text/html");
response->print(style);
response->print(script);
response->print(R"(<p>)");
response->print(R"(<a href="/player?index=0">Player 0</a><br>)");
response->print(R"(<a href="/player?index=1">Player 1</a><br>)");
response->print(R"(</p>)");
response->print(R"(<p>)");
response->print(R"(<a onclick="get('/mode?mode=0');">NONE</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=1');">BORDER</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=2');">CLOCK</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=3');">GAME_OF_LIFE_BLACK_WHITE</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=4');">GAME_OF_LIFE_GRAYSCALE</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=5');">GAME_OF_LIFE_COLOR_FADE</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=6');">GAME_OF_LIFE_RANDOM_COLOR</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=7');">PONG</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=8');">SPACE_INVADERS</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=9');">COUNT_DOWN</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=10');">COUNT_DOWN_BARS</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=11');">COUNT_DOWN_SLEEP</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=12');">STARFIELD</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=13');">MATRIX</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=14');">POWER</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=15');">ENERGY</a><br>)");
response->print(R"(<a onclick="get('/mode?mode=16');">TIMER</a><br>)");
response->print(R"(</p>)");
response->print(R"(<p>)");
response->print(R"(Helligkeit: <a onclick="get('/brighter');">+</a> / <a onclick="get('/darker');">-</a><br>)");
response->print(R"(Geschwindigkeit: <a onclick="get('/faster');">+</a> / <a onclick="get('/slower');">-</a><br>)");
response->print(R"(</p>)");
response->print(R"(<p>)");
response->printf(R"(<input type="number" min="2025" max="3000" step="1" id="year">)");
response->printf(R"(<input type="number" min="1" max="12" step="1" id="month">)");
response->printf(R"(<input type="number" min="1" max="31" step="1" id="day">)");
response->printf(R"(<input type="number" min="0" max="23" step="1" id="hour">)");
response->printf(R"(<input type="number" min="0" max="59" step="1" id="minute">)");
response->printf(R"(<input type="number" min="0" max="59" step="1" id="second">)");
response->print(R"(<button onclick="configDate();">Datum setzen</button>)");
response->print(R"(</p>)");
response->print(R"(<p>)");
response->print(R"(<button onclick="get('/config/save');">Speichern erzwingen</button>)");
response->print(R"(</p>)");
request->send(response);
}
inline void web_player(AsyncWebServerRequest *request) {
char buffer[128];
if (!request->hasParam("index")) {
request->send(400, "text/plain", "Missing 'index'");
return;
}
const auto value = request->getParam("index")->value().toDouble();
const auto index = static_cast<int>(value);
auto *response = request->beginResponseStream("text/html");
response->print(style);
response->print(script);
response->print(R"(<meta name="viewport" content= "width=device-width, user-scalable=no">)");
response->print(R"(<table>)");
response->print(R"(<tr>)");
response->print(R"(<td><a href='/'>&larr;</td>)");
response->print(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button class="player" onclick="get('/player/move?index=%d&x=0&y=-1');">&uarr;</button><br>)", index);
response->print(buffer);
response->print(R"(</td>)");
response->print(R"(<td>&nbsp;</td>)");
response->print(R"(</tr>)");
response->print(R"(<tr>)");
response->print(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button class="player" onclick="get('/player/move?index=%d&x=-1&y=0');">&larr;</button><br>)", index);
response->print(buffer);
response->print(R"(</td>)");
response->print(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button class="player" onclick="get('/player/fire?index=%d');">X</button><br>)", index);
response->print(buffer);
response->print(R"(</td>)");
response->print(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button class="player" onclick="get('/player/move?index=%d&x=+1&y=0');">&rarr;</button><br>)", index);
response->print(buffer);
response->print(R"(</td>)");
response->print(R"(</tr>)");
response->print(R"(<tr>)");
response->print(R"(<td>&nbsp;</td>)");
response->print(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button class="player" onclick="get('/player/move?index=%d&x=0&y=+1');">&darr;</button><br>)", index);
response->print(buffer);
response->print(R"(</td>)");
response->print(R"(<td>&nbsp;</td>)");
response->print(R"(</tr>)");
response->print(R"(</table>)");
request->send(response);
}
inline void web_player_move(AsyncWebServerRequest *request) {
// ReSharper disable once CppJoinDeclarationAndAssignment
double value;
if (!request->hasParam("index")) {
request->send(400, "text/plain", "Missing 'index'");
return;
}
value = request->getParam("index")->value().toDouble();
const auto index = static_cast<int>(value);
if (!request->hasParam("x")) {
request->send(400, "text/plain", "Missing 'x'");
return;
}
value = request->getParam("x")->value().toDouble();
const auto x = static_cast<int>(value);
if (!request->hasParam("y")) {
request->send(400, "text/plain", "Missing 'y'");
return;
}
value = request->getParam("y")->value().toDouble();
const auto y = static_cast<int>(value);
modeMove(index, x, y);
request->send(200, "application/json", "true");
}
inline void web_player_fire(AsyncWebServerRequest *request) {
// ReSharper disable once CppJoinDeclarationAndAssignment
double value;
if (!request->hasParam("index")) {
request->send(400, "text/plain", "Missing 'index'");
return;
}
value = request->getParam("index")->value().toDouble();
const auto index = static_cast<int>(value);
modeFire(index);
request->send(200, "application/json", "true");
}
inline void web_setMode(AsyncWebServerRequest *request) {
if (!request->hasParam("mode")) {
request->send(400, "text/plain", "Missing 'mode'");
return;
}
auto value = request->getParam("mode")->value().toDouble();
if (isnan(value)) {
request->send(400, "text/plain", "'mode' not a number");
return;
}
setMode(static_cast<ModeId>(value));
request->send(200);
}
inline void web_brighter(AsyncWebServerRequest *request) {
const auto newBrightness = display.getBrightness() + 10;
display.setBrightness(newBrightness >= 255 ? 255 : newBrightness);
request->send(200);
}
inline void web_darker(AsyncWebServerRequest *request) {
const auto newBrightness = display.getBrightness() - 10;
display.setBrightness(newBrightness <= 0 ? 0 : newBrightness);
request->send(200);
}
inline void web_faster(AsyncWebServerRequest *request) {
setSpeed(getSpeed() * 1.1);
request->send(200);
}
inline void web_slower(AsyncWebServerRequest *request) {
setSpeed(getSpeed() / 1.1);
request->send(200);
}
inline void web_config_save(AsyncWebServerRequest *request) {
config.write();
request->send(200);
}
inline void web_config_date(AsyncWebServerRequest *request) {
const auto targetEpochSeconds = std::stoul(request->getParam("targetEpochSeconds")->value().c_str());
config.set("targetEpochSeconds", targetEpochSeconds);
modeLoadConfig();
request->send(200);
}
class Node final : public PatrixNode { class Node final : public PatrixNode {
@ -263,28 +16,14 @@ public:
} }
void setup() override { void setup() override {
displaySetup();
modeSetup(); modeSetup();
patrixHttpSetup();
server.on("/", httpIndex);
server.on("/player", web_player);
server.on("/player/move", web_player_move);
server.on("/player/fire", web_player_fire);
server.on("/mode", httpMode);
server.on("/brighter", web_brighter);
server.on("/darker", web_darker);
server.on("/faster", web_faster);
server.on("/slower", web_slower);
server.on("/config/date", web_config_date);
server.on("/config/save", web_config_save);
display.setup();
display.setBrightness(10);
display.clear();
} }
void loop() override { void loop() override {
modeLoop(display); modeLoop(display);
display.loop(); displayLoop();
} }
void mqttMessage(char *topic, char *message) override { void mqttMessage(char *topic, char *message) override {

86
src/http.cpp Normal file
View File

@ -0,0 +1,86 @@
#include "http.h"
#include <patrix/core/http.h>
#include <Display.h>
#include <mode.h>
// ReSharper disable CppLocalVariableMayBeConst
void httpState(AsyncWebServerRequest *request) {
auto doc = JsonDocument();
auto rootJson = doc.to<JsonObject>();
rootJson["config"] = config.json;
rootJson["configAutoWriteInMillis"] = config.getAutoWriteInMillis();
rootJson["configAutoWriteAtEpoch"] = config.getAutoWriteAtEpoch();
auto stream = request->beginResponseStream("application/json");
serializeJson(doc, *stream);
request->send(stream);
}
// ReSharper restore CppLocalVariableMayBeConst
void httpPlayerMove(AsyncWebServerRequest *request) {
if (!request->hasParam("index") || !request->hasParam("x") || !request->hasParam("y")) {
request->send(400, "text/plain", "required parameters: index, x, y");
return;
}
const auto index = request->getParam("index")->value().toInt();
const auto x = request->getParam("x")->value().toInt();
const auto y = request->getParam("y")->value().toInt();
modeMove(index, x, y);
request->send(200);
}
void httpPlayerFire(AsyncWebServerRequest *request) {
if (!request->hasParam("index")) {
request->send(400, "text/plain", "required parameters: index");
return;
}
const auto index = request->getParam("index")->value().toInt();
modeFire(index);
request->send(200);
}
void httpSet(AsyncWebServerRequest *request) {
if (!request->hasParam("n") || !request->hasParam("v")) {
request->send(400, "text/plain", "required parameters: n, v");
return;
}
const auto name = request->getParam("n")->value();
const auto value = request->getParam("v")->value();
debug(R"(http: set("%s", "%s"))", name.c_str(), value.c_str());
if (name.equals("mode")) {
const auto mode = static_cast<ModeId>(value.toInt());
debug(" => mode = %d", mode);
setMode(mode);
} else if (name.equals("timerMillis") || name.equals("deadlineEpoch")) {
debug(" => config");
config.set(name, value.toInt());
modeLoadConfig();
} else if (name.equals("brightness")) {
const auto brightness = value.toInt();
setBrightness(brightness);
} else if (name.equals("speed")) {
const auto speed = value.toDouble();
setModeSpeed(getModeSpeed() * speed);
}
request->send(200);
}
void httpConfigSave(AsyncWebServerRequest *request) {
config.write();
request->send(200);
}
void patrixHttpSetup() {
server.on("/state", httpState);
server.on("/set", httpSet);
server.on("/config/save", httpConfigSave);
server.on("/player/move", httpPlayerMove);
server.on("/player/fire", httpPlayerFire);
}

6
src/http.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef HTTP_H
#define HTTP_H
void patrixHttpSetup();
#endif

View File

@ -32,7 +32,7 @@ void modeStep();
void modeSetup() { void modeSetup() {
wanted = config.get("mode", GAME_OF_LIFE_RANDOM_COLOR); wanted = config.get("mode", GAME_OF_LIFE_RANDOM_COLOR);
modeSpeed = config.get("mode_speed", 1.0); modeSpeed = config.get("speed", 1.0);
} }
void modeLoop(Display& display) { void modeLoop(Display& display) {
@ -61,11 +61,15 @@ void setMode(const ModeId newMode) {
wanted = newMode; wanted = newMode;
} }
double getSpeed() { ModeId getModeId() {
return current;
}
double getModeSpeed() {
return modeSpeed; return modeSpeed;
} }
void setSpeed(const double newSpeed) { void setModeSpeed(const double newSpeed) {
modeSpeed = min(max(0.01, newSpeed), 10000.0); modeSpeed = min(max(0.01, newSpeed), 10000.0);
config.setIfNot("speed", modeSpeed); config.setIfNot("speed", modeSpeed);
} }

View File

@ -12,9 +12,11 @@ void modeLoop(Display& display);
void setMode(ModeId newMode); void setMode(ModeId newMode);
double getSpeed(); ModeId getModeId();
void setSpeed(double newSpeed); double getModeSpeed();
void setModeSpeed(double newSpeed);
void modeMove(int index, int x, int y); void modeMove(int index, int x, int y);

View File

@ -10,7 +10,7 @@ class CountDown final : public Mode {
Firework fireworks[MAX_FIREWORKS]; Firework fireworks[MAX_FIREWORKS];
time_t targetEpochSeconds = 0; time_t deadlineEpoch = 0;
tm target{}; tm target{};
@ -44,7 +44,7 @@ public:
} }
void loadConfig() override { void loadConfig() override {
targetEpochSeconds = config.get("targetEpochSeconds", 1767222000); deadlineEpoch = config.get("deadlineEpoch", 1767222000);
} }
protected: protected:
@ -63,11 +63,11 @@ protected:
return; return;
} }
localtime_r(&targetEpochSeconds, &target); localtime_r(&deadlineEpoch, &target);
target.tm_year += 1900; target.tm_year += 1900;
target.tm_mon += 1; target.tm_mon += 1;
const auto diffSeconds = difftime(targetEpochSeconds, nowEpochSeconds); const auto diffSeconds = difftime(deadlineEpoch, nowEpochSeconds);
days = static_cast<int>(floor(diffSeconds / (24 * 60 * 60))); days = static_cast<int>(floor(diffSeconds / (24 * 60 * 60)));
hours = static_cast<int>(floor(diffSeconds / (60 * 60))) % 24; hours = static_cast<int>(floor(diffSeconds / (60 * 60))) % 24;
minutes = static_cast<int>(floor(diffSeconds / 60)) % 60; minutes = static_cast<int>(floor(diffSeconds / 60)) % 60;

View File

@ -7,9 +7,9 @@
class Timer2 final : public Mode { class Timer2 final : public Mode {
long durationMillis = DEFAULT_DURATION_MILLIS; long timerMillis = DEFAULT_DURATION_MILLIS;
long restMillis = durationMillis; long restMillis = timerMillis;
unsigned long lastMillis = 0; unsigned long lastMillis = 0;
@ -32,15 +32,15 @@ public:
} }
void loadConfig() override { void loadConfig() override {
const auto newDurationMillis = config.get("durationMillis", DEFAULT_DURATION_MILLIS); const auto newTimerMillis = config.get("timerMillis", DEFAULT_DURATION_MILLIS);
if (restMillis > 0) { if (restMillis > 0) {
restMillis += newDurationMillis - durationMillis; restMillis += newTimerMillis - timerMillis;
} }
durationMillis = newDurationMillis; timerMillis = newTimerMillis;
} }
void start() override { void start() override {
restMillis = durationMillis; restMillis = timerMillis;
lastMillis = millis(); lastMillis = millis();
} }