-
+
+
+
+
diff --git a/data/load-data.js b/data/load-data.js
index 2ae6f1e..1ec35a7 100644
--- a/data/load-data.js
+++ b/data/load-data.js
@@ -1,35 +1,47 @@
+import {
+ showLoadingScreen,
+ showError,
+ hideLoadingScreen,
+} from "./loading-screen.js";
+
const form = document.querySelector("form");
-async function loadData() {
- try {
- const req = await fetch("/config", {
- method: "GET",
- });
- if (!req.ok) {
- throw new Error(`Response status: ${req.status}`);
- }
-
- const json = await req.json();
- console.log(json);
- return json;
- } catch (error) {
- console.log(error);
- return null;
+export async function loadData(timeout = null) {
+ const req = await fetch("/config", {
+ method: "GET",
+ signal: timeout !== null ? AbortSignal.timeout(timeout) : undefined,
+ });
+ if (!req.ok) {
+ throw new Error(`Response status: ${req.status}`);
}
+
+ const json = await req.json();
+ console.log(json);
+ return json;
}
-function writeDataToInput(data) {
- console.log("write data", typeof data);
+export function writeDataToInput(data) {
+ console.log("write data");
for (const [key, value] of Object.entries(data)) {
const element = document.querySelector(`[name=${key}]`);
- console.log(element);
- element.value = value;
+ console.log(key, element);
+
+ if (element.type === "checkbox") {
+ element.checked = value;
+ } else {
+ element.value = value;
+ }
}
// send "change" event
form.dispatchEvent(new Event("change", { bubbles: true }));
}
-const data = await loadData();
-if (data !== null) {
+showLoadingScreen("Konfiguration wird geladen...");
+try {
+ const data = await loadData();
+ hideLoadingScreen();
writeDataToInput(data);
+} catch (error) {
+ console.log(error.message);
+ showError("Die Konfiguration konnte nicht geladen werden.");
}
diff --git a/data/loading-screen.js b/data/loading-screen.js
new file mode 100644
index 0000000..4b04769
--- /dev/null
+++ b/data/loading-screen.js
@@ -0,0 +1,40 @@
+const form = document.querySelector("form");
+const loadingScreen = document.querySelector(".loading-screen");
+const loadingMsg = loadingScreen.querySelector("h2");
+const spinner = loadingScreen.querySelector(".spinner");
+const reloadBtn = loadingScreen.querySelector(".reload");
+
+export function showLoadingScreen(msg) {
+ hide(form, reloadBtn);
+ show(loadingScreen, spinner);
+ loadingMsg.classList.remove("error");
+ loadingMsg.textContent = msg;
+}
+
+export function showError(msg) {
+ showLoadingScreen(msg);
+ loadingMsg.innerHTML +=
+ "
Stelle sicher, dass du mit dem DMX-Interface verbunden bist und die IP-Adresse stimmt.";
+ show(reloadBtn);
+ hide(spinner);
+ loadingMsg.classList.add("error");
+}
+
+export function hideLoadingScreen() {
+ hide(loadingScreen, reloadBtn);
+ show(form);
+ loadingMsg.classList.remove("error");
+ loadingMsg.textContent = "";
+}
+
+function show(...elements) {
+ for (const element of elements) {
+ element.classList.remove("hidden");
+ }
+}
+
+function hide(...elements) {
+ for (const element of elements) {
+ element.classList.add("hidden");
+ }
+}
diff --git a/data/reset.js b/data/reset.js
new file mode 100644
index 0000000..c34c41f
--- /dev/null
+++ b/data/reset.js
@@ -0,0 +1,16 @@
+import { updateConfig } from "/submit.js";
+
+const form = document.querySelector("form");
+
+form.addEventListener("reset", async (event) => {
+ event.preventDefault();
+
+ const ok = confirm(
+ "Sicher, dass du alle Einstellungen zurücksetzen möchtest?"
+ );
+ if (ok) {
+ updateConfig({
+ method: "DELETE",
+ });
+ }
+});
diff --git a/data/style.css b/data/style.css
index b0676df..7cce882 100644
--- a/data/style.css
+++ b/data/style.css
@@ -1,6 +1,8 @@
:root {
--color-primary: #087e8b;
--color-on-primary: white;
+ --color-background: #222;
+ --color-danger: #fa2b58;
}
body {
@@ -13,7 +15,7 @@ body {
}
main {
- background-color: #222;
+ background-color: var(--color-background);
max-width: 700px;
padding: 8px max(5%, 8px);
margin: 0 auto;
@@ -44,7 +46,7 @@ label {
input,
select {
width: clamp(200px, 100%, 400px);
- background-color: #222;
+ background-color: var(--color-background);
color: white;
border: 1px solid white;
border-radius: 8px;
@@ -59,14 +61,16 @@ select:focus {
}
button {
- display: block;
border: none;
inset: none;
border-radius: 8px;
background-color: var(--color-primary);
color: var(--color-on-primary);
padding: 8px 16px;
- margin: 0 auto;
+}
+
+button[type="reset"] {
+ background-color: var(--color-danger);
}
:is(div:has(:is(input, select)), input, select, label)
@@ -75,7 +79,7 @@ button {
}
.hidden {
- display: none;
+ display: none !important;
}
label.switch {
@@ -126,3 +130,68 @@ label.switch input:checked + .slider::before {
left: 100%;
translate: -100% -50%;
}
+
+.buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ gap: 8px;
+}
+
+.loading-screen {
+ display: grid;
+ justify-content: center;
+}
+
+h2.error {
+ color: var(--color-danger);
+}
+
+button.reload {
+ display: block;
+ margin: 0 auto;
+}
+
+.spinner-container {
+ width: min(max-content, 100%);
+}
+
+.spinner {
+ position: relative;
+ margin: 10px auto;
+ background: conic-gradient(transparent 150deg, var(--color-primary));
+ --outer-diameter: 50px;
+ width: var(--outer-diameter);
+ height: var(--outer-diameter);
+ border-radius: 50%;
+
+ animation-name: spin;
+ animation-duration: 1s;
+ animation-iteration-count: infinite;
+ animation-timing-function: linear;
+}
+
+.spinner::after {
+ position: absolute;
+ content: "";
+ display: block;
+ --spinner-border: 5px;
+ top: var(--spinner-border);
+ left: var(--spinner-border);
+
+ --inner-diameter: calc(var(--outer-diameter) - 2 * var(--spinner-border));
+ width: var(--inner-diameter);
+ height: var(--inner-diameter);
+
+ background-color: var(--color-background);
+ border-radius: 50%;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
diff --git a/data/submit.js b/data/submit.js
index 6a24318..0fdbda8 100644
--- a/data/submit.js
+++ b/data/submit.js
@@ -1,3 +1,10 @@
+import { loadData, writeDataToInput } from "./load-data.js";
+import {
+ showLoadingScreen,
+ hideLoadingScreen,
+ showError,
+} from "./loading-screen.js";
+
const form = document.querySelector("form");
function parseValue(input) {
@@ -13,7 +20,7 @@ function parseValue(input) {
if (input.type === "number") {
const number = Number(input.value);
- return isNaN(number) ? null : number;
+ return Number.isNaN(number) ? null : number;
}
return input.value;
@@ -31,25 +38,35 @@ form.addEventListener("submit", (event) => {
}, {});
console.log(data);
- putData(data);
+ updateConfig({
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ });
});
-async function putData(data) {
+export async function updateConfig(fetchOptions) {
+ showLoadingScreen("Konfiguration anwenden und ESP neustarten...");
+
try {
- const res = await fetch("/config", {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- });
+ const res = await fetch("/config", fetchOptions);
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
- const json = await res.json();
- console.log(json);
+ // wait for the esp to restart
+ const delay = new Promise((resolve) =>
+ setTimeout(() => resolve(), 500)
+ );
+ await delay;
+
+ const data = await loadData(30 * 1000);
+ writeDataToInput(data);
+ hideLoadingScreen();
} catch (error) {
console.error(error.message);
+ showError(error.message);
}
}
diff --git a/platformio.ini b/platformio.ini
index eec3bd7..a372945 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -15,4 +15,4 @@ framework = arduino
lib_deps =
hideakitai/ArtNet @ ^0.8.0
bblanchon/ArduinoJson @ ^7.2.0
- ESP Async WebServer
+ me-no-dev/ESP Async WebServer
diff --git a/src/ESPDMX.cpp b/src/ESPDMX.cpp
index efdc7a2..5408807 100644
--- a/src/ESPDMX.cpp
+++ b/src/ESPDMX.cpp
@@ -1,94 +1,71 @@
-// - - - - -
-// ESPDMX - A Arduino library for sending and receiving DMX using the builtin serial hardware port.
-// ESPDMX.cpp: Library implementation file
-//
-// Copyright (C) 2015 Rick
-// This work is licensed under a GNU style license.
-//
-// Last change: Marcel Seerig
-//
-// Documentation and samples are available at https://github.com/Rickgg/ESP-Dmx
-// - - - - -
-
/* ----- LIBRARIES ----- */
#include
#include "ESPDMX.h"
-#define DMXSPEED 250000
-#define DMXFORMAT SERIAL_8N2
-#define BREAKSPEED 83333
-#define BREAKFORMAT SERIAL_8N1
-#define SERIALPORT Serial0
-#define DMXCHANNELS 512
-
-bool dmxStarted = false;
-int sendPin = 18;
-int receivePin = -1;
-
// DMX value array and size. Entry 0 will hold startbyte, so we need 512+1 elements
-uint8_t dmxDataStore[DMXCHANNELS + 1] = {};
+// std::vector dmxDataStores(MAX_IDS);
+// uint8_t dmxDataStores[MAX_IDS][DMXCHANNELS + 1];
// Set up the DMX-Protocol
-void DMXESPSerial::init()
+void DMXESPSerial::init(int pinSend = 19, int pinRecv = -1)
{
- SERIALPORT.begin(DMXSPEED, DMXFORMAT, receivePin, sendPin);
+ sendPin = pinSend;
+ recvPin = pinRecv;
+ SERIALPORT.begin(DMXSPEED, DMXFORMAT, recvPin, sendPin);
pinMode(sendPin, OUTPUT);
dmxStarted = true;
}
// Function to read DMX data
-uint8_t DMXESPSerial::read(int Channel)
+uint8_t DMXESPSerial::read(int channel)
{
if (dmxStarted == false)
init();
- if (Channel < 1)
- Channel = 1;
- if (Channel > DMXCHANNELS)
- Channel = DMXCHANNELS;
- return (dmxDataStore[Channel]);
+ if (channel < 1)
+ channel = 1;
+ if (channel > DMXCHANNELS)
+ channel = DMXCHANNELS;
+ return (dmxDataStore[channel]);
}
// Function to send DMX data
-void DMXESPSerial::write(int Channel, uint8_t value)
+void DMXESPSerial::write(int channel, uint8_t value)
{
+
if (dmxStarted == false)
init();
- if (Channel < 1)
- Channel = 1;
- if (Channel > DMXCHANNELS)
- Channel = DMXCHANNELS;
+ if (channel < 1)
+ channel = 1;
+ if (channel > DMXCHANNELS)
+ channel = DMXCHANNELS;
if (value < 0)
value = 0;
if (value > 255)
value = 255;
- dmxDataStore[Channel] = value;
+ dmxDataStore[channel] = value;
}
void DMXESPSerial::end()
{
SERIALPORT.end();
- dmxStarted = false;
}
// Function to update the DMX bus
void DMXESPSerial::update()
{
- if (dmxStarted == false)
- init();
-
// Send break
digitalWrite(sendPin, HIGH);
- SERIALPORT.begin(BREAKSPEED, BREAKFORMAT, receivePin, sendPin);
+ SERIALPORT.begin(BREAKSPEED, BREAKFORMAT, recvPin, sendPin);
SERIALPORT.write(0);
SERIALPORT.flush();
delay(1);
SERIALPORT.end();
// send data
- SERIALPORT.begin(DMXSPEED, DMXFORMAT, receivePin, sendPin);
+ SERIALPORT.begin(DMXSPEED, DMXFORMAT, recvPin, sendPin);
digitalWrite(sendPin, LOW);
SERIALPORT.write(dmxDataStore, DMXCHANNELS);
SERIALPORT.flush();
diff --git a/src/ESPDMX.h b/src/ESPDMX.h
index 8cdc522..04123ae 100644
--- a/src/ESPDMX.h
+++ b/src/ESPDMX.h
@@ -1,29 +1,28 @@
-// - - - - -
-// ESPDMX - A Arduino library for sending and receiving DMX using the builtin serial hardware port.
-// ESPDMX.cpp: Library implementation file
-//
-// Copyright (C) 2015 Rick
-// This work is licensed under a GNU style license.
-//
-// Last change: Marcel Seerig
-//
-// Documentation and samples are available at https://github.com/Rickgg/ESP-Dmx
-// - - - - -
-
#include
-
#ifndef ESPDMX_h
#define ESPDMX_h
-// ---- Methods ----
+#define DMXSPEED 250000
+#define DMXFORMAT SERIAL_8N2
+#define BREAKSPEED 83333
+#define BREAKFORMAT SERIAL_8N1
+#define SERIALPORT Serial0
+#define DMXCHANNELS 512
-class DMXESPSerial {
- public:
- void init();
- uint8_t read(int Channel);
- void write(int channel, uint8_t value);void update();
- void end();
+class DMXESPSerial
+{
+public:
+ int sendPin;
+ int recvPin;
+ bool dmxStarted;
+ uint8_t dmxDataStore[DMXCHANNELS + 1];
+
+ void init(int pinSend, int pinRecv);
+ uint8_t read(int Channel);
+ void write(int channel, uint8_t value);
+ void update();
+ void end();
};
#endif
diff --git a/src/main.cpp b/src/main.cpp
index 39c9646..c48587c 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,18 +1,18 @@
#include
// #include
-
-#include
#include "ESPDMX.h"
#include
#include
-#include
+#include "routes/config.h"
-Preferences config;
-DMXESPSerial dmx;
+DMXESPSerial dmx1;
+DMXESPSerial dmx2;
AsyncWebServer server(80);
ArtnetWiFi artnet;
+DMXESPSerial dmx;
+
const uint16_t size = 512;
uint8_t data[size];
@@ -20,20 +20,27 @@ void setup()
{
Serial.begin(9600);
- config.begin("dmx", false);
+ config.begin("dmx", true);
- uint8_t universe = config.getUChar("universe", 1);
+ uint8_t universe1 = config.getUChar("universe-1", 1);
+ uint8_t universe2 = config.getUChar("universe-2", 1);
+
+ Direction direction1 = static_cast(config.getUInt("direction-1", 0));
+ Direction direction2 = static_cast(config.getUInt("direction-2", 1));
+
+ Connection connection = static_cast(config.getUInt("connection", WiFiSta));
+ IpMethod ipMethod = static_cast(config.getUInt("ip-method"), Static);
String ssid = config.getString("ssid", "artnet");
- String pwd = config.getString("pwd", "mbgmbgmbg");
+ String pwd = config.getString("password", "mbgmbgmbg");
IPAddress defaultIp(192, 168, 1, 201);
IPAddress ip = config.getUInt("ip", defaultIp);
+ IPAddress defaultSubnet(255, 255, 255, 0);
+ IPAddress subnet = config.getUInt("subnet", defaultSubnet);
+ IPAddress defaultGateway(192, 168, 1, 1);
+ IPAddress gateway = config.getUInt("gateway", defaultGateway);
- IPAddress cidr = config.getUChar("cidr", 24);
-
- // TODO: \/ Herleiten \/ @psxde
- const IPAddress gateway(192, 168, 1, 1);
- const IPAddress subnet(255, 255, 255, 0);
+ config.end();
// WiFi stuff
// WiFi.begin(ssid, pwd);
@@ -51,17 +58,17 @@ void setup()
artnet.begin();
// Initialize DMX ports
- dmx.init();
+ dmx1.init(19, -1);
// if Artnet packet comes to this universe, this function is called
- artnet.subscribeArtDmxUniverse(universe, [&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote)
+ artnet.subscribeArtDmxUniverse(universe1, [&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote)
{
for (size_t i = 0; i < size; ++i)
{
- dmx.write((i + 1), data[i]);
+ dmx1.write((i + 1), data[i]);
}
- dmx.update(); });
+ dmx1.update(); });
// if Artnet packet comes, this function is called to every universe
artnet.subscribeArtDmx([&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote) {});
@@ -74,19 +81,26 @@ void setup()
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
- server.on("/config", HTTP_GET, [&, defaultIp, ssid, pwd, universe](AsyncWebServerRequest *request)
+ server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request)
+ { onGetConfig(request); });
+
+ server.on("/config", HTTP_DELETE, [](AsyncWebServerRequest *request)
{
- DynamicJsonDocument doc(1024);
+ config.begin("dmx", false);
+ config.clear();
+ config.end();
+ // respond with default config
+ onGetConfig(request);
- doc["ssid"] = ssid;
- doc["pwd"] = pwd;
- doc["ip"] = defaultIp;
- doc["universe"] = universe;
+ ESP.restart(); });
- String jsonString;
- serializeJson(doc, jsonString);
+ server.onRequestBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
+ {
+ if (request->url() == "/config" && request->method() == HTTP_PUT) {
+ onPutConfig(request, data, len, index, total);
+ ESP.restart();
+ } });
- request->send(200, "application/json", jsonString); });
delay(1000);
server.begin();
Serial.println("Server started!");
@@ -95,4 +109,4 @@ void setup()
void loop()
{
artnet.parse(); // check if artnet packet has come and execute callback
-}
\ No newline at end of file
+}
diff --git a/src/routes/config.cpp b/src/routes/config.cpp
new file mode 100644
index 0000000..fe68a5a
--- /dev/null
+++ b/src/routes/config.cpp
@@ -0,0 +1,163 @@
+#include "config.h"
+#include
+#include
+
+Preferences config;
+
+#pragma region Utility
+
+uint32_t parseIp(String str)
+{
+ const int size = 4;
+
+ String ipStrings[size];
+ uint8_t ipIndex = 0;
+
+ for (int i = 0; i < str.length(); i++)
+ {
+ if (str[i] == '.')
+ {
+ ipIndex++;
+ continue;
+ }
+ ipStrings[ipIndex] += str[i];
+ }
+
+ String ip = "";
+ for (int i = 0; i < size; i++)
+ {
+ String paddedString = ipStrings[i];
+ while (paddedString.length() < 3)
+ {
+ paddedString = "0" + paddedString;
+ }
+ ip.concat(paddedString);
+ }
+
+ Serial.println("ip string: " + ip);
+ return atoi(ip.c_str());
+}
+
+IpMethod parseIpMethod(uint8_t ipMethod)
+{
+ if (ipMethod > 0 || ipMethod < IP_METHOD_SIZE)
+ {
+ return static_cast(ipMethod);
+ }
+
+ throw ::std::invalid_argument("Invalid IP method value" + ipMethod);
+}
+
+Connection parseConnection(uint8_t connection)
+{
+ if (connection > 0 || connection < CONNECTION_SIZE)
+ {
+ return static_cast(connection);
+ }
+
+ throw ::std::invalid_argument("Invalid connection value: " + connection);
+}
+
+Direction parseDirection(uint8_t direction)
+{
+ if (direction > 0 || direction < DIRECTION_SIZE)
+ {
+ return static_cast(direction);
+ }
+
+ throw ::std::invalid_argument("Invalid direction value: " + direction);
+}
+
+#pragma endregion
+
+void onGetConfig(AsyncWebServerRequest *request)
+{
+ config.begin("dmx", true);
+
+ IPAddress defaultIp(192, 168, 1, 201);
+ IPAddress ip = config.getUInt("ip", defaultIp);
+
+ IPAddress defaultSubnet(255, 255, 255, 0);
+ IPAddress subnet = config.getUInt("subnet", defaultSubnet);
+
+ IPAddress defaultGateway(192, 168, 1, 1);
+ IPAddress gateway = config.getUInt("gateway", defaultGateway);
+
+ JsonDocument doc;
+ doc["connection"] = config.getUInt("connection", WiFiSta);
+ doc["ssid"] = config.getString("ssid", "artnet");
+ doc["password"] = config.getString("password", "mbgmbgmbg");
+ doc["ip-method"] = config.getUInt("ip-method"), Static;
+ doc["ip"] = ip.toString();
+ doc["subnet"] = subnet.toString();
+ doc["gateway"] = gateway.toString();
+ doc["universe-1"] = config.getUInt("universe-1", 1);
+ doc["direction-1"] = config.getUInt("direction-1", Output);
+ doc["universe-2"] = config.getUInt("universe-2", 1);
+ doc["direction-2"] = config.getUInt("direction-2", Input);
+
+ config.end();
+
+ String jsonString;
+ serializeJson(doc, jsonString);
+
+ request->send(200, "application/json", jsonString);
+}
+
+void onPutConfig(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)
+{
+ Serial.printf("[REQUEST]\t%s\r\n", (const char *)data);
+
+ JsonDocument doc;
+ deserializeJson(doc, data);
+
+ try
+ {
+ config.begin("dmx", false);
+
+ IpMethod ipMethod = parseIpMethod(doc["ip-method"].as());
+ config.putUInt("ip-method", ipMethod);
+
+ if (ipMethod == Static)
+ {
+ IPAddress ipAddress;
+ ipAddress.fromString(doc["ip"].as());
+ config.putUInt("ip", ipAddress);
+
+ IPAddress subnet;
+ subnet.fromString(doc["subnet"].as());
+ config.putUInt("subnet", subnet);
+
+ IPAddress gateway;
+ gateway.fromString(doc["gateway"].as());
+ config.putUInt("gateway", gateway);
+ }
+
+ Connection connection = parseConnection(doc["connection"].as());
+ config.putUInt("connection", connection);
+
+ if (connection == WiFiSta || connection == WiFiAP)
+ {
+ config.putString("ssid", doc["ssid"].as());
+ config.putString("password", doc["password"].as());
+ }
+
+ Direction direction1 = parseDirection(doc["direction-1"].as());
+ config.putUInt("direction-1", direction1);
+
+ Direction direction2 = parseDirection(doc["direction-2"].as());
+ config.putUInt("direction-2", direction2);
+
+ config.putUInt("universe-1", doc["universe-1"]);
+ config.putUInt("universe-2", doc["universe-2"]);
+
+ config.end();
+
+ request->send(200);
+ }
+ catch (::std::invalid_argument &e)
+ {
+ config.end();
+ request->send(400, "text/plain", e.what());
+ }
+}
diff --git a/src/routes/config.h b/src/routes/config.h
new file mode 100644
index 0000000..cc325ee
--- /dev/null
+++ b/src/routes/config.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include
+#include
+#include
+
+// #ifndef CONFIG_h
+// #define CONFIG_h
+
+extern Preferences config;
+
+enum IpMethod
+{
+ Static,
+ DHCP
+};
+const uint8_t IP_METHOD_SIZE = 2;
+
+enum Connection
+{
+ WiFiSta,
+ WiFiAP,
+ Ethernet
+};
+const uint8_t CONNECTION_SIZE = 3;
+
+enum Direction
+{
+ Output,
+ Input
+};
+const uint8_t DIRECTION_SIZE = 2;
+
+void onGetConfig(AsyncWebServerRequest *request);
+
+void onPutConfig(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total);
+
+// #endif
\ No newline at end of file