Compare commits

..

23 Commits

Author SHA1 Message Date
215c633a77 added missing config.read(); Config still defect 2025-01-27 15:16:11 +01:00
ad91b7ce86 http static serve, minify, http set config, http state 2025-01-27 15:11:24 +01:00
978075d2de added missing config.loop 2025-01-27 14:51:42 +01:00
17961bad6d print Align LEFT, CENTER, RIGHT 2025-01-27 08:43:43 +01:00
687e6cf70d REFACTOR: removed Display members: cursorX, cursorY, foreground, background 2025-01-25 22:21:13 +01:00
4efc101e56 http hooks for targetEpochSeconds, mode_speed, config.write, brightness, player_move, player_fire, display segments font style 2025-01-25 19:26:36 +01:00
0b891d9604 FIXES after firsts tests on hardware (basically working now) 2025-01-24 14:36:31 +01:00
80b83705ac using Patrix library now 2025-01-23 14:33:30 +01:00
bee63d137f data-pretty WIP 2025-01-22 08:40:01 +01:00
4f9fe2a24d Creeper original look 2025-01-20 14:47:18 +01:00
ad16660716 Creeper blink 2025-01-20 14:32:56 +01:00
6b4ed1658c Emil 5. Geburtstag + Creeper 2025-01-19 23:59:22 +01:00
0d73d112b1 _USB + _OTA envs 2025-01-18 21:29:36 +01:00
9c49004fda Merge remote-tracking branch 'origin/master' 2025-01-18 21:24:46 +01:00
a0b3ace3e6 FIX Display.print2 bug for value==10 (resulted in "0") 2025-01-10 18:25:38 +01:00
374fcee02f Timer 2025-01-07 19:36:57 +01:00
71b99cca5e forcing save from ui 2024-12-31 12:16:59 +01:00
399723d77a modifiable date for countdown + COUNT_DOWN_SLEEP 2024-12-31 12:01:32 +01:00
f3d4c68e94 renamed Electricity -> Power, added Energy 2024-12-30 12:00:26 +01:00
5790286e32 MQTT_MAX_MESSAGE_AGE_MILLIS 11 seconds 2024-12-29 09:29:18 +01:00
93a0ee8a86 Display print for auto integers 2024-12-29 09:29:10 +01:00
8d99d033fe MQTT, Mode Electricity 2024-12-28 12:58:02 +01:00
022bc23eff renamed to RGBMatrixDisplay 2024-12-28 11:05:22 +01:00
51 changed files with 1986 additions and 2235 deletions

7
.gitignore vendored
View File

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

View File

@ -13,3 +13,6 @@ Hardware:
Heatsink Heatsink
Extend Matrix (double?) Extend Matrix (double?)
Microphone Microphone
MQTT:
register globally, so the values are already there when switching mode

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

@ -1,23 +1,39 @@
; PlatformIO Project Configuration File [basic]
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32 platform = espressif32
board = esp32dev board = esp32dev
framework = arduino framework = arduino
lib_deps = https://github.com/adafruit/Adafruit_NeoPixel board_build.filesystem = littlefs
build_flags = extra_scripts = pre:./scripts/prepare_http.py
upload_port = 10.0.0.116 lib_deps = ../Patrix
upload_protocol = espota build_flags = -DWIFI_SSID=\"HappyNet\" -DWIFI_PKEY=\"1Grausame!Sackratte7\" -DWIFI_HOST=\"RGBMatrixDisplay\"
;upload_port = /dev/ttyUSB0
;upload_speed = 921600
monitor_port = /dev/ttyUSB0 monitor_port = /dev/ttyUSB0
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
[env:RGBMatrixDisplay_USB]
platform = ${basic.platform}
board = ${basic.board}
framework = ${basic.framework}
board_build.filesystem = ${basic.board_build.filesystem}
extra_scripts = ${basic.extra_scripts}
lib_deps = ${basic.lib_deps}
build_flags = ${basic.build_flags}
monitor_port = ${basic.monitor_port}
monitor_speed = ${basic.monitor_speed}
monitor_filters = ${basic.monitor_filters}
upload_port = /dev/ttyUSB0
upload_speed = 921600
[env:RGBMatrixDisplay_OTA]
platform = ${basic.platform}
board = ${basic.board}
framework = ${basic.framework}
board_build.filesystem = ${basic.board_build.filesystem}
extra_scripts = ${basic.extra_scripts}
lib_deps = ${basic.lib_deps}
build_flags = ${basic.build_flags}
monitor_port = ${basic.monitor_port}
monitor_speed = ${basic.monitor_speed}
monitor_filters = ${basic.monitor_filters}
upload_port = 10.0.0.116
upload_protocol = espota

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

View File

@ -1,13 +1,13 @@
#include "BASICS.h" #include "BASICS.h"
double doStep(double valueCurrent, double valueMin, double valueMax, long long millisecondsTotal, microseconds_t microsecondsDelta) { double doStep(const double valueCurrent, const double valueMin, const double valueMax, const long long millisecondsTotal, const microseconds_t microsecondsDelta) {
double valueRange = valueMax - valueMin; const auto valueRange = valueMax - valueMin;
double timeRatio = (double) microsecondsDelta / ((double) millisecondsTotal * 1000.0); const auto timeRatio = static_cast<double>(microsecondsDelta) / (static_cast<double>(millisecondsTotal) * 1000.0);
double valueStep = valueRange * timeRatio; const auto valueStep = valueRange * timeRatio;
double valueNew = max(valueMin, min(valueMax, valueCurrent + valueStep)); const auto valueNew = max(valueMin, min(valueMax, valueCurrent + valueStep));
return valueNew; return valueNew;
} }
bool randomBool(int uncertainty) { bool randomBool(const int uncertainty) {
return random(uncertainty) == 0; return random(uncertainty) == 0;
} }

View File

@ -1,17 +1,11 @@
#ifndef BASICS_H #ifndef BASICS_H
#define BASICS_H #define BASICS_H
#include <Arduino.h> #include <patrix/display/Display.h>
#define X true #define X true
#define _ false #define _ false
#define ____ 0
#define HALF 127
#define FULL 255
#define countof(x) (sizeof(x) / sizeof(x[0]))
typedef int64_t microseconds_t; typedef int64_t microseconds_t;
typedef unsigned long milliseconds_t; typedef unsigned long milliseconds_t;

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

35
src/Node.h Normal file
View File

@ -0,0 +1,35 @@
#ifndef NODE_H
#define NODE_H
#include <patrix/node/PatrixNode.h>
#include <Display.h>
#include <http.h>
#include <mode.h>
class Node final : public PatrixNode {
public:
explicit Node() : PatrixNode(true, true, true) {
//
}
void setup() override {
displaySetup();
modeSetup();
patrixHttpSetup();
}
void loop() override {
modeLoop(display);
displayLoop();
}
void mqttMessage(char *topic, char *message) override {
modeMqttMessage(topic, message);
}
};
#endif

54
src/Vector.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef POSITION_H
#define POSITION_H
#include <Arduino.h>
class Vector {
public:
double x;
double y;
double length;
Vector() : x(0.0), y(0.0), length(0.0) {
//
}
Vector(const double x, const double y) : x(x), y(y), length(sqrt(x * x + y * y)) {
//
}
static Vector polar(const long degrees, const double length) {
const auto radians = static_cast<double>(degrees) * DEG_TO_RAD;
return {
cos(radians) * length,
sin(radians) * length,
};
}
Vector plus(const double _x, const double _y) const {
return {x + _x, y + _y};
}
Vector plus(const Vector vector) const {
return {x + vector.x, y + vector.y};
}
Vector minus(const double _x, const double _y) const {
return {x - _x, y - _y};
}
Vector minus(const Vector vector) const {
return {x - vector.x, y - vector.y};
}
Vector multiply(const double i) const {
return {x * i, y * i};
}
};
#endif

View File

@ -1,98 +0,0 @@
#include <EEPROM.h>
#include "config.h"
#include "display.h"
#define WRITE_DELAY_MS (30 * 1000)
uint32_t calculateChecksum(Config *ptr);
void config_defaults();
bool validateChecksum(uint32_t checksumEEPROM, Config &tmp);
Config config;
bool dirty = false;
bool notify = false;
milliseconds_t lastDirtyMillis = 0;
void config_setup() {
if (!config_load()) {
config_defaults();
}
}
void config_set_dirty() {
dirty = true;
lastDirtyMillis = millis();
}
void config_loop() {
if (notify && millis() - lastDirtyMillis > WRITE_DELAY_MS + 2000) {
notify = false;
}
if (notify) {
bool blink = ((millis() - lastDirtyMillis) / 100) % 2 == 0;
display.set(0, 0, blink ? MAGENTA : BLACK);
}
if (!dirty) {
return;
}
if (millis() - lastDirtyMillis <= 30000) {
return;
}
dirty = false;
notify = true;
uint32_t checksum = calculateChecksum(&config);
EEPROM.begin(512);
EEPROM.writeBytes(0, &config, sizeof(Config));
EEPROM.writeUInt(sizeof(Config), checksum);
EEPROM.end();
Serial.printf("Config saved to EEPROM.\n");
}
bool config_load() {
Config tmp{};
EEPROM.begin(512);
EEPROM.readBytes(0, &tmp, sizeof(Config));
uint32_t checksum = EEPROM.readUInt(sizeof(Config));
EEPROM.end();
bool success = validateChecksum(checksum, tmp);
if (success) {
memcpy(&config, &tmp, sizeof(Config));
Serial.printf("Config loaded from EEPROM.\n");
} else {
Serial.printf("Failed to load config from EEPROM.\n");
}
return success;
}
bool validateChecksum(uint32_t checksumEEPROM, Config &tmp) {
uint32_t calculated = calculateChecksum(&tmp);
return checksumEEPROM == calculated;
}
void config_defaults() {
config.mode = GAME_OF_LIFE_GRAYSCALE;
config.speed = 1.0;
config.brightness = 16;
Serial.printf("Config DEFAULTS loaded.\n");
}
uint32_t calculateChecksum(Config *ptr) {
uint32_t checksum = 0;
for (auto *b = (uint8_t *) ptr; b < (uint8_t *) ptr + sizeof(Config); b++) {
checksum += *b;
}
return checksum;
}

View File

@ -1,22 +0,0 @@
#ifndef MEDIATABLE_CONFIG_H
#define MEDIATABLE_CONFIG_H
#include "mode/Mode.h"
struct Config {
ModeId mode;
double speed;
uint8_t brightness;
};
extern Config config;
void config_setup();
void config_loop();
bool config_load();
void config_set_dirty();
#endif

View File

@ -1,17 +0,0 @@
#include "display.h"
#include "config.h"
Display display(32, 8);
void setBrightness(int brightness) {
brightness = max(1, min(brightness, 255));
if (display.getBrightness() == brightness) {
return;
}
config.brightness = brightness;
display.setBrightness(brightness);
config_set_dirty();
Serial.printf("Setting brightness to %5.1f%%\n", brightness / 2.55);
}

View File

@ -1,10 +0,0 @@
#ifndef MEDIATABLE_DISPLAY_H
#define MEDIATABLE_DISPLAY_H
#include "display/Display.h"
extern Display display;
void setBrightness(int brightness);
#endif

View File

@ -1,27 +0,0 @@
#include "Color.h"
const Color BLACK = {____, ____, ____};
const Color WHITE = {FULL, FULL, FULL};
const Color RED = {FULL, ____, ____};
const Color GREEN = {____, FULL, ____};
const Color BLUE = {____, ____, FULL};
const Color YELLOW = {FULL, FULL, ____};
const Color MAGENTA = {FULL, ____, FULL};
const Color VIOLET = {HALF, ____, FULL};
const Color TURQUOISE = {____, FULL, FULL};
Color gray(uint8_t brightness) {
return {brightness, brightness, brightness};
}
Color randomColor() {
return {(uint8_t) random(255), (uint8_t) random(255), (uint8_t) random(255)};
}

View File

@ -1,34 +0,0 @@
#ifndef PIXEL_H
#define PIXEL_H
#include "BASICS.h"
struct Color {
uint8_t r;
uint8_t g;
uint8_t b;
};
Color gray(uint8_t brightness);
Color randomColor();
extern const Color BLACK;
extern const Color WHITE;
extern const Color RED;
extern const Color GREEN;
extern const Color BLUE;
extern const Color YELLOW;
extern const Color MAGENTA;
extern const Color VIOLET;
extern const Color TURQUOISE;
#endif

View File

@ -1,110 +0,0 @@
#include "Display.h"
bool SYMBOLS[SYMBOL_COUNT][DISPLAY_CHAR_WIDTH * DISPLAY_CHAR_HEIGHT] = {
{
X, X, X,
X, _, X,
X, _, X,
X, _, X,
X, X, X,
},
{
_, _, X,
_, X, X,
X, _, X,
_, _, X,
_, _, X,
},
{
X, X, X,
_, _, X,
X, X, X,
X, _, _,
X, X, X,
},
{
X, X, X,
_, _, X,
_, X, X,
_, _, X,
X, X, X,
},
{
X, _, X,
X, _, X,
X, X, X,
_, _, X,
_, _, X,
},
{
X, X, X,
X, _, _,
X, X, X,
_, _, X,
X, X, X,
},
{
X, X, X,
X, _, _,
X, X, X,
X, _, X,
X, X, X,
},
{
X, X, X,
_, _, X,
_, X, _,
X, _, _,
X, _, _,
},
{
X, X, X,
X, _, X,
X, X, X,
X, _, X,
X, X, X,
},
{
X, X, X,
X, _, X,
X, X, X,
_, _, X,
X, X, X,
},
{
_, _, _,
_, X, _,
_, _, _,
_, X, _,
_, _, _,
},
{
_, _, X,
_, _, X,
_, _, X,
X, _, X,
_, X, _,
},
{
X, _, X,
X, X, X,
_, X, _,
X, X, X,
X, _, X,
},
{
_, _, _,
_, _, _,
X, X, X,
_, _, _,
_, _, _,
},
// this must always be the last symbol (fallback)
{
X, X, X,
X, X, X,
X, X, X,
X, X, X,
X, X, X,
},
};

View File

@ -1,193 +0,0 @@
#ifndef DISPLAY_H
#define DISPLAY_H
#include "Color.h"
#include "Adafruit_NeoPixel.h"
#include "Vector.h"
#define SYMBOL_COUNT 15
#define DISPLAY_CHAR_WIDTH 3
#define DISPLAY_CHAR_HEIGHT 5
extern bool SYMBOLS[SYMBOL_COUNT][DISPLAY_CHAR_WIDTH * DISPLAY_CHAR_HEIGHT];
class Display {
public:
const uint8_t width;
const uint8_t height;
const size_t pixelCount;
const size_t pixelByteCount;
bool fpsShow = false;
private:
Adafruit_NeoPixel leds;
milliseconds_t fpsLastMillis = 0;
int fps = 0;
Color *buffer = nullptr;
uint8_t brightness = 10;
public:
Display(uint8_t width, uint8_t height) :
width(width), height(height),
pixelCount(width * height),
pixelByteCount(pixelCount * sizeof(Color)),
leds(pixelCount, GPIO_NUM_13) {
buffer = (Color *) malloc(pixelByteCount);
if (buffer == nullptr) {
Serial.print("+-----------------------------------------------+\n");
Serial.print("| OUT OF MEMORY: Cannot allocate double-buffer! |\n");
Serial.print("+-----------------------------------------------+\n");
}
}
~Display() {
if (buffer == nullptr) {
return;
}
free(buffer);
buffer = nullptr;
}
void setup() {
leds.begin();
clear();
flush();
}
void loop() {
calculateFPS();
drawFpsBorder();
if (isDirty()) {
flush();
}
}
void setBrightness(uint8_t value) {
brightness = value;
}
uint8_t getBrightness() const {
return brightness;
}
void clear() {
if (buffer == nullptr) {
return;
}
memset(buffer, 0, pixelByteCount);
}
uint8_t print(uint8_t xPos, uint8_t yPos, uint8_t index, Color color) {
if (index >= SYMBOL_COUNT) {
Serial.printf("Cannot print symbol #%u.\n", index);
index = SYMBOL_COUNT - 1;
}
bool *symbolBit = SYMBOLS[index];
for (uint8_t y = 0; y < DISPLAY_CHAR_HEIGHT; ++y) {
for (uint8_t x = 0; x < DISPLAY_CHAR_WIDTH; ++x) {
if (*(symbolBit++)) {
set(xPos + x, yPos + y, color);
} else {
set(xPos + x, yPos + y, BLACK);
}
}
}
return DISPLAY_CHAR_WIDTH;
}
void set(Vector pos, Color color) {
set((uint8_t) round(pos.x), (uint8_t) round(pos.y), color);
}
void set(uint8_t x, uint8_t y, Color color) {
if (x >= width || y >= height) {
return;
}
if ((y % 2) != 0) {
x = width - x - 1;
}
set(y * width + x, color);
}
void set(uint16_t index, Color color) {
if (buffer == nullptr) {
return;
}
buffer[index] = {
// yes, correct order is GRB !!!
(uint8_t) (color.g * brightness >> 8),
(uint8_t) (color.r * brightness >> 8),
(uint8_t) (color.b * brightness >> 8)
};
}
private:
void flush() {
if (buffer == nullptr) {
return;
}
memcpy(leds.getPixels(), buffer, pixelByteCount);
leds.show();
}
bool isDirty() const {
if (buffer == nullptr) {
return false;
}
return memcmp(leds.getPixels(), buffer, pixelByteCount) != 0;
}
void calculateFPS() {
fps = (int) round(1000.0 / (millis() - fpsLastMillis));
fpsLastMillis = millis();
}
void drawFpsBorder() {
if (!fpsShow) {
return;
}
int frames = fps;
Color color = RED;
if (frames > 3 * 76) {
frames -= 3 * 76;
color = WHITE;
} else if (frames > 2 * 76) {
frames -= 2 * 76;
color = BLUE;
} else if (frames > 76) {
frames -= 76;
color = GREEN;
}
for (int x = 0; x <= width - 1 && frames-- > 0; x++) {
set(x, 0, color);
}
for (int y = 0; y <= height - 1 && frames-- > 0; y++) {
set(width - 1, y, color);
}
for (int x = width - 1; x >= 0 && frames-- > 0; x--) {
set(x, height - 1, color);
}
for (int y = height - 1; y >= 0 && frames-- > 0; y--) {
set(0, y, color);
}
}
};
#endif

View File

@ -1,56 +0,0 @@
#ifndef POSITION_H
#define POSITION_H
#include "BASICS.h"
class Vector {
public:
double x;
double y;
double length;
Vector() :
x(0.0), y(0.0), length(0.0) {
// nothing
}
Vector(double x, double y) :
x(x), y(y), length(sqrt(x * x + y * y)) {
// nothing
}
static Vector polar(long degrees, double length) {
double radians = (double) degrees * DEG_TO_RAD;
return {
cos(radians) * length,
sin(radians) * length,
};
}
Vector plus(double _x, double _y) const {
return {x + _x, y + _y};
}
Vector plus(Vector vector) const {
return {x + vector.x, y + vector.y};
}
Vector minus(double _x, double _y) const {
return {x - _x, y - _y};
}
Vector minus(Vector vector) const {
return {x - vector.x, y - vector.y};
}
Vector multiply(double i) const {
return {x * i, y * i};
}
};
#endif

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

@ -1,30 +1,19 @@
#include "serial.h" #include "Node.h"
#include "config.h"
#include "wifi.h"
#include "server.h" // ReSharper disable once CppUnusedIncludeDirective
#include <ArduinoJson.h>
#include "mode.h" // ReSharper disable once CppUnusedIncludeDirective
#include "display.h" #include <ArduinoOTA.h>
void setup() { // ReSharper disable once CppUnusedIncludeDirective
delay(500); #include <PubSubClient.h>
serial_setup(); // ReSharper disable once CppUnusedIncludeDirective
config_setup(); #include <Adafruit_NeoPixel.h>
wifi_setup();
server_setup(); auto node = Node();
display.setup(); PatrixNode &patrixGetNode() {
display.setBrightness(config.brightness); return node;
}
void loop() {
serial_loop();
wifi_loop();
server_loop();
mode_loop();
config_loop();
display.loop();
} }

View File

@ -1,77 +1,104 @@
#include "mode.h" #include "mode.h"
#include "config.h"
#include "mode/Border/Border.h" #include "mode/Border/Border.h"
#include "mode/Clock/Clock.h" #include "mode/Clock/Clock.h"
#include "mode/GameOfLife/GameOfLife.h"
#include "mode/Pong/Pong.h"
#include "mode/SpaceInvaders/SpaceInvaders.h"
#include "mode/CountDown/CountDown.h" #include "mode/CountDown/CountDown.h"
#include "mode/Starfield/Starfield.h" #include "mode/Energy/Energy.h"
#include "mode/GameOfLife/GameOfLife.h"
#include "mode/Matrix/Matrix.h" #include "mode/Matrix/Matrix.h"
#include "display.h" #include "mode/Pong/Pong.h"
#include "mode/Power/Power.h"
#include "mode/SpaceInvaders/SpaceInvaders.h"
#include "mode/Starfield/Starfield.h"
#include "mode/Timer/Timer.h"
ModeId currentModeId = NONE; Config config("/main.json");
microseconds_t lastMicros = 0; auto current = NONE;
auto wanted = NONE;
microseconds_t modeStepLastMicros = 0;
Mode *mode = nullptr; Mode *mode = nullptr;
void unloadOldMode(); auto modeSpeed = 1.0;
void loadNewMode(); void unloadOldMode(Display& display);
void mode_step(); void loadNewMode(Display& display);
void mode_loop() { void modeStep();
if (currentModeId != config.mode) {
unloadOldMode(); void modeSetup() {
loadNewMode(); config.read();
} wanted = config.get("mode", GAME_OF_LIFE_RANDOM_COLOR);
mode_step(); modeSpeed = config.get("speed", 1.0);
} }
void setMode(ModeId value) { void modeLoop(Display& display) {
if (config.mode == value) { config.loop();
return; if (current != wanted) {
unloadOldMode(display);
loadNewMode(display);
} }
config.mode = value; modeStep();
config_set_dirty();
} }
void modeMove(int index, int x, int y) { void modeMqttMessage(const char *topic, const char *message) {
if (mode != nullptr) {
mode->mqttMessage(topic, message);
}
}
void modeLoadConfig() {
if (mode != nullptr) {
mode->loadConfig();
}
}
void setMode(const ModeId newMode) {
config.setIfNot("mode", newMode);
wanted = newMode;
}
ModeId getModeId() {
return current;
}
double getModeSpeed() {
return modeSpeed;
}
void setModeSpeed(const double newSpeed) {
modeSpeed = min(max(0.01, newSpeed), 10000.0);
config.setIfNot("speed", modeSpeed);
}
void modeMove(const int index, const int x, const int y) {
if (mode != nullptr) { if (mode != nullptr) {
mode->move(index, x, y); mode->move(index, x, y);
} }
} }
void modeFire(int index) { void modeFire(const int index) {
if (mode != nullptr) { if (mode != nullptr) {
mode->fire(index); mode->fire(index);
} }
} }
void setSpeed(double speed) { void unloadOldMode(Display& display) {
speed = min(max(0.01, speed), 10000.0);
if (config.speed == speed) {
return;
}
config.speed = speed;
config_set_dirty();
Serial.printf("Setting speed to %6.2fx\n", config.speed);
}
void unloadOldMode() {
if (mode != nullptr) { if (mode != nullptr) {
info("Unloading mode: %s", mode->getName());
mode->stop();
delete mode; delete mode;
mode = nullptr; mode = nullptr;
display.clear();
} }
display.clear();
} }
void loadNewMode() { void loadNewMode(Display& display) {
currentModeId = config.mode; switch (wanted) {
lastMicros = 0;
switch (currentModeId) {
case BORDER: case BORDER:
mode = new Border(display); mode = new Border(display);
break; break;
@ -97,10 +124,13 @@ void loadNewMode() {
mode = new SpaceInvaders(display); mode = new SpaceInvaders(display);
break; break;
case COUNT_DOWN: case COUNT_DOWN:
mode = new CountDown(display, false); mode = new CountDown(display, false, false);
break; break;
case COUNT_DOWN_BARS: case COUNT_DOWN_BARS:
mode = new CountDown(display, true); mode = new CountDown(display, true, false);
break;
case COUNT_DOWN_SLEEP:
mode = new CountDown(display, false, true);
break; break;
case STARFIELD: case STARFIELD:
mode = new Starfield(display); mode = new Starfield(display);
@ -108,20 +138,34 @@ void loadNewMode() {
case MATRIX: case MATRIX:
mode = new Matrix(display); mode = new Matrix(display);
break; break;
case POWER:
mode = new Power(display);
break;
case ENERGY:
mode = new Energy(display);
break;
case TIMER:
mode = new Timer2(display);
break;
default: default:
Serial.print("No mode loaded.\n"); info("No such mode: %d", wanted);
display.clear();
break; break;
} }
Serial.printf("Mode: %s\n", mode == nullptr ? "None" : mode->getName()); if (mode != nullptr) {
info("Loaded mode: %s", mode->getName());
mode->loadConfig();
mode->start();
}
modeStepLastMicros = 0;
current = wanted;
} }
void mode_step() { void modeStep() {
if (mode == nullptr) { if (mode == nullptr) {
return; return;
} }
auto currentMicros = (int64_t) micros(); const auto currentMicros = static_cast<int64_t>(micros());
microseconds_t microseconds = (microseconds_t) min(1000000.0, max(1.0, (double) (currentMicros - lastMicros) * config.speed)); const auto deltaMicros = static_cast<microseconds_t>(min(1000000.0, max(1.0, static_cast<double>(currentMicros - modeStepLastMicros) * modeSpeed)));
lastMicros = currentMicros; modeStepLastMicros = currentMicros;
mode->loop(microseconds); mode->loop(deltaMicros);
} }

View File

@ -1,16 +1,29 @@
#ifndef MEDIATABLE_MODE_H #ifndef RGB_MATRIX_DISPLAY_MODE_H
#define MEDIATABLE_MODE_H #define RGB_MATRIX_DISPLAY_MODE_H
#include <patrix/core/Config.h>
#include "mode/Mode.h" #include "mode/Mode.h"
void mode_loop(); extern Config config;
void setMode(ModeId value); void modeSetup();
void setSpeed(double speed); void modeLoop(Display& display);
void setMode(ModeId newMode);
ModeId getModeId();
double getModeSpeed();
void setModeSpeed(double newSpeed);
void modeMove(int index, int x, int y); void modeMove(int index, int x, int y);
void modeFire(int index); void modeFire(int index);
void modeMqttMessage(const char *topic, const char *message);
void modeLoadConfig();
#endif #endif

View File

@ -3,32 +3,27 @@
#include "mode/Mode.h" #include "mode/Mode.h"
class Border : public Mode { class Border final : public Mode {
public: public:
explicit Border(Display &display) : explicit Border(Display& display) : Mode(display) {
Mode(display) { // nothing
// nothing }
}
const char *getName() override { const char *getName() override {
return "Border"; return "Border";
} }
protected: protected:
void draw(Display &display) override { void draw(Display& display) override {
for (int y = 0; y < height; y++) { display.clear();
for (int x = 0; x < width; x++) { display.drawLine(0, 0, display.width, 0, 1, White);
if (x == 0 || x == width - 1 || y == 0 || y == height - 1) { display.drawLine(0, 0, 0, display.height, 1, White);
display.set(x, y, WHITE); display.drawLine(display.width - 1, display.height - 1, -display.width, 0, 1, White);
} else { display.drawLine(display.width - 1, display.height - 1, 0, -display.height, 1, White);
display.set(x, y, BLACK); }
}
}
}
}
}; };

View File

@ -3,43 +3,32 @@
#include "mode/Mode.h" #include "mode/Mode.h"
class Clock : public Mode { class Clock final : public Mode {
public: public:
explicit Clock(Display &display) : explicit Clock(Display& display) : Mode(display) {
Mode(display) { //
// nothing }
}
const char *getName() override { ~Clock() override = default;
return "Clock";
} const char *getName() override {
return "Clock";
}
protected: protected:
void step(microseconds_t microseconds) override { void step(microseconds_t microseconds) override {
if (realtimeChanged) { if (realtimeChanged) {
markDirty(); markDirty();
}
} }
}
void draw(Display &display) override { void draw(Display& display) override {
display.clear(); display.clear();
display.printf(16, 1, CENTER, White, "%2d:%02d:%02d", now.tm_hour, now.tm_min, now.tm_sec);
uint8_t x = 2; }
x += display.print(x, 1, realtimeOK ? now.tm_hour / 10 : 13, WHITE);
x++;
x += display.print(x, 1, realtimeOK ? now.tm_hour % 10 : 13, WHITE);
x += display.print(x, 1, 10, WHITE);
x += display.print(x, 1, realtimeOK ? now.tm_min / 10 : 13, WHITE);
x++;
x += display.print(x, 1, realtimeOK ? now.tm_min % 10 : 13, WHITE);
x += display.print(x, 1, 10, WHITE);
x += display.print(x, 1, realtimeOK ? now.tm_sec / 10 : 13, WHITE);
x++;
x += display.print(x, 1, realtimeOK ? now.tm_sec % 10 : 13, WHITE);
}
}; };

View File

@ -1,257 +1,217 @@
#ifndef MODE_COUNT_DOWN_H #ifndef MODE_COUNT_DOWN_H
#define MODE_COUNT_DOWN_H #define MODE_COUNT_DOWN_H
#define MAX_FIREWORKS 5 #define MAX_FIREWORKS 6
#include "mode/Mode.h"
#include "CountDownFirework.h" #include "CountDownFirework.h"
#include "mode/Mode.h"
class CountDown : public Mode { class CountDown final : public Mode {
private: Firework fireworks[MAX_FIREWORKS];
Firework fireworks[MAX_FIREWORKS]; time_t deadlineEpoch = 0;
uint16_t days = 0; tm target{};
uint8_t level = 0; uint16_t days = 0;
bool bars; uint16_t hours = 0;
uint16_t minutes = 0;
uint16_t seconds = 0;
uint8_t level = 0;
bool bars;
bool plus1DayForSleepingCount;
public: public:
CountDown(Display &display, bool bars) : CountDown(Display& display, const bool bars, const bool plus1DayForSleepingCount) : Mode(display), bars(bars), plus1DayForSleepingCount(plus1DayForSleepingCount) {
Mode(display), bars(bars) { for (auto& firework: fireworks) {
for (auto &firework: fireworks) { firework.init(display);
firework.init(display);
}
} }
}
const char *getName() override { const char *getName() override {
if (bars) { if (bars) {
return "CountDown (Bars)"; return "CountDown (Bars)";
} else {
return "CountDown (Numbers)";
}
} }
return "CountDown (Numbers)";
}
void loadConfig() override {
deadlineEpoch = config.get("deadlineEpoch", 1767222000);
}
protected: protected:
void step(microseconds_t microseconds) override { void step(const microseconds_t microseconds) override {
if (!realtimeOK) { if (!realtimeOK) {
setMode(NO_TIME); setMode(NO_TIME);
} else if (now.tm_mon != 1 || now.tm_mday != 1 || now.tm_hour != 0) { return;
days = getDayCountForYear(now.tm_year) - now.tm_yday - 1; }
setMode(COUNTDOWN); if (dateReached()) {
if (days == 0) { setMode(FIREWORK);
loopLastDay(); for (auto& firework: fireworks) {
} else { firework.step(microseconds);
loopMultipleDays();
}
} else {
setMode(FIREWORK);
for (auto &firework: fireworks) {
firework.step(microseconds);
}
markDirty();
} }
markDirty();
return;
} }
void loopLastDay() { localtime_r(&deadlineEpoch, &target);
int levelTmp = (int) round(32 * realtimeMilliseconds / 1000.0);; target.tm_year += 1900;
if (level != levelTmp) { target.tm_mon += 1;
level = levelTmp;
markDirty();
}
}
void loopMultipleDays() { const auto diffSeconds = difftime(deadlineEpoch, nowEpochSeconds);
if (realtimeChanged) { days = static_cast<int>(floor(diffSeconds / (24 * 60 * 60)));
markDirty(); hours = static_cast<int>(floor(diffSeconds / (60 * 60))) % 24;
} minutes = static_cast<int>(floor(diffSeconds / 60)) % 60;
seconds = static_cast<int>(diffSeconds) % 60;
setMode(COUNTDOWN);
if (days == 0) {
loopLastDay();
} else {
loopMultipleDays();
} }
}
void draw(Display &display) override { void loopLastDay() {
display.clear(); const auto levelTmp = static_cast<int>(round(32 * realtimeMilliseconds / 1000.0));
if (!realtimeOK) { if (level != levelTmp) {
drawNoTime(display); level = levelTmp;
} else if (now.tm_mon == 1 && now.tm_mday == 1 && now.tm_hour == 0) { markDirty();
for (auto &firework: fireworks) {
firework.draw(display);
}
drawYear(display, now.tm_year);
} else {
drawCountdown(display, now);
}
} }
}
void loopMultipleDays() {
if (realtimeChanged) {
markDirty();
}
}
bool dateReached() const {
return now.tm_year == target.tm_year && now.tm_mon == target.tm_mon && now.tm_mday == target.tm_mday;
}
void draw(Display& display) override {
display.clear();
if (!realtimeOK) {
drawNoTime(display);
} else if (dateReached()) {
for (auto& firework: fireworks) {
firework.draw(display);
}
drawYear(display, now.tm_year);
} else {
drawCountdown(display);
}
}
private: private:
enum State { enum State {
NO_TIME, NO_TIME,
COUNTDOWN, COUNTDOWN,
FIREWORK, FIREWORK,
}; };
State _state = NO_TIME; State _state = NO_TIME;
void setMode(State state) { void setMode(const State state) {
if (_state != state) { if (_state != state) {
_state = state; _state = state;
markDirty(); markDirty();
}
} }
}
void drawCountdown(Display &display, const tm &now) { void drawCountdown(Display& display) const {
uint8_t hours = (24 - now.tm_hour - (now.tm_min > 0 || now.tm_sec > 0 ? 1 : 0)); if (plus1DayForSleepingCount) {
uint8_t minutes = (60 - now.tm_min - (now.tm_sec > 0 ? 1 : 0)) % 60; drawSleepingCount(display);
uint8_t seconds = (60 - now.tm_sec) % 60; } else if (days <= 0 && bars) {
if (days <= 0 && bars) { drawCountdownBars(display);
drawCountdownBars(display, hours, minutes, seconds); } else {
} else { drawCountdownNumbers(display);
drawCountdownNumbers(display, hours, minutes, seconds);
}
} }
}
void drawCountdownBars(Display &display, uint8_t hours, uint8_t minutes, uint8_t seconds) { void drawSleepingCount(Display& display) const {
drawBar(display, 0, 0, 24, 1, 0, 24, hours, RED, MAGENTA, 0); const auto sleepCount = days + 1;
drawBar(display, 0, 2, 30, 2, 0, 60, minutes, BLUE, VIOLET, 5); display.printf(30, 1, RIGHT, White, "%d %s", sleepCount, sleepCount == 1 ? "TAG" : "TAGE");
drawBar(display, 0, 5, 30, 2, 0, 60, seconds, GREEN, YELLOW, 5); }
void drawCountdownBars(Display& display) const {
drawBar(display, 0, 24, 1, 24, hours, Red, Magenta, 0);
drawBar(display, 2, 30, 2, 60, minutes, Blue, Violet, 5);
drawBar(display, 5, 30, 2, 60, seconds, Green, Yellow, 5);
}
void drawCountdownNumbers(Display& display) const {
if (days >= 10) {
display.printf(30, 1, RIGHT, White, "%d TAGE", days);
drawSecondsBar(display, seconds);
} else if (days > 0) {
display.printf(30, 1, RIGHT, White, "%d %2d:%02d", days, hours, minutes);
drawSecondsBar(display, seconds);
} else {
display.printf(30, 1, RIGHT, White, "%d:%02d:%02d", hours, minutes, seconds);
drawSubSecondsBar(display);
} }
}
void drawBar(Display &display, uint8_t _x, uint8_t _y, uint8_t _w, uint8_t _h, uint8_t min, uint8_t max, uint8_t value, const Color &color, const Color &tickColor, uint8_t ticks) { static void drawBar(Display& display, const uint8_t _y, const uint8_t _w, const uint8_t _h, const uint8_t max, const uint8_t value, const RGBA& color, const RGBA& tickColor, const uint8_t ticks) {
auto totalOnCount = (uint8_t) round(((double) value - min) / (max - min) * _w * _h); const auto totalOnCount = static_cast<uint8_t>(round(static_cast<double>(value) / max * _w * _h));
uint8_t doneOnCount = 0; uint8_t doneOnCount = 0;
for (uint8_t y = 0; y < _h; y++) { for (uint8_t y = 0; y < _h; y++) {
for (uint8_t x = 0; x < _w; x++) { for (uint8_t x = 0; x < _w; x++) {
if (doneOnCount >= totalOnCount) { if (doneOnCount >= totalOnCount) {
return; return;
}
doneOnCount++;
auto c = color;
if (ticks != 0) {
if (doneOnCount % ticks == 0) {
c = tickColor;
} }
doneOnCount++;
Color c = color;
if (ticks != 0) {
if (doneOnCount % ticks == 0) {
c = tickColor;
}
}
display.set(_x + x, _y + y, c);
} }
display.setPixel(x, _y + y, c);
} }
} }
}
static Color factor(Color color, double factor) { static void drawNoTime(Display& display) {
return { display.print(1, 1, LEFT, Red, "--:--:--");
(uint8_t) round(color.r * factor), }
(uint8_t) round(color.g * factor),
(uint8_t) round(color.b * factor),
};
}
void drawCountdownNumbers(Display &display, uint8_t hours, uint8_t minutes, uint8_t seconds) const { static void drawSecondsBar(Display& display, const int seconds) {
uint8_t x = 0; for (auto pos = 0; pos < 30; pos++) {
if (days > 0) { if (pos <= seconds - 30) {
drawDay(display, days, &x); display.setPixel(pos + 1, 7, Green);
x += display.print(x, 1, 10, WHITE); } else if (pos <= seconds) {
} else { display.setPixel(pos + 1, 7, Red);
x += 2;
}
drawHour(display, days, hours, &x);
x += display.print(x, 1, 10, WHITE);
draw2Digit(display, minutes, &x);
if (days <= 0) {
x += display.print(x, 1, 10, WHITE);
draw2Digit(display, seconds, &x);
drawSubSecondsBar(display);
} else {
drawSecondsBar(display, seconds);
} }
} }
}
static void drawNoTime(Display &display) { void drawSubSecondsBar(Display& display) const {
uint8_t x = 2; for (auto pos = 0; pos < 32; pos++) {
x += display.print(x, 1, 13, WHITE); if (pos < 32 - level) {
x++; display.setPixel(pos, 7, Green);
x += display.print(x, 1, 13, WHITE);
x += display.print(x, 1, 10, WHITE);
x += display.print(x, 1, 13, WHITE);
x++;
x += display.print(x, 1, 13, WHITE);
x += display.print(x, 1, 10, WHITE);
x += display.print(x, 1, 13, WHITE);
x++;
x += display.print(x, 1, 13, WHITE);
}
static void drawDay(Display &display, int days, uint8_t *x) {
if (days >= 100) {
*x += display.print(*x, 1, days / 100, WHITE);
} else {
*x += 3;
}
(*x)++;
if (days >= 10) {
*x += display.print(*x, 1, days / 10 % 10, WHITE);
} else {
*x += 3;
}
(*x)++;
*x += display.print(*x, 1, days % 10, WHITE);
}
static void drawHour(Display &display, int days, int hours, uint8_t *x) {
if (days > 0 || hours >= 10) {
*x += display.print(*x, 1, hours / 10, WHITE);
} else {
*x += 3;
}
(*x)++;
*x += display.print(*x, 1, hours % 10, WHITE);
}
static void draw2Digit(Display &display, int value, uint8_t *x) {
*x += display.print(*x, 1, value / 10, WHITE);
(*x)++;
*x += display.print(*x, 1, value % 10, WHITE);
}
static void drawSecondsBar(Display &display, int seconds) {
for (int pos = 0; pos < 30; pos++) {
if (pos <= seconds - 30) {
display.set(pos + 1, 7, GREEN);
} else if (pos <= seconds) {
display.set(pos + 1, 7, RED);
}
} }
} }
}
void drawSubSecondsBar(Display &display) const { void drawYear(Display& display, const int year) const {
for (int pos = 0; pos < 32; pos++) { if (plus1DayForSleepingCount) {
if (pos < 32 - level) { display.printf(1, 1, LEFT, White, "EMIL 5");
display.set(pos, 7, GREEN); } else {
} display.printf(1, 1, LEFT, White, "%5d", year);
}
}
static int getDayCountForYear(int year) {
bool leapYear = year % 4 == 0 && (year % 400 == 0 || year % 100 != 0);
if (leapYear) {
return 366;
}
return 365;
}
static void drawYear(Display &display, int year) {
uint8_t x = 8;
x += display.print(x, 1, year / 1000 % 10, WHITE);
x++;
x += display.print(x, 1, year / 100 % 10, WHITE);
x++;
x += display.print(x, 1, year / 10 % 10, WHITE);
x++;
x += display.print(x, 1, year / 1 % 10, WHITE);
} }
}
}; };

View File

@ -2,146 +2,138 @@
#define COUNT_DOWN_FIREWORK_H #define COUNT_DOWN_FIREWORK_H
#include "BASICS.h" #include "BASICS.h"
#include "display/Vector.h" #include "Vector.h"
#include "display/Display.h"
#define DARKER_FACTOR 0.75 #define DARKER_FACTOR 0.75
#define SLOWER_DIVISOR 1 #define SLOWER_DIVISOR 1
class Firework { class Firework {
enum State { enum State {
INITIAL, RISE, EXPLODE, SPARKLE INITIAL, RISE, EXPLODE, SPARKLE
}; };
uint8_t width = 0; uint8_t width = 0;
uint8_t height = 0; uint8_t height = 0;
Color color = MAGENTA; RGBA color = Magenta;
State state = RISE; State state = RISE;
Vector position; Vector position;
double destinationHeight = 0; double destinationHeight = 0;
double explosionRadius = 0; double explosionRadius = 0;
double sparkleMax = 0; double sparkleMax = 0;
double explosion{}; double explosion{};
double sparkle{}; double sparkle{};
public: public:
void init(Display &display) { void init(const Display& display) {
width = display.width; width = display.width;
height = display.height; height = display.height;
reset(); reset();
}
void reset() {
position = Vector(random(width), height);
color = RGBA::rnd();
state = INITIAL;
destinationHeight = height / 2.0 + static_cast<double>(random(5)) - 2;
explosionRadius = static_cast<double>(random(3)) + 1;
sparkleMax = 100;
explosion = 0.0;
sparkle = 0.0;
}
void step(microseconds_t microseconds) {
microseconds = microseconds / SLOWER_DIVISOR;
switch (state) {
case INITIAL:
state = RISE;
break;
case RISE:
if (position.y > destinationHeight) {
position.y = doStep(position.y, 0.0, height, 1000, -microseconds);
} else {
state = EXPLODE;
}
break;
case EXPLODE:
if (explosion < explosionRadius) {
explosion = doStep(explosion, 0.0, explosionRadius, 500, +microseconds);
} else {
state = SPARKLE;
}
break;
case SPARKLE:
if (sparkle < sparkleMax) {
sparkle = doStep(sparkle, 0.0, sparkleMax, 1000, +microseconds);
} else {
reset();
}
break;
} }
}
void reset() { void draw(Display& display) const {
position = Vector((double) random(width), height); switch (state) {
color = randomColor(); case INITIAL:
state = INITIAL; break;
case RISE:
destinationHeight = height / 2.0 + (double) random(5) - 2; display.setPixel(position.x, position.y, Yellow.factor(DARKER_FACTOR));
explosionRadius = (double) random(3) + 1; break;
sparkleMax = 100; case EXPLODE:
drawParticle(display, +0.0, +1.0);
explosion = 0.0; drawParticle(display, +0.7, +0.7);
sparkle = 0.0; drawParticle(display, +1.0, +0.0);
drawParticle(display, +0.7, -0.7);
drawParticle(display, +0.0, -1.0);
drawParticle(display, -0.7, -0.7);
drawParticle(display, -1.0, +0.0);
drawParticle(display, -0.7, +0.7);
break;
case SPARKLE:
if (randomBool(2)) drawParticle(display, +0.0, +1.0);
if (randomBool(2)) drawParticle(display, +0.7, +0.7);
if (randomBool(2)) drawParticle(display, +1.0, +0.0);
if (randomBool(2)) drawParticle(display, +0.7, -0.7);
if (randomBool(2)) drawParticle(display, +0.0, -1.0);
if (randomBool(2)) drawParticle(display, -0.7, -0.7);
if (randomBool(2)) drawParticle(display, -1.0, +0.0);
if (randomBool(2)) drawParticle(display, -0.7, +0.7);
break;
} }
}
void step(microseconds_t microseconds) { const char *getStateName() const {
microseconds = microseconds / SLOWER_DIVISOR; switch (state) {
switch (state) { case INITIAL:
case INITIAL: return "INITIAL";
state = RISE; case RISE:
break; return "RISE";
case RISE: case EXPLODE:
if (position.y > destinationHeight) { return "EXPLODE";
position.y = doStep(position.y, 0.0, height, 1000, -microseconds); case SPARKLE:
} else { return "SPARKLE";
state = EXPLODE;
}
break;
case EXPLODE:
if (explosion < explosionRadius) {
explosion = doStep(explosion, 0.0, explosionRadius, 500, +microseconds);
} else {
state = SPARKLE;
}
break;
case SPARKLE:
if (sparkle < sparkleMax) {
sparkle = doStep(sparkle, 0.0, sparkleMax, 1000, +microseconds);
} else {
reset();
}
break;
}
}
void draw(Display &display) {
switch (state) {
case INITIAL:
break;
case RISE:
display.set(position, factor(YELLOW, DARKER_FACTOR));
break;
case EXPLODE:
drawParticle(display, +0.0, +1.0);
drawParticle(display, +0.7, +0.7);
drawParticle(display, +1.0, +0.0);
drawParticle(display, +0.7, -0.7);
drawParticle(display, +0.0, -1.0);
drawParticle(display, -0.7, -0.7);
drawParticle(display, -1.0, +0.0);
drawParticle(display, -0.7, +0.7);
break;
case SPARKLE:
if (randomBool(2)) drawParticle(display, +0.0, +1.0);
if (randomBool(2)) drawParticle(display, +0.7, +0.7);
if (randomBool(2)) drawParticle(display, +1.0, +0.0);
if (randomBool(2)) drawParticle(display, +0.7, -0.7);
if (randomBool(2)) drawParticle(display, +0.0, -1.0);
if (randomBool(2)) drawParticle(display, -0.7, -0.7);
if (randomBool(2)) drawParticle(display, -1.0, +0.0);
if (randomBool(2)) drawParticle(display, -0.7, +0.7);
break;
}
}
const char *getStateName() const {
switch (state) {
case INITIAL:
return "INITIAL";
case RISE:
return "RISE";
case EXPLODE:
return "EXPLODE";
case SPARKLE:
return "SPARKLE";
}
return "[???]";
} }
return "[???]";
}
private: private:
static Color factor(Color color, double factor) { void drawParticle(Display& display, const double x, const double y) const {
return { const auto p = position.plus(x * explosion, y * explosion);
(uint8_t) round(color.r * factor), display.setPixel(p.x, p.y, color.factor(DARKER_FACTOR));
(uint8_t) round(color.g * factor), }
(uint8_t) round(color.b * factor),
};
}
void drawParticle(Display &display, double x, double y) {
display.set(position.plus(x * explosion, y * explosion), factor(color, DARKER_FACTOR));
}
}; };

89
src/mode/Energy/Energy.h Normal file
View File

@ -0,0 +1,89 @@
#ifndef MODE_ENERGY_H
#define MODE_ENERGY_H
#include <patrix/core/mqtt.h>
#include "mode/Mode.h"
#define PHOTOVOLTAIC_ENERGY_KWH "openDTU/pv/ac/yieldtotal"
#define GRID_IMPORT_WH "electricity/grid/energy/import/wh"
#define GRID_EXPORT_WH "electricity/grid/energy/export/wh"
#define POWER_PHOTOVOLTAIC_photovoltaicEnergyKWh_BEFORE_METER_CHANGE 287.995
#define PV_COST_TOTAL_EURO 576.52
#define GRID_KWH_EURO 0.33
#define PV_COST_AMORTISATION_KWH ( PV_COST_TOTAL_EURO / GRID_KWH_EURO )
class Energy final : public Mode {
double photovoltaicEnergyKWh = NAN;
unsigned long photovoltaicEnergyKWhLast = 0;
double gridImportKWh = NAN;
unsigned long gridImportKWhLast = 0;
double gridExportKWh = NAN;
unsigned long gridExportKWhLast = 0;
int page = 0;
public:
explicit Energy(Display& display) : Mode(display) {
timer(0, 7000);
}
const char *getName() override {
return "Energy";
}
void start() override {
mqttSubscribe(PHOTOVOLTAIC_ENERGY_KWH);
mqttSubscribe(GRID_IMPORT_WH);
mqttSubscribe(GRID_EXPORT_WH);
}
void stop() override {
mqttUnsubscribe(PHOTOVOLTAIC_ENERGY_KWH);
mqttUnsubscribe(GRID_IMPORT_WH);
mqttUnsubscribe(GRID_EXPORT_WH);
}
protected:
void tick(uint8_t index, microseconds_t microseconds) override {
page = (page + 1) % 3;
}
void step(microseconds_t microseconds) override {
if (realtimeChanged) {
markDirty();
}
}
void draw(Display& display) override {
const auto photovoltaicEnergyKWhAfterMeterChange = photovoltaicEnergyKWh - POWER_PHOTOVOLTAIC_photovoltaicEnergyKWh_BEFORE_METER_CHANGE;
const auto selfAfterMeterChange = photovoltaicEnergyKWhAfterMeterChange - gridExportKWh;
const auto selfRatio = selfAfterMeterChange / photovoltaicEnergyKWhAfterMeterChange;
const auto selfConsumedKWh = selfRatio * photovoltaicEnergyKWh;
const auto costSaved = selfConsumedKWh * GRID_KWH_EURO;
const auto amortisationPercent = selfConsumedKWh / PV_COST_AMORTISATION_KWH * 100;
display.clear();
if (page == 0) {
display.printf(1, 1, LEFT, Green, "%3.0f€", costSaved);
display.printf(30, 1, RIGHT, White, "%.0f%%", amortisationPercent);
} else if (page == 1) {
display.printf(1, 1, LEFT, Blue, "%3.0f", photovoltaicEnergyKWh);
display.printf(30, 1, RIGHT, Green, "%.0f", selfConsumedKWh);
} else {
display.printf(1, 1, LEFT, Orange, "%4.0f", gridImportKWh);
display.printf(30, 1, RIGHT, Magenta, "%.0f", gridExportKWh);
}
}
};
#endif

View File

@ -11,9 +11,9 @@ public:
double fade = 0.0; double fade = 0.0;
Color color = BLACK; RGBA color = Black;
void animate(microseconds_t microseconds) { void animate(const microseconds_t microseconds) {
// TODO fading does not work as expected // TODO fading does not work as expected
if (alive) { if (alive) {
fade = doStep(fade, 0.0, 255.0, 200, +microseconds); fade = doStep(fade, 0.0, 255.0, 200, +microseconds);
@ -26,40 +26,33 @@ public:
if (alive) { if (alive) {
if (fade < 128) { if (fade < 128) {
return 0; return 0;
} else {
return (uint8_t) ((fade - 128) * 2.0 + 1);
}
} else {
if (fade < 128) {
return (uint8_t) (fade * 2.0 + 1);
} else {
return 255;
} }
return static_cast<uint8_t>((fade - 128) * 2.0 + 1);
} }
if (fade < 128) {
return static_cast<uint8_t>(fade * 2.0 + 1);
}
return 255;
} }
uint8_t getG() const { uint8_t getG() const {
if (alive) { if (alive) {
if (fade < 128) { if (fade < 128) {
return (uint8_t) (fade * 2.0 + 1); return static_cast<uint8_t>(fade * 2.0 + 1);
} else {
return 255;
}
} else {
if (fade < 128) {
return 0;
} else {
return (uint8_t) ((fade - 128) * 2.0 + 1);
} }
return 255;
} }
if (fade < 128) {
return 0;
}
return static_cast<uint8_t>((fade - 128) * 2.0 + 1);
} }
uint8_t getB() const { uint8_t getB() const {
if (fade < 128) { if (fade < 128) {
return 0; return 0;
} else {
return (uint8_t) ((fade - 128) * 2.0 + 1);
} }
return static_cast<uint8_t>((fade - 128) * 2.0 + 1);
} }
}; };

View File

@ -1,209 +1,207 @@
#ifndef MODE_GAME_OF_LIFE_H #ifndef MODE_GAME_OF_LIFE_H
#define MODE_GAME_OF_LIFE_H #define MODE_GAME_OF_LIFE_H
#include "mode/Mode.h"
#include "Cell.h" #include "Cell.h"
#include "mode/Mode.h"
enum ColorMode { enum ColorMode {
BLACK_WHITE, GRAYSCALE, COLOR_FADE, RANDOM_COLOR BLACK_WHITE, GRAYSCALE, COLOR_FADE, RANDOM_COLOR
}; };
class GameOfLife : public Mode { class GameOfLife final : public Mode {
private: ColorMode colorMode;
ColorMode colorMode; size_t cellsSize;
size_t cellsSize; Cell *cells;
Cell *cells; Cell *cellsEnd;
Cell *cellsEnd; Cell *next;
Cell *next; microseconds_t runtime = 0;
microseconds_t runtime = 0; uint8_t steps = 0;
uint8_t steps = 0; uint16_t aliveCount = 0;
uint16_t aliveCount = 0; uint16_t lastAliveCount = 0;
uint16_t lastAliveCount = 0; uint8_t isSteadyCount = 0;
uint8_t isSteadyCount = 0;
public: public:
explicit GameOfLife(Display &display, ColorMode colorMode) : explicit GameOfLife(Display& display, const ColorMode colorMode) : Mode(display),
Mode(display), colorMode(colorMode),
colorMode(colorMode), cellsSize(display.pixelCount * sizeof(Cell)) {
cellsSize(display.pixelCount * sizeof(Cell)) { cells = static_cast<Cell *>(malloc(cellsSize));
cells = (Cell *) malloc(cellsSize); cellsEnd = cells + display.pixelCount;
cellsEnd = cells + display.pixelCount; for (auto cell = cells; cell < cells + display.pixelCount; cell++) {
for (Cell *cell = cells; cell < cells + display.pixelCount; cell++) { kill(cell);
kill(cell);
}
next = (Cell *) malloc(cellsSize);
for (Cell *cell = next; cell < next + display.pixelCount; cell++) {
kill(cell);
}
} }
next = static_cast<Cell *>(malloc(cellsSize));
for (auto cell = next; cell < next + display.pixelCount; cell++) {
kill(cell);
}
}
~GameOfLife() override { ~GameOfLife() override {
if (cells != nullptr) { if (cells != nullptr) {
free(cells); free(cells);
cells = nullptr; cells = nullptr;
}
if (next != nullptr) {
free(next);
next = nullptr;
}
} }
if (next != nullptr) {
free(next);
next = nullptr;
}
}
const char *getName() override { const char *getName() override {
switch (colorMode) { switch (colorMode) {
case BLACK_WHITE: case BLACK_WHITE:
return "Game of Life (black white)"; return "Game of Life (black white)";
case GRAYSCALE: case GRAYSCALE:
return "Game of Life (grayscale)"; return "Game of Life (grayscale)";
case COLOR_FADE: case COLOR_FADE:
return "Game of Life (color fade)"; return "Game of Life (color fade)";
case RANDOM_COLOR: case RANDOM_COLOR:
return "Game of Life (random color)"; return "Game of Life (random color)";
}
return "???";
} }
return "???";
}
protected: protected:
void step(microseconds_t microseconds) override { void step(const microseconds_t microseconds) override {
runtime += microseconds; runtime += microseconds;
if (runtime >= 500000) { if (runtime >= 500000) {
runtime = 0; runtime = 0;
if (lastAliveCount == aliveCount) { if (lastAliveCount == aliveCount) {
isSteadyCount++; isSteadyCount++;
} else { } else {
isSteadyCount = 0; isSteadyCount = 0;
}
lastAliveCount = aliveCount;
if (steps++ == 0 || aliveCount == 0 || isSteadyCount >= 15) {
randomFill();
} else {
nextGeneration();
}
} }
for (Cell *cell = cells; cell < cellsEnd; cell++) { lastAliveCount = aliveCount;
cell->animate(microseconds); if (steps++ == 0 || aliveCount == 0 || isSteadyCount >= 15) {
} randomFill();
} else {
// TODO don't always markDirty. Be more efficient nextGeneration();
markDirty();
}
void draw(Display &display) override {
Cell *cell = cells;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
uint8_t brightness;
switch (colorMode) {
case BLACK_WHITE:
brightness = cell->alive ? 255 : 0;
display.set(x, y, gray(brightness));
break;
case GRAYSCALE:
brightness = (uint8_t) cell->fade;
display.set(x, y, gray(brightness));
break;
case COLOR_FADE:
display.set(x, y, {cell->getR(), cell->getG(), cell->getB()});
break;
case RANDOM_COLOR:
display.set(x, y, cell->alive ? cell->color : BLACK);
break;
}
cell++;
}
} }
} }
for (auto cell = cells; cell < cellsEnd; cell++) {
cell->animate(microseconds);
}
// TODO don't always markDirty. Be more efficient
markDirty();
}
void draw(Display& display) override {
auto cell = cells;
display.clear();
for (auto y = 0; y < height; y++) {
for (auto x = 0; x < width; x++) {
uint8_t brightness;
switch (colorMode) {
case BLACK_WHITE:
brightness = cell->alive ? 255 : 0;
display.setPixel(x, y, RGBA::gray(brightness));
break;
case GRAYSCALE:
brightness = static_cast<uint8_t>(cell->fade);
display.setPixel(x, y, RGBA::gray(brightness));
break;
case COLOR_FADE:
display.setPixel(x, y, {cell->getR(), cell->getG(), cell->getB()});
break;
case RANDOM_COLOR:
display.setPixel(x, y, cell->alive ? cell->color : Black);
break;
}
cell++;
}
}
}
private: private:
void randomFill() { void randomFill() {
isSteadyCount = 0; isSteadyCount = 0;
for (Cell *cell = cells; cell < cellsEnd; cell++) { for (auto cell = cells; cell < cellsEnd; cell++) {
if (random(4) == 0) { if (random(4) == 0) {
if (!cell->alive) { if (!cell->alive) {
spawn(cell); spawn(cell);
} }
} else { } else {
if (cell->alive) { if (cell->alive) {
kill(cell); kill(cell);
}
} }
} }
} }
}
void spawn(Cell *cell) { void spawn(Cell *cell) {
cell->color = randomColor(); cell->color = RGBA::rnd(255);
cell->alive = true; cell->alive = true;
aliveCount++; aliveCount++;
} }
void kill(Cell *cell) { void kill(Cell *cell) {
cell->alive = false; cell->alive = false;
aliveCount--; aliveCount--;
} }
void nextGeneration() { void nextGeneration() {
memcpy(next, cells, cellsSize); memcpy(next, cells, cellsSize);
Cell *src = cells; auto src = cells;
Cell *dst = next; auto dst = next;
for (int y = 0; y < height; y++) { for (auto y = 0; y < height; y++) {
for (int x = 0; x < width; x++) { for (auto x = 0; x < width; x++) {
uint8_t around = countAround(x, y); const auto around = countAround(x, y);
if (src->alive) { if (src->alive) {
if (around <= 2 || 6 <= around) { if (around <= 2 || 6 <= around) {
kill(dst); kill(dst);
}
} else if (around == 3) {
spawn(dst);
} }
src++; } else if (around == 3) {
dst++; spawn(dst);
} }
} src++;
memcpy(cells, next, cellsSize); dst++;
}
void print() {
Cell *cell = cells;
for (int y = 0; y < height; y++) {
Serial.print("|");
for (int x = 0; x < width; x++) {
Serial.print(cell->alive ? "x|" : " |");
cell++;
}
Serial.println();
Serial.println();
} }
} }
memcpy(cells, next, cellsSize);
}
uint8_t countAround(int x, int y) { void print() const {
return countIfAlive(x - 1, y - 1) + countIfAlive(x + 0, y - 1) + countIfAlive(x + 1, y - 1) + auto cell = cells;
countIfAlive(x - 1, y + 0) + /* */ countIfAlive(x + 1, y + 0) + for (auto y = 0; y < height; y++) {
countIfAlive(x - 1, y + 1) + countIfAlive(x + 0, y + 1) + countIfAlive(x + 1, y + 1); Serial.print("|");
} for (auto x = 0; x < width; x++) {
Serial.print(cell->alive ? "x|" : " |");
uint8_t countIfAlive(int x, int y) { cell++;
if (x < 0 || y < 0 || x >= width || y >= height) {
return 0;
} }
return get(x, y)->alive ? 1 : 0; Serial.println();
Serial.println();
} }
}
Cell *get(int x, int y) { uint8_t countAround(const int x, const int y) const {
return cells + width * y + x; return countIfAlive(x - 1, y - 1) + countIfAlive(x + 0, y - 1) + countIfAlive(x + 1, y - 1) +
countIfAlive(x - 1, y + 0) + /* */ countIfAlive(x + 1, y + 0) +
countIfAlive(x - 1, y + 1) + countIfAlive(x + 0, y + 1) + countIfAlive(x + 1, y + 1);
}
uint8_t countIfAlive(const int x, const int y) const {
if (x < 0 || y < 0 || x >= width || y >= height) {
return 0;
} }
return get(x, y)->alive ? 1 : 0;
}
Cell *get(const int x, const int y) const {
return cells + width * y + x;
}
}; };

View File

@ -3,62 +3,59 @@
#include "mode/Mode.h" #include "mode/Mode.h"
class Matrix : public Mode { class Matrix final : public Mode {
private: struct Glyph {
double x = 0;
double y = 0;
double velocity = 0;
uint8_t length = 0;
};
struct Glyph { Glyph glyphs[10];
double x;
double y;
double velocity = 0;
uint8_t length = 0;
};
Glyph glyphs[10];
public: public:
explicit Matrix(Display &display) : explicit Matrix(Display& display) : Mode(display) {
Mode(display) { for (auto& glyph: glyphs) {
for (auto &glyph: glyphs) { resetGlyph(glyph);
resetGlyph(glyph);
}
} }
}
void resetGlyph(Glyph &glyph) const { void resetGlyph(Glyph& glyph) const {
glyph.x = random(width); glyph.x = random(width);
glyph.y = 0; glyph.y = 0;
glyph.velocity = (random(20) + 5); glyph.velocity = random(20) + 5;
glyph.length = random(8) + 2; glyph.length = random(8) + 2;
} }
const char *getName() override { const char *getName() override {
return "Matrix"; return "Matrix";
} }
protected: protected:
void step(microseconds_t microseconds) override { void step(const microseconds_t microseconds) override {
for (auto &glyph: glyphs) { for (auto& glyph: glyphs) {
glyph.y += glyph.velocity * (double) microseconds / 1000000.0; glyph.y += glyph.velocity * static_cast<double>(microseconds) / 1000000.0;
if (glyph.y - glyph.length >= height) { if (glyph.y - glyph.length >= height) {
resetGlyph(glyph); resetGlyph(glyph);
}
} }
markDirty();
} }
markDirty();
}
void draw(Display &display) override { void draw(Display& display) override {
display.clear(); display.clear();
for (auto &glyph: glyphs) { for (const auto& glyph: glyphs) {
for (int i = 0; i < glyph.length; ++i) { for (auto i = 0; i < glyph.length; ++i) {
display.set((int) round(glyph.x), (int) round(glyph.y - i), {64, 128, 64}); display.setPixel(glyph.x, glyph.y - i, {64, 128, 64});
} }
if (((int) round(glyph.y) + glyph.length) % 2 == 0) { if ((static_cast<int>(round(glyph.y)) + glyph.length) % 2 == 0) {
display.set((int) round(glyph.x), (int) round(glyph.y), {0, 255, 0}); display.setPixel(glyph.x, glyph.y, {0, 255, 0});
}
} }
} }
}
}; };

View File

@ -1,8 +1,8 @@
#ifndef MODE_H #ifndef MODE_H
#define MODE_H #define MODE_H
#include "BASICS.h" #include <BASICS.h>
#include "display/Display.h" #include <patrix/core/log.h>
#define FAKE_DAYS 0 #define FAKE_DAYS 0
#define FAKE_HOURS 0 #define FAKE_HOURS 0
@ -10,153 +10,158 @@
#define FAKE_SECONDS 0 #define FAKE_SECONDS 0
enum ModeId { enum ModeId {
NONE, NONE,
BORDER, BORDER,
CLOCK, CLOCK,
GAME_OF_LIFE_BLACK_WHITE, GAME_OF_LIFE_BLACK_WHITE,
GAME_OF_LIFE_GRAYSCALE, GAME_OF_LIFE_GRAYSCALE,
GAME_OF_LIFE_COLOR_FADE, GAME_OF_LIFE_COLOR_FADE,
GAME_OF_LIFE_RANDOM_COLOR, GAME_OF_LIFE_RANDOM_COLOR,
PONG, PONG,
SPACE_INVADERS, SPACE_INVADERS,
COUNT_DOWN, COUNT_DOWN,
COUNT_DOWN_BARS, COUNT_DOWN_BARS,
STARFIELD, COUNT_DOWN_SLEEP,
MATRIX, STARFIELD,
MATRIX,
POWER,
ENERGY,
TIMER,
}; };
class Mode { class Mode {
private: struct Timer {
microseconds_t interval;
microseconds_t rest;
};
struct Timer { Display& _display;
microseconds_t interval;
microseconds_t rest;
};
Display &_display; bool dirty = true;
bool dirty = true; Timer timers[2] = {
{0, 0},
{0, 0},
};
Timer timers[2] = { int8_t lastSecond = -1;
{0, 0},
{0, 0},
};
int8_t lastSecond = -1; milliseconds_t lastSecondChange_Milliseconds = 0;
milliseconds_t lastSecondChange_Milliseconds = 0;
protected: protected:
const uint8_t width; const uint8_t width;
const uint8_t height; const uint8_t height;
bool realtimeOK = false; bool realtimeOK = false;
bool realtimeChanged = false; bool realtimeChanged = false;
tm now = {0, 0, 0, 0, 0, 0, 0, 0, 0}; tm now = {0, 0, 0, 0, 0, 0, 0, 0, 0};
milliseconds_t realtimeMilliseconds = 0; milliseconds_t realtimeMilliseconds = 0;
time_t epoch = 0; time_t nowEpochSeconds = 0;
virtual void tick(uint8_t index, microseconds_t microseconds) {}; virtual void tick(uint8_t index, microseconds_t microseconds) {}
virtual void step(microseconds_t microseconds) {}; virtual void step(const microseconds_t microseconds) {}
virtual void draw(Display &display) {}; virtual void draw(Display& display) {}
void timer(uint8_t index, milliseconds_t milliseconds) { void timer(const uint8_t index, const milliseconds_t milliseconds) {
if (index >= countof(timers)) { if (index >= countof(timers)) {
return; return;
}
timers[index].interval = milliseconds * 1000;
timers[index].rest = milliseconds * 1000;
} }
timers[index].interval = milliseconds * 1000;
timers[index].rest = milliseconds * 1000;
}
void markDirty() { void markDirty() {
dirty = true; dirty = true;
} }
public: public:
explicit Mode(Display &display) : explicit Mode(Display& display) : _display(display),
_display(display), width(display.width),
width(display.width), height(display.height) {
height(display.height) { //
// nothing }
}
virtual ~Mode() = default;
virtual ~Mode() = default;
virtual const char *getName() = 0;
virtual const char *getName() = 0;
virtual void start() {}
virtual void move(int index, int x, int y) {
// virtual void stop() {}
};
virtual void move(int index, int x, int y) {}
virtual void fire(int index) {
// virtual void fire(int index) {}
};
virtual void mqttMessage(const String& topic, const String& message) {}
void loop(microseconds_t microseconds) {
handleRealtime(); virtual void loadConfig() {}
handleTimers(microseconds);
step(microseconds); void loop(const microseconds_t microseconds) {
if (dirty) { handleRealtime();
dirty = false; handleTimers(microseconds);
draw(_display); step(microseconds);
} if (dirty) {
dirty = false;
draw(_display);
} }
}
private: private:
void handleRealtime() { void handleRealtime() {
realtimeUpdate(); realtimeUpdate();
realtimeMillisecondsUpdate(); realtimeMillisecondsUpdate();
} }
void realtimeUpdate() { void realtimeUpdate() {
time_t tmp; time_t tmp;
time(&tmp); time(&tmp);
tmp += ((FAKE_DAYS * 24 + FAKE_HOURS) * 60 + FAKE_MINUTES) * 60 + FAKE_SECONDS; tmp += ((FAKE_DAYS * 24 + FAKE_HOURS) * 60 + FAKE_MINUTES) * 60 + FAKE_SECONDS;
realtimeOK = tmp > 1600000000; realtimeOK = tmp > 1600000000;
if (realtimeOK) { if (realtimeOK) {
realtimeChanged = epoch != tmp; realtimeChanged = nowEpochSeconds != tmp;
if (realtimeChanged) { if (realtimeChanged) {
epoch = tmp; nowEpochSeconds = tmp;
localtime_r(&tmp, &now); localtime_r(&tmp, &now);
now.tm_year += 1900; now.tm_year += 1900;
now.tm_mon += 1; now.tm_mon += 1;
}
} else {
realtimeChanged = false;
} }
} else {
realtimeChanged = false;
} }
}
void realtimeMillisecondsUpdate() { void realtimeMillisecondsUpdate() {
if (lastSecond < 0 || lastSecond != now.tm_sec) { if (lastSecond < 0 || lastSecond != now.tm_sec) {
lastSecond = (int8_t) now.tm_sec; lastSecond = static_cast<int8_t>(now.tm_sec);
lastSecondChange_Milliseconds = millis(); lastSecondChange_Milliseconds = millis();
}
realtimeMilliseconds = millis() - lastSecondChange_Milliseconds;
} }
realtimeMilliseconds = millis() - lastSecondChange_Milliseconds;
}
void handleTimers(microseconds_t microseconds) { void handleTimers(const microseconds_t microseconds) {
for (Timer *timer = timers; timer < timers + countof(timers); timer++) { for (auto timer = timers; timer < timers + countof(timers); timer++) {
if (timer->interval > 0) { if (timer->interval > 0) {
if (microseconds >= timer->rest) { if (microseconds >= timer->rest) {
timer->rest = timer->interval; timer->rest = timer->interval;
tick(timer - timers, timer->interval); tick(timer - timers, timer->interval);
} else { } else {
timer->rest -= microseconds; timer->rest -= microseconds;
}
} }
} }
} }
}
}; };

View File

@ -17,7 +17,7 @@ public:
bool random = true; bool random = true;
void randomMove(uint8_t height) { void randomMove(const uint8_t height) {
if (moveUp) { if (moveUp) {
y--; y--;
if (y <= 0 || randomBool(20)) { if (y <= 0 || randomBool(20)) {

View File

@ -1,193 +1,188 @@
#ifndef MODE_PONG_H #ifndef MODE_PONG_H
#define MODE_PONG_H #define MODE_PONG_H
#include "mode/Mode.h"
#include "Player.h" #include "Player.h"
#include "display/Vector.h" #include "Vector.h"
#include "mode/Mode.h"
class Pong : public Mode { class Pong final : public Mode {
private: enum Status {
SCORE, PLAY, OVER
};
enum Status { Player player0;
SCORE, PLAY, OVER
};
Player player0; Player player1;
Player player1; Vector ball;
Vector ball; Vector velocity;
Vector velocity; Status status = PLAY;
Status status = PLAY; microseconds_t timeoutMicroseconds = 0;
microseconds_t timeoutMicroseconds = 0;
public: public:
explicit Pong(Display &display) : explicit Pong(Display& display) : Mode(display),
Mode(display), ball(width / 2.0, height / 2.0),
ball(width / 2.0, height / 2.0), velocity(Vector::polar(random(360), exp10(1))) {
velocity(Vector::polar(random(360), exp10(1))) { timer(0, 100);
timer(0, 100); spawnBall(random(2) == 0 ? -1 : +1);
spawnBall(random(2) == 0 ? -1 : +1); resetPlayer();
resetPlayer(); }
}
const char *getName() override { const char *getName() override {
return "Pong"; return "Pong";
} }
void move(int index, int x, int y) override { void move(const int index, int x, const int y) override {
if (index == 0) { if (index == 0) {
player0.random = false; player0.random = false;
player0.y = min(height - player0.size, max(0, player0.y + y)); player0.y = min(height - player0.size, max(0, player0.y + y));
} else if (index == 1) { } else if (index == 1) {
player1.random = false; player1.random = false;
player1.y = min(height - player1.size, max(0, player1.y + y)); player1.y = min(height - player1.size, max(0, player1.y + y));
}
} }
}
void fire(int index) override { void fire(const int index) override {
if (index == 0) { if (index == 0) {
player0.random = false; player0.random = false;
} else if (index == 1) { } else if (index == 1) {
player1.random = false; player1.random = false;
}
} }
}
protected: protected:
void tick(uint8_t index, microseconds_t microseconds) override { void tick(uint8_t index, const microseconds_t microseconds) override {
switch (status) { switch (status) {
case SCORE: case SCORE:
timeoutMicroseconds -= microseconds; timeoutMicroseconds -= microseconds;
if (timeoutMicroseconds <= 0) { if (timeoutMicroseconds <= 0) {
status = PLAY; status = PLAY;
} }
break; break;
case PLAY: case PLAY:
ball = ball.plus(velocity); ball = ball.plus(velocity);
if (player0.random) { if (player0.random) {
player0.randomMove(height); player0.randomMove(height);
} }
if (player1.random) { if (player1.random) {
player1.randomMove(height); player1.randomMove(height);
} }
topBottomBounce(); topBottomBounce();
paddleBounce(); paddleBounce();
checkScoring(); checkScoring();
markDirty(); markDirty();
break; break;
case OVER: case OVER:
timeoutMicroseconds -= microseconds; timeoutMicroseconds -= microseconds;
if (timeoutMicroseconds <= 0) { if (timeoutMicroseconds <= 0) {
resetPlayer(); resetPlayer();
status = PLAY; status = PLAY;
timeoutMicroseconds = 0; timeoutMicroseconds = 0;
} }
break; break;
}
} }
}
void draw(Display &display) override { void draw(Display& display) override {
display.clear(); display.clear();
switch (status) { switch (status) {
case SCORE: case SCORE:
display.print(1, 1, player0.score, GREEN); display.printf(1, 1, LEFT, Green, "%d", player0.score);
display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, player1.score, RED); display.printf(30, 1, RIGHT, Red, "%d", player1.score);
break; break;
case PLAY: case PLAY:
for (int i = 0; i < player0.size; ++i) { for (auto i = 0; i < player0.size; ++i) {
display.set(1, (uint8_t) round(player0.y) + i, GREEN); display.setPixel(1, static_cast<uint8_t>(round(player0.y)) + i, Green);
} }
for (int i = 0; i < player1.size; ++i) { for (auto i = 0; i < player1.size; ++i) {
display.set(width - 2, (uint8_t) round(player1.y) + i, RED); display.setPixel(width - 2, static_cast<uint8_t>(round(player1.y)) + i, Red);
} }
display.set((uint8_t) round(ball.x), (uint8_t) round(ball.y), WHITE); display.setPixel(static_cast<uint8_t>(round(ball.x)), static_cast<uint8_t>(round(ball.y)), White);
break; break;
case OVER: case OVER:
if (player0.score > player1.score) { if (player0.score > player1.score) {
display.print(1, 1, 11, GREEN); display.printf(1, 1, LEFT, Green, "W", player0.score);
display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, 12, RED); display.printf(30, 1, RIGHT, Red, "L", player1.score);
} else if (player0.score < player1.score) { } else if (player0.score < player1.score) {
display.print(1, 1, 12, RED); display.printf(1, 1, LEFT, Red, "L", player0.score);
display.print(width - 1 - DISPLAY_CHAR_WIDTH, 1, 11, GREEN); display.printf(30, 1, RIGHT, Green, "W", player1.score);
} }
break; break;
}
} }
}
private: private:
void resetPlayer() { void resetPlayer() {
player0.size = 3; player0.size = 3;
player0.score = 0; player0.score = 0;
player0.y = (height - player0.size) / 2; player0.y = (height - player0.size) / 2;
player0.random = true; player0.random = true;
player1.size = 3; player1.size = 3;
player1.score = 0; player1.score = 0;
player1.y = (height - player1.size) / 2; player1.y = (height - player1.size) / 2;
player1.random = true; player1.random = true;
} }
void topBottomBounce() { void topBottomBounce() {
while (ball.y < 0 || ball.y >= height) { while (ball.y < 0 || ball.y >= height) {
if (ball.y < 0) { if (ball.y < 0) {
ball.y = -ball.y; ball.y = -ball.y;
velocity.y = -velocity.y; velocity.y = -velocity.y;
} else if (ball.y >= height) { } else if (ball.y >= height) {
ball.y = 2 * height - ball.y - 1; ball.y = 2 * height - ball.y - 1;
velocity.y = -velocity.y; velocity.y = -velocity.y;
}
} }
} }
}
void checkScoring() { void checkScoring() {
if (ball.x < 0) { if (ball.x < 0) {
player1.score++; player1.score++;
Serial.println("Player 1 scored"); Serial.printf("Player 1 scored: %d\n", player1.score);
spawnBall(+1); spawnBall(+1);
} else if (ball.x >= width) { } else if (ball.x >= width) {
player0.score++; player0.score++;
Serial.println("Player 0 scored"); Serial.printf("Player 0 scored: %d\n", player0.score);
spawnBall(-1); spawnBall(-1);
}
if (player0.score >= 10 || player1.score >= 10) {
status = OVER;
timeoutMicroseconds = 2000 * 1000;
}
} }
if (player0.score >= 10 || player1.score >= 10) {
void paddleBounce() { status = OVER;
double paddleHitPosition0 = ball.y - player0.y;
if (ball.x >= 1 && ball.x < 2 && paddleHitPosition0 >= 0 && paddleHitPosition0 < player0.size) {
Serial.printf("Player 0 hit: paddleHitPosition0=%.2f\n", paddleHitPosition0);
velocity.x = -velocity.x;
velocity.y = max(-2.0, min(+2.0, velocity.y + paddleHitPosition0 - 1));
ball.x = 3;
return;
}
double paddleHitPosition1 = ball.y - player1.y;
if (ball.x >= width - 2 && ball.x < width - 1 && paddleHitPosition1 >= 0 && paddleHitPosition1 < player1.size) {
Serial.printf("Player 1 hit: paddleHitPosition1=%.2f\n", paddleHitPosition1);
velocity.x = -velocity.x;
velocity.y = max(-2.0, min(+2.0, velocity.y + paddleHitPosition1 - 1));
ball.x = width - 4;
}
}
void spawnBall(int direction) {
ball.x = (double) width / 2.0;
ball.y = (double) height / 2.0;
velocity.x = direction;
velocity.y = 0;
status = SCORE;
timeoutMicroseconds = 2000 * 1000; timeoutMicroseconds = 2000 * 1000;
} }
}
void paddleBounce() {
const auto paddleHitPosition0 = ball.y - player0.y;
if (ball.x >= 1 && ball.x < 2 && paddleHitPosition0 >= 0 && paddleHitPosition0 < player0.size) {
velocity.x = -velocity.x;
velocity.y = max(-2.0, min(+2.0, velocity.y + paddleHitPosition0 - 1));
ball.x = 3;
return;
}
const auto paddleHitPosition1 = ball.y - player1.y;
if (ball.x >= width - 2 && ball.x < width - 1 && paddleHitPosition1 >= 0 && paddleHitPosition1 < player1.size) {
velocity.x = -velocity.x;
velocity.y = max(-2.0, min(+2.0, velocity.y + paddleHitPosition1 - 1));
ball.x = width - 4;
}
}
void spawnBall(const int direction) {
ball.x = static_cast<double>(width) / 2.0;
ball.y = static_cast<double>(height) / 2.0;
velocity.x = direction;
velocity.y = 0;
status = SCORE;
timeoutMicroseconds = 2000 * 1000;
}
}; };

70
src/mode/Power/Power.h Normal file
View File

@ -0,0 +1,70 @@
#ifndef MODE_POWER_H
#define MODE_POWER_H
#include <patrix/core/mqtt.h>
#include "mode/Mode.h"
#define PHOTOVOLTAIC_POWER_W "openDTU/pv/ac/power"
#define GRID_POWER_W "electricity/grid/power/signed/w"
class Power final : public Mode {
double photovoltaicPowerW = NAN;
unsigned long photovoltaicPowerWLast = 0;
double gridPowerW = NAN;
unsigned long gridPowerWLast = 0;
public:
explicit Power(Display& display) : Mode(display) {
// nothing
}
const char *getName() override {
return "Power";
}
void start() override {
mqttSubscribe(PHOTOVOLTAIC_POWER_W);
mqttSubscribe(GRID_POWER_W);
}
void stop() override {
mqttUnsubscribe(PHOTOVOLTAIC_POWER_W);
mqttUnsubscribe(GRID_POWER_W);
}
protected:
void mqttMessage(const String& topic, const String& message) override {
if (topic.equals(PHOTOVOLTAIC_POWER_W)) {
photovoltaicPowerW = message.toDouble();
photovoltaicPowerWLast = millis();
} else if (topic.equals(GRID_POWER_W)) {
gridPowerW = message.toDouble();
gridPowerWLast = millis();
}
}
void step(microseconds_t microseconds) override {
if (realtimeChanged) {
markDirty();
}
}
void draw(Display& display) override {
display.clear();
const auto pvColor = photovoltaicPowerW >= 100 ? Green : photovoltaicPowerW >= 20 ? Yellow : Red;
display.printf(1, 1, LEFT, pvColor, "%3.0f", photovoltaicPowerW);
const auto gridColor = gridPowerW >= 20 ? Orange : gridPowerW >= -20 ? Green : Magenta;
display.printf(30, 1, RIGHT, gridColor, "%.0f", gridPowerW);
}
};
#endif

View File

@ -6,279 +6,276 @@
#include "mode/Mode.h" #include "mode/Mode.h"
struct Rocket { struct Rocket {
bool alive; bool alive;
uint16_t flash; uint16_t flash;
uint8_t x; uint8_t x;
uint8_t y; uint8_t y;
}; };
struct Invader { struct Invader {
bool alive; bool alive;
uint8_t x; uint8_t x;
uint8_t y; uint8_t y;
}; };
class SpaceInvaders : public Mode { class SpaceInvaders final : public Mode {
private: microseconds_t heroRuntime = 0;
uint8_t heroX = 0;
bool heroLeft = false;
uint8_t heroShoot = 0;
bool randomEnabled = true;
microseconds_t heroRuntime = 0; uint8_t invadersCountX;
uint8_t heroX = 0; uint8_t invadersCountY;
bool heroLeft = false; uint8_t invadersAlive = 0;
uint8_t heroShoot = 0;
bool randomEnabled = true;
uint8_t invadersCountX; microseconds_t swarmRuntime = 0;
uint8_t invadersCountY; uint8_t swarmY = 0;
uint8_t invadersAlive = 0; bool swarmLeft = false;
bool swarmDown = false;
uint8_t swarmX = 0;
Invader *swarmBegin;
Invader *swarmEnd;
microseconds_t swarmRuntime = 0; microseconds_t rocketRuntime = 0;
uint8_t swarmY = 0; Rocket *rocketsBegin;
bool swarmLeft = false; Rocket *rocketsEnd;
bool swarmDown = false;
uint8_t swarmX = 0;
Invader *swarmBegin;
Invader *swarmEnd;
microseconds_t rocketRuntime = 0;
Rocket *rocketsBegin;
Rocket *rocketsEnd;
public: public:
explicit SpaceInvaders(Display &display) : explicit SpaceInvaders(Display& display) : Mode(display),
Mode(display), invadersCountX(width / 3),
invadersCountX(width / 3), invadersCountY(height / 4) {
invadersCountY(height / 4) {
swarmBegin = (Invader *) malloc(sizeof(Invader) * invadersCountX * invadersCountY); swarmBegin = static_cast<Invader *>(malloc(sizeof(Invader) * invadersCountX * invadersCountY));
swarmEnd = swarmBegin + invadersCountX * invadersCountY; swarmEnd = swarmBegin + invadersCountX * invadersCountY;
rocketsBegin = (Rocket *) malloc(sizeof(Rocket) * ROCKET_MAX); rocketsBegin = static_cast<Rocket *>(malloc(sizeof(Rocket) * ROCKET_MAX));
rocketsEnd = rocketsBegin + ROCKET_MAX; rocketsEnd = rocketsBegin + ROCKET_MAX;
reset(); reset();
} }
~SpaceInvaders() override { ~SpaceInvaders() override {
free(swarmBegin); free(swarmBegin);
free(rocketsBegin); free(rocketsBegin);
}; }
const char *getName() override { const char *getName() override {
return "Space Invaders"; return "Space Invaders";
} }
void move(int index, int x, int y) override { void move(int index, const int x, int y) override {
randomEnabled = false; randomEnabled = false;
heroX = max(1, min(width - 2, heroX + x)); heroX = max(1, min(width - 2, heroX + x));
} }
void fire(int index) override { void fire(int index) override {
randomEnabled = false; randomEnabled = false;
shoot(); shoot();
} }
protected: protected:
void step(microseconds_t microseconds) override { void step(const microseconds_t microseconds) override {
stepRockets(microseconds); stepRockets(microseconds);
stepInvaders(microseconds); stepInvaders(microseconds);
if (randomEnabled) { if (randomEnabled) {
randomStepHero(microseconds); randomStepHero(microseconds);
}
collide();
if (invadersAlive == 0) {
Serial.println("WINNER!");
reset();
}
// TODO this is only correct if there still are any invaders in the last row (otherwise we "Game Over" too early)
if (swarmY + (invadersCountY - 1) * 2 >= height - 1) {
Serial.println("GAME OVER");
reset();
}
// TODO don't always markDirty. Be more efficient
markDirty();
} }
void draw(Display &display) override { collide();
display.clear(); if (invadersAlive == 0) {
drawInvaders(display); Serial.println("WINNER!");
drawRockets(display); reset();
drawHero(display);
} }
// TODO this is only correct if there still are any invaders in the last row (otherwise we "Game Over" too early)
if (swarmY + (invadersCountY - 1) * 2 >= height - 1) {
Serial.println("GAME OVER");
reset();
}
// TODO don't always markDirty. Be more efficient
markDirty();
}
void draw(Display& display) override {
display.clear();
drawInvaders(display);
drawRockets(display);
drawHero(display);
}
private: private:
void stepRockets(microseconds_t microseconds) { void stepRockets(const microseconds_t microseconds) {
rocketRuntime += microseconds; rocketRuntime += microseconds;
if (rocketRuntime > 200000) { if (rocketRuntime > 200000) {
rocketRuntime = rocketRuntime % 200000; rocketRuntime = rocketRuntime % 200000;
for (Rocket *rocket = rocketsBegin; rocket < rocketsEnd; rocket++) { for (auto rocket = rocketsBegin; rocket < rocketsEnd; rocket++) {
if (rocket->alive) { if (rocket->alive) {
if (rocket->y == 0) { if (rocket->y == 0) {
rocket->alive = false; rocket->alive = false;
} else { } else {
rocket->y -= 1; rocket->y -= 1;
} }
} else if (rocket->flash > 0) { } else if (rocket->flash > 0) {
if (rocket->flash < microseconds) { if (rocket->flash < microseconds) {
rocket->flash = 0; rocket->flash = 0;
} else { } else {
rocket->flash -= microseconds; rocket->flash -= microseconds;
}
} }
} }
} }
} }
}
void stepInvaders(microseconds_t microseconds) { void stepInvaders(const microseconds_t microseconds) {
swarmRuntime += microseconds; swarmRuntime += microseconds;
if (swarmDown && swarmRuntime > 500000) { if (swarmDown && swarmRuntime > 500000) {
swarmDown = false; swarmDown = false;
swarmY++; swarmY++;
} }
if (swarmRuntime >= 1000000) { if (swarmRuntime >= 1000000) {
swarmRuntime = swarmRuntime % 1000000; swarmRuntime = swarmRuntime % 1000000;
if (swarmLeft) { if (swarmLeft) {
swarmX--; swarmX--;
if (swarmX == 0) {
swarmLeft = false;
}
} else {
swarmX++;
if (swarmX == 3) {
swarmLeft = true;
}
}
if (swarmX == 0) { if (swarmX == 0) {
swarmDown = true; swarmLeft = false;
}
} else {
swarmX++;
if (swarmX == 3) {
swarmLeft = true;
} }
} }
} if (swarmX == 0) {
swarmDown = true;
void randomStepHero(microseconds_t microseconds) {
heroRuntime += microseconds;
if (heroRuntime >= 50000) {
heroRuntime = heroRuntime % 50000;
if (heroLeft) {
heroX--;
if (heroX <= 1 || randomBool(20)) {
heroLeft = false;
}
} else {
heroX++;
if (heroX >= width - 2 || randomBool(20)) {
heroLeft = true;
}
}
heroShoot++;
if (heroShoot >= 20 || randomBool(5)) {
heroShoot = 0;
shoot();
}
} }
} }
}
void shoot() { void randomStepHero(const microseconds_t microseconds) {
for (Rocket *rocket = rocketsBegin; rocket < rocketsEnd; rocket++) { heroRuntime += microseconds;
if (!rocket->alive && rocket->flash == 0) { if (heroRuntime >= 50000) {
rocket->alive = true; heroRuntime = heroRuntime % 50000;
rocket->x = heroX; if (heroLeft) {
rocket->y = height - 2; heroX--;
if (heroX <= 1 || randomBool(20)) {
heroLeft = false;
}
} else {
heroX++;
if (heroX >= width - 2 || randomBool(20)) {
heroLeft = true;
} }
} }
}
void collide() { heroShoot++;
for (Rocket *rocket = rocketsBegin; rocket < rocketsEnd; rocket++) { if (heroShoot >= 20 || randomBool(5)) {
if (!rocket->alive) { heroShoot = 0;
shoot();
}
}
}
void shoot() const {
for (auto rocket = rocketsBegin; rocket < rocketsEnd; rocket++) {
if (!rocket->alive && rocket->flash == 0) {
rocket->alive = true;
rocket->x = heroX;
rocket->y = height - 2;
}
}
}
void collide() {
for (auto rocket = rocketsBegin; rocket < rocketsEnd; rocket++) {
if (!rocket->alive) {
continue;
}
for (auto invader = swarmBegin; invader < swarmEnd; invader++) {
if (!invader->alive) {
continue; continue;
} }
for (Invader *invader = swarmBegin; invader < swarmEnd; invader++) { if (collide(rocket, invader)) {
if (!invader->alive) { rocket->alive = false;
continue; rocket->flash = 1000;
} invader->alive = false;
if (collide(rocket, invader)) { invadersAlive--;
rocket->alive = false; break;
rocket->flash = 1000;
invader->alive = false;
invadersAlive--;
break;
}
} }
} }
} }
}
bool collide(const Rocket *rocket, const Invader *invader) const { bool collide(const Rocket *rocket, const Invader *invader) const {
return swarmY + invader->y * 2 == rocket->y return swarmY + invader->y * 2 == rocket->y
&& swarmX + invader->x * 3 <= rocket->x && swarmX + invader->x * 3 <= rocket->x
&& swarmX + invader->x * 3 + 1 >= rocket->x; && swarmX + invader->x * 3 + 1 >= rocket->x;
} }
void drawInvaders(Display &display) { void drawInvaders(Display& display) const {
for (Invader *invader = swarmBegin; invader < swarmEnd; invader++) { for (auto invader = swarmBegin; invader < swarmEnd; invader++) {
if (invader->alive) { if (invader->alive) {
display.set(swarmX + invader->x * 3 + 0, swarmY + invader->y * 2, RED); display.setPixel(swarmX + invader->x * 3 + 0, swarmY + invader->y * 2, Red);
display.set(swarmX + invader->x * 3 + 1, swarmY + invader->y * 2, RED); display.setPixel(swarmX + invader->x * 3 + 1, swarmY + invader->y * 2, Red);
}
} }
} }
}
void drawRockets(Display &display) { void drawRockets(Display& display) const {
for (Rocket *rocket = rocketsBegin; rocket < rocketsEnd; rocket++) { for (auto rocket = rocketsBegin; rocket < rocketsEnd; rocket++) {
if (rocket->alive) { if (rocket->alive) {
display.set(rocket->x, rocket->y, YELLOW); display.setPixel(rocket->x, rocket->y, Yellow);
} else if (rocket->flash > 0) { } else if (rocket->flash > 0) {
display.set(rocket->x - 1, rocket->y - 1, gray(rocket->flash)); display.setPixel(rocket->x - 1, rocket->y - 1, RGBA::gray(rocket->flash));
display.set(rocket->x - 1, rocket->y + 1, gray(rocket->flash)); display.setPixel(rocket->x - 1, rocket->y + 1, RGBA::gray(rocket->flash));
display.set(rocket->x + 0, rocket->y + 0, gray(rocket->flash)); display.setPixel(rocket->x + 0, rocket->y + 0, RGBA::gray(rocket->flash));
display.set(rocket->x + 1, rocket->y + 1, gray(rocket->flash)); display.setPixel(rocket->x + 1, rocket->y + 1, RGBA::gray(rocket->flash));
display.set(rocket->x + 1, rocket->y - 1, gray(rocket->flash)); display.setPixel(rocket->x + 1, rocket->y - 1, RGBA::gray(rocket->flash));
}
} }
} }
}
void drawHero(Display &display) { void drawHero(Display& display) const {
display.set(heroX - 1, height - 1, BLUE); display.setPixel(heroX - 1, height - 1, Blue);
display.set(heroX + 0, height - 1, BLUE); display.setPixel(heroX + 0, height - 1, Blue);
display.set(heroX + 1, height - 1, BLUE); display.setPixel(heroX + 1, height - 1, Blue);
}
void reset() {
heroRuntime = 0;
heroLeft = false;
heroShoot = 0;
heroX = width / 2;
randomEnabled = true;
rocketRuntime = 0;
for (auto rocket = rocketsBegin; rocket < rocketsEnd; rocket++) {
rocket->alive = false;
rocket->flash = 0;
rocket->x = 0;
rocket->y = 0;
} }
void reset() { swarmRuntime = 0;
heroRuntime = 0; invadersAlive = invadersCountX * invadersCountY;
heroLeft = false; swarmX = 0;
heroShoot = 0; swarmY = 0;
heroX = width / 2; swarmLeft = false;
randomEnabled = true; swarmDown = false;
uint8_t n = 0;
rocketRuntime = 0; for (auto invader = swarmBegin; invader < swarmEnd; invader++) {
for (Rocket *rocket = rocketsBegin; rocket < rocketsEnd; rocket++) { invader->alive = true;
rocket->alive = false; invader->x = n % invadersCountX;
rocket->flash = 0; invader->y = n / invadersCountX;
rocket->x = 0; n++;
rocket->y = 0;
}
swarmRuntime = 0;
invadersAlive = invadersCountX * invadersCountY;
swarmX = 0;
swarmY = 0;
swarmLeft = false;
swarmDown = false;
uint8_t n = 0;
for (Invader *invader = swarmBegin; invader < swarmEnd; invader++) {
invader->alive = true;
invader->x = n % invadersCountX;
invader->y = n / invadersCountX;
n++;
}
} }
}
}; };

View File

@ -3,75 +3,73 @@
#include "mode/Mode.h" #include "mode/Mode.h"
class Starfield : public Mode { class Starfield final : public Mode {
private: Vector center;
Vector center; Vector centerNext;
Vector centerNext; Vector stars[20];
Vector stars[20];
public: public:
explicit Starfield(Display &display) : explicit Starfield(Display& display) : Mode(display),
Mode(display), center(width / 2.0, height / 2.0),
center(width / 2.0, height / 2.0), centerNext(center.x, center.y) {
centerNext(center.x, center.y) { for (auto& star: stars) {
for (auto &star: stars) { star.x = random(width);
star.x = random(width); star.y = random(height);
star.y = random(height);
}
} }
}
const char *getName() override { const char *getName() override {
return "Starfield"; return "Starfield";
} }
protected: protected:
void step(microseconds_t microseconds) override { void step(const microseconds_t microseconds) override {
stepCenter(); stepCenter();
for (auto &star: stars) { for (auto& star: stars) {
const Vector velocity = star.minus(center).multiply((double) microseconds / 200000.0); const auto velocity = star.minus(center).multiply(static_cast<double>(microseconds) / 200000.0);
star = star.plus(velocity); star = star.plus(velocity);
if (star.x < 0 || star.x >= width || star.y < 0 || star.y >= height) { if (star.x < 0 || star.x >= width || star.y < 0 || star.y >= height) {
star = center.plus(Vector::polar(random(360), 1)); star = center.plus(Vector::polar(random(360), 1));
}
}
// TODO don't always markDirty. Be more efficient
markDirty();
}
void stepCenter() {
// TODO moving center overtakes moving stars (less stars in direction of moving center)
// Vector diff = centerNext.minus(center);
// if (diff.length < 0.01) {
// centerNext = Vector(random(width), random(height));
// } else {
// if (diff.x >= 0) {
// center.x += min(0.1, diff.x);
// } else {
// center.x += max(-0.1, diff.x);
// }
// if (diff.y >= 0) {
// center.y += min(0.1, diff.y);
// } else {
// center.y += max(-0.1, diff.y);
// }
// }
}
void draw(Display &display) override {
display.clear();
for (auto &star: stars) {
uint8_t brightness = (uint8_t) round(255.0 * star.minus(center).length / (width / 2.0));
display.set(star, gray(brightness));
} }
} }
// TODO don't always markDirty. Be more efficient
markDirty();
}
// ReSharper disable once CppMemberFunctionMayBeStatic
void stepCenter() {
// TODO moving center overtakes moving stars (less stars in direction of moving center)
// Vector diff = centerNext.minus(center);
// if (diff.length < 0.01) {
// centerNext = Vector(random(width), random(height));
// } else {
// if (diff.x >= 0) {
// center.x += min(0.1, diff.x);
// } else {
// center.x += max(-0.1, diff.x);
// }
// if (diff.y >= 0) {
// center.y += min(0.1, diff.y);
// } else {
// center.y += max(-0.1, diff.y);
// }
// }
}
void draw(Display& display) override {
display.clear();
for (auto& star: stars) {
const auto brightness = static_cast<uint8_t>(round(255.0 * star.minus(center).length / (width / 2.0)));
display.setPixel(star.x, star.y, RGBA::gray(brightness));
}
}
}; };
#endif #endif

84
src/mode/Timer/Timer.h Normal file
View File

@ -0,0 +1,84 @@
#ifndef MODE_TIMER_H
#define MODE_TIMER_H
#include "mode/Mode.h"
#define DEFAULT_DURATION_MILLIS (6 * 60 * 1000L)
class Timer2 final : public Mode {
long timerMillis = DEFAULT_DURATION_MILLIS;
long restMillis = timerMillis;
unsigned long lastMillis = 0;
uint16_t days = 0;
uint16_t hours = 0;
uint16_t minutes = 0;
uint16_t seconds = 0;
public:
explicit Timer2(Display& display) : Mode(display) {
//
}
const char *getName() override {
return "Timer";
}
void loadConfig() override {
const auto newTimerMillis = config.get("timerMillis", DEFAULT_DURATION_MILLIS);
if (restMillis > 0) {
restMillis += newTimerMillis - timerMillis;
}
timerMillis = newTimerMillis;
}
void start() override {
restMillis = timerMillis;
lastMillis = millis();
}
protected:
void step(microseconds_t microseconds) override {
const auto now = millis();
const auto deltaMillis = now - lastMillis;
lastMillis = now;
restMillis -= static_cast<long>(deltaMillis);
if (restMillis < 0) {
restMillis = 0;
}
const auto restSeconds = restMillis / 1000;
days = restSeconds / (24 * 60 * 60);
hours = restSeconds / (60 * 60) % 24;
minutes = restSeconds / 60 % 60;
seconds = restSeconds % 60;
markDirty();
}
void draw(Display& display) override {
display.clear();
if (days > 10) {
display.printf(30, 1, RIGHT, White, "%d TAGE", days);
} else if (days > 0) {
display.printf(30, 1, RIGHT, White, "%d %2d:%02d", days, hours, minutes);
} else if (hours > 0) {
display.printf(30, 1, RIGHT, White, "%d:%02d:%02d", hours, minutes, seconds);
} else if (minutes > 0) {
display.printf(30, 1, RIGHT, White, "%d:%02d", minutes, seconds);
} else {
display.printf(30, 1, RIGHT, Orange, "%d", seconds);
}
}
};
#endif

View File

@ -1,52 +0,0 @@
#include <Arduino.h>
#include "serial.h"
#include "display.h"
#include "config.h"
#include "mode.h"
void serial_setup() {
Serial.begin(115200);
Serial.println("\n\n\nStartup!");
}
void serial_loop() {
if (Serial.available()) {
int input = Serial.read();
switch (input) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
setMode((ModeId) (input - '0'));
break;
case 'a':
case 'b':
setMode((ModeId) (input - 'a' + 10));
break;
case 'r':
setSpeed(1.0);
break;
case '+':
setBrightness(display.getBrightness() + 10);
break;
case '-':
setBrightness(display.getBrightness() - 10);
break;
case ',':
setSpeed(config.speed / 1.1);
break;
case '.':
setSpeed(config.speed * 1.1);
break;
default:
Serial.printf("Unknown command: %c\n", input);
break;
}
}
}

View File

@ -1,8 +0,0 @@
#ifndef MEDIATABLE_SERIAL_H
#define MEDIATABLE_SERIAL_H
void serial_loop();
void serial_setup();
#endif

View File

@ -1,272 +0,0 @@
#include <WebServer.h>
#include <cmath>
#include "server.h"
#include "mode/Mode.h"
#include "mode.h"
#include "display.h"
#include "config.h"
static const char *const style = R"(
<style>
body {
font-family: sans-serif;
font-size: 8vw;
margin: 0;
}
button{
width: 33vmin;
height: 33vmin;
font-size: 9vw;
}
table{
border-collapse: collapse;
}
td{
text-align: center;
}
</style>
)";
static const char *const script = R"(
<script>
function get(path){
var r = new XMLHttpRequest();
r.open("GET", path, true);
r.send();
}
</script>
)";
WebServer server(80);
void web_index();
void web_player();
void web_player_move();
void web_player_fire();
void web_setMode();
void web_brighter();
void web_darker();
void web_faster();
void web_slower();
void web_fps_on();
void web_fps_off();
void server_setup() {
server.on("/", web_index);
server.on("/player", web_player);
server.on("/player/move", web_player_move);
server.on("/player/fire", web_player_fire);
server.on("/mode", web_setMode);
server.on("/brighter", web_brighter);
server.on("/darker", web_darker);
server.on("/faster", web_faster);
server.on("/slower", web_slower);
server.on("/fps/on", web_fps_on);
server.on("/fps/off", web_fps_off);
server.begin();
}
void server_loop() {
server.handleClient();
}
void web_index() {
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "text/html", "");
server.sendContent(style);
server.sendContent(script);
server.sendContent(R"(<p>)");
server.sendContent(R"(<a href="/player?index=0">Player 0</a><br>)");
server.sendContent(R"(<a href="/player?index=1">Player 1</a><br>)");
server.sendContent(R"(</p>)");
server.sendContent(R"(<p>)");
server.sendContent(R"(<a onclick="get('/mode?mode=0');">NONE</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=1');">BORDER</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=2');">CLOCK</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=3');">GAME_OF_LIFE_BLACK_WHITE</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=4');">GAME_OF_LIFE_GRAYSCALE</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=5');">GAME_OF_LIFE_COLOR_FADE</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=6');">GAME_OF_LIFE_RANDOM_COLOR</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=7');">PONG</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=8');">SPACE_INVADERS</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=9');">COUNT_DOWN</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=10');">COUNT_DOWN_BARS</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=11');">STARFIELD</a><br>)");
server.sendContent(R"(<a onclick="get('/mode?mode=12');">MATRIX</a><br>)");
server.sendContent(R"(</p>)");
server.sendContent(R"(<p>)");
server.sendContent(R"(Helligkeit: <a onclick="get('/brighter');">+</a> / <a onclick="get('/darker');">-</a><br>)");
server.sendContent(R"(Geschwindigkeit: <a onclick="get('/faster');">+</a> / <a onclick="get('/slower');">-</a><br>)");
server.sendContent(R"(FPS: <a onclick="get('/fps/on');">EIN</a> / <a onclick="get('/fps/off');">AUS</a><br>)");
server.sendContent(R"(</p>)");
server.client().flush();
}
void web_player() {
char buffer[128];
if (!server.hasArg("index")) {
server.send(400, "text/plain", "Missing 'index'");
return;
}
double value = strtod(server.arg("index").c_str(), nullptr);
int index = (int) value;
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "text/html", "");
server.sendContent(style);
server.sendContent(script);
server.sendContent(R"(<meta name="viewport" content= "width=device-width, user-scalable=no">)");
server.sendContent(R"(<table>)");
server.sendContent(R"(<tr>)");
server.sendContent(R"(<td><a href='/'>&larr;</td>)");
server.sendContent(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button onclick="get('/player/move?index=%d&x=0&y=-1');">&uarr;</button><br>)", index);
server.sendContent(buffer);
server.sendContent(R"(</td>)");
server.sendContent(R"(<td>&nbsp;</td>)");
server.sendContent(R"(</tr>)");
server.sendContent(R"(<tr>)");
server.sendContent(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button onclick="get('/player/move?index=%d&x=-1&y=0');">&larr;</button><br>)", index);
server.sendContent(buffer);
server.sendContent(R"(</td>)");
server.sendContent(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button onclick="get('/player/fire?index=%d');">X</button><br>)", index);
server.sendContent(buffer);
server.sendContent(R"(</td>)");
server.sendContent(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button onclick="get('/player/move?index=%d&x=+1&y=0');">&rarr;</button><br>)", index);
server.sendContent(buffer);
server.sendContent(R"(</td>)");
server.sendContent(R"(</tr>)");
server.sendContent(R"(<tr>)");
server.sendContent(R"(<td>&nbsp;</td>)");
server.sendContent(R"(<td>)");
snprintf(buffer, sizeof buffer, R"(<button onclick="get('/player/move?index=%d&x=0&y=+1');">&darr;</button><br>)", index);
server.sendContent(buffer);
server.sendContent(R"(</td>)");
server.sendContent(R"(<td>&nbsp;</td>)");
server.sendContent(R"(</tr>)");
server.sendContent(R"(</table>)");
server.client().flush();
}
void web_player_move() {
double value;
if (!server.hasArg("index")) {
server.send(400, "text/plain", "Missing 'index'");
return;
}
value = strtod(server.arg("index").c_str(), nullptr);
int index = (int) value;
if (!server.hasArg("x")) {
server.send(400, "text/plain", "Missing 'x'");
return;
}
value = strtod(server.arg("x").c_str(), nullptr);
int x = (int) value;
if (!server.hasArg("y")) {
server.send(400, "text/plain", "Missing 'y'");
return;
}
value = strtod(server.arg("y").c_str(), nullptr);
int y = (int) value;
modeMove(index, x, y);
server.send(200, "application/json", "true");
}
void web_player_fire() {
double value;
if (!server.hasArg("index")) {
server.send(400, "text/plain", "Missing 'index'");
return;
}
value = strtod(server.arg("index").c_str(), nullptr);
int index = (int) value;
modeFire(index);
server.send(200, "application/json", "true");
}
void web_setMode() {
if (!server.hasArg("mode")) {
server.send(400, "text/plain", "Missing 'mode'");
return;
}
double value = strtod(server.arg("mode").c_str(), nullptr);
if (isnan(value)) {
server.send(400, "text/plain", "'mode' not a number");
return;
}
setMode((ModeId) value);
server.send(200);
}
void web_brighter() {
setBrightness(display.getBrightness() + 10);
server.send(200);
}
void web_darker() {
setBrightness(display.getBrightness() - 10);
server.send(200);
}
void web_faster() {
setSpeed(config.speed * 1.1);
server.send(200);
}
void web_slower() {
setSpeed(config.speed / 1.1);
server.send(200);
}
void web_fps_on() {
display.fpsShow = true;
server.send(200);
}
void web_fps_off() {
display.fpsShow = false;
server.send(200);
}

View File

@ -1,8 +0,0 @@
#ifndef MEDIATABLE_SERVER_H
#define MEDIATABLE_SERVER_H
void server_setup();
void server_loop();
#endif

View File

@ -1,98 +0,0 @@
#include "wifi.h"
#include "display.h"
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <esp_sntp.h>
bool connected = false;
void ntp_setup();
uint32_t ip2int(const IPAddress &ip);
void timeSyncCallback(struct timeval *tv);
char *calculateGateway(char *calculatedGateway, size_t size);
void wifi_setup() {
WiFi.begin("HappyNet", "1Grausame!Sackratte7");
yield();
ArduinoOTA.onStart([]() {
Serial.print("\n\nOTA Update: ");
display.clear();
display.loop();
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
double ratio = (double) progress / (double) total;
Serial.printf("\rOTA Update: %3.0f%%", ratio * 100);
auto index = (uint16_t) round(ratio * (double) display.pixelCount);
auto color = (uint8_t) round(ratio * 255.0);
display.set(index, {(uint8_t) (255 - color), color, 0});
display.loop();
});
ArduinoOTA.onEnd([]() {
Serial.println("\nOTA Success!\n");
display.clear();
display.loop();
});
ArduinoOTA.onError([](int error) {
Serial.println("\nOTA Failure!\n");
display.clear();
display.loop();
});
ArduinoOTA.begin();
yield();
}
void wifi_loop() {
ArduinoOTA.handle();
bool hasIp = (uint32_t) WiFi.localIP() != 0;
if (!connected) {
if (hasIp) {
connected = true;
Serial.printf("WiFi connected: %s\n", WiFi.localIP().toString().c_str());
ntp_setup();
}
} else {
if (!hasIp) {
connected = false;
Serial.println("WiFi disconnected!");
}
}
}
void ntp_setup() {
char calculatedGateway[16] = {0};
calculateGateway(calculatedGateway, sizeof(calculatedGateway));
sntp_set_time_sync_notification_cb(timeSyncCallback);
Serial.printf("configTime(%s / %s / %s)\n", WiFi.gatewayIP().toString().c_str(), calculatedGateway, "pool.ntp.org");
configTime(3600, 3600, "pool.ntp.org", WiFi.gatewayIP().toString().c_str(), calculatedGateway);
yield();
}
char *calculateGateway(char *calculatedGateway, size_t size) {
uint32_t local = ip2int(WiFi.localIP());
uint32_t netmask = ip2int(WiFi.subnetMask());
uint32_t gateway = local & netmask + 1;
snprintf(
calculatedGateway,
size,
"%u.%u.%u.%u",
(gateway >> 24) & 0xFF,
(gateway >> 16) & 0xFF,
(gateway >> 8) & 0xFF,
(gateway >> 0) & 0xFF
);
return calculatedGateway;
}
uint32_t ip2int(const IPAddress &ip) {
return ((ip[0] * 256 + ip[1]) * 256 + ip[2]) * 256 + ip[3];
}
void timeSyncCallback(struct timeval *tv) {
Serial.printf("timeSyncCallback: %ld\n", tv->tv_sec);
}

View File

@ -1,8 +0,0 @@
#ifndef MEDIATABLE_WIFI_H
#define MEDIATABLE_WIFI_H
void wifi_setup();
void wifi_loop();
#endif