Merge pull request #4 from HendrikRauh/DMX-3-configuration-WebUI

Dmx 3 configuration web UI
This commit is contained in:
Raffael Wolf 2024-10-31 21:33:38 +01:00 committed by GitHub
commit 3df5f2e020
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 441 additions and 61 deletions

144
data/index.html Normal file
View file

@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Konfiguration</title>
<link rel="stylesheet" href="/style.css" />
<script type="module" src="/input-visibility.js" defer></script>
<script type="module" src="/load-data.js" defer></script>
<script type="module" src="/submit.js" defer></script>
</head>
<body>
<main>
<h1>Konfiguration</h1>
<form>
<fieldset>
<legend>Verbindung</legend>
<label for="ip-method"
>IP-Zuweisung:
<select
name="ip-method"
id="input-ip-method"
title="IP-"
>
<option value="static">Statisch</option>
<option value="dhcp">DHCP</option>
</select>
</label>
<div data-field="input-ip-method" data-values="static">
<label>
IP-Adresse/CIDR:
<input
type="text"
name="ip"
id="input-ip"
placeholder="IP-Adresse/CIDR"
/>
</label>
</div>
<label>
Verbindungsmethode:
<select
name="connection"
id="input-connection"
title="Verbindung"
>
<option value="wifi-sta">WiFi-Station</option>
<option value="wifi-ap">WiFi-AccessPoint</option>
<option value="ethernet">Ethernet</option>
</select>
</label>
<div data-field="input-connection" data-values="wifi-sta">
<label>
SSID:
<input
type="text"
name="ssid"
id="input-ssid"
placeholder="SSID"
/>
</label>
</div>
<div data-field="input-connection" data-values="wifi-ap">
<label>
Netzwerk:
<select
name="network"
id="input-network"
title="Netzwerk"
></select>
</label>
</div>
<div
data-field="input-connection"
data-values="wifi-sta|wifi-ap"
>
<label>
Password:
<input
type="password"
name="password"
id="input-password"
placeholder="Passwort"
/>
</label>
</div>
</fieldset>
<fieldset>
<legend>Input/Output 1</legend>
<label class="switch">
<span>Output</span>
<input
type="checkbox"
name="input-or-output-1"
id="input-input-or-output-1"
data-value-not-checked="output"
data-value-checked="input"
/>
<span class="slider"></span>
<span>Input</span>
</label>
<label>
ArtNet-Universe:
<input
type="number"
name="universe-1"
id="universe-1"
placeholder="Universe"
min="0"
max="15"
/>
</label>
</fieldset>
<fieldset>
<legend>Input/Output 2</legend>
<label class="switch">
<span>Output</span>
<input
type="checkbox"
name="input-or-output-2"
id="input-input-or-output-2"
data-value-not-checked="output"
data-value-checked="input"
/>
<span class="slider"></span>
<span>Input</span>
</label>
<label>
ArtNet-Universe:
<input
type="number"
name="universe-2"
id="universe-2"
placeholder="Universe"
min="0"
max="15"
/>
</label>
</fieldset>
<button type="submit">Speichern</button>
</form>
</main>
</body>
</html>

23
data/input-visibility.js Normal file
View file

@ -0,0 +1,23 @@
const form = document.querySelector("form");
const dynamicInputs = form.querySelectorAll("[data-field][data-values]");
document.addEventListener("change", updateVisibility);
function updateVisibility() {
dynamicInputs.forEach((element) => {
const input = form.querySelector(`#${element.dataset.field}`);
if (element.dataset.values.split("|").includes(input.value)) {
element.classList.remove("hidden");
element
.querySelectorAll("input, select, button, textarea")
.forEach((childInput) => (childInput.disabled = false));
} else {
element.classList.add("hidden");
element
.querySelectorAll("input, select, button, textarea")
.forEach((childInput) => (childInput.disabled = true));
}
});
}
updateVisibility();

35
data/load-data.js Normal file
View file

@ -0,0 +1,35 @@
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;
}
}
function writeDataToInput(data) {
console.log("write data", typeof data);
for (const [key, value] of Object.entries(data)) {
const element = document.querySelector(`[name=${key}]`);
console.log(element);
element.value = value;
}
// send "change" event
form.dispatchEvent(new Event("change", { bubbles: true }));
}
const data = await loadData();
if (data !== null) {
writeDataToInput(data);
}

128
data/style.css Normal file
View file

@ -0,0 +1,128 @@
:root {
--color-primary: #087e8b;
--color-on-primary: white;
}
body {
margin: 0;
padding: 0;
background: linear-gradient(to left, #065760, black, black, #065760);
color: white;
font-family: Arial, Helvetica, sans-serif;
overflow: hidden;
}
main {
background-color: #222;
max-width: 700px;
padding: 8px max(5%, 8px);
margin: 0 auto;
height: 100vh;
overflow: scroll;
}
h1 {
text-align: center;
}
form > * {
margin-bottom: 16px;
}
fieldset {
border-radius: 8px;
}
label {
display: block;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
input,
select {
width: clamp(200px, 100%, 400px);
background-color: #222;
color: white;
border: 1px solid white;
border-radius: 8px;
padding: 8px;
box-sizing: border-box;
}
input:focus,
select:focus {
outline: none;
border-color: var(--color-primary);
}
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;
}
:is(div:has(:is(input, select)), input, select, label)
+ :is(div:has(:is(input, select)), input, select, label) {
margin-top: 8px;
}
.hidden {
display: none;
}
label.switch {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 8px;
-webkit-user-select: none;
user-select: none;
}
label.switch input {
display: none;
}
label.switch .slider {
display: inline-block;
position: relative;
height: 1em;
width: 2em;
background-color: #444;
border-radius: 1em;
border: 4px solid #444;
}
label.switch .slider::before {
content: "";
position: absolute;
height: 100%;
aspect-ratio: 1 / 1;
border-radius: 50%;
top: 50%;
background-color: white;
transition: all 0.1s linear;
}
label.switch:active .slider::before {
transform: scale(1.3);
transform-origin: 50% 50%;
}
label.switch input:not(:checked) + .slider::before {
left: 0%;
translate: 0 -50%;
}
label.switch input:checked + .slider::before {
left: 100%;
translate: -100% -50%;
}

55
data/submit.js Normal file
View file

@ -0,0 +1,55 @@
const form = document.querySelector("form");
function parseValue(input) {
if (input.type === "checkbox") {
return input.checked
? input.dataset.valueChecked
: input.dataset.valueNotChecked;
}
if (input.value === "") {
return null;
}
if (input.type === "number") {
const number = Number(input.value);
return isNaN(number) ? null : number;
}
return input.value;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const inputFields = document.querySelectorAll(
"form :is(input, select, textarea):not(:disabled)"
);
const data = Array.from(inputFields).reduce((data, input) => {
data[input.name] = parseValue(input);
return data;
}, {});
console.log(data);
putData(data);
});
async function putData(data) {
try {
const res = await fetch("/config", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
if (!res.ok) {
throw new Error(`Response status: ${res.status}`);
}
const json = await res.json();
console.log(json);
} catch (error) {
console.error(error.message);
}
}

View file

@ -14,4 +14,5 @@ board = lolin_s2_mini
framework = arduino framework = arduino
lib_deps = lib_deps =
hideakitai/ArtNet @ ^0.8.0 hideakitai/ArtNet @ ^0.8.0
someweisguy/esp_dmx @ ^4.1.0 bblanchon/ArduinoJson @ ^7.2.0
ESP Async WebServer

View file

@ -1,34 +1,39 @@
// Art-Net DMX Interface Demo
// 2024-10-17 Patrick Schwarz
#include <ArtnetWiFi.h> #include <ArtnetWiFi.h>
// #include <ArtnetEther.h> // #include <ArtnetEther.h>
#include <ArduinoJson.h>
#include "ESPDMX.h" #include "ESPDMX.h"
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <Preferences.h>
// WiFi stuff Preferences config;
const char *ssid = "artnet"; DMXESPSerial dmx;
const char *pwd = "mbgmbgmbg";
const IPAddress ip(192, 168, 1, 201); AsyncWebServer server(80);
const IPAddress gateway(192, 168, 1, 1);
const IPAddress subnet(255, 255, 255, 0);
// Art-Net stuff
ArtnetWiFi artnet; ArtnetWiFi artnet;
// const String target_ip = "192.168.1.200";
uint8_t universe = 1; // 0 - 15
const uint16_t size = 512; const uint16_t size = 512;
uint8_t data[size]; uint8_t data[size];
uint8_t value = 0;
// DMX stuff
DMXESPSerial dmx;
void setup() void setup()
{ {
Serial.begin(9600);
// Serial console config.begin("dmx", false);
// Serial.begin(115200);
uint8_t universe = config.getUChar("universe", 1);
String ssid = config.getString("ssid", "artnet");
String pwd = config.getString("pwd", "mbgmbgmbg");
IPAddress defaultIp(192, 168, 1, 201);
IPAddress ip = config.getUInt("ip", defaultIp);
IPAddress cidr = config.getUChar("cidr", 24);
// TODO: \/ Herleiten \/ @psxde
const IPAddress gateway(192, 168, 1, 1);
const IPAddress subnet(255, 255, 255, 0);
// WiFi stuff // WiFi stuff
// WiFi.begin(ssid, pwd); // WiFi.begin(ssid, pwd);
@ -51,54 +56,43 @@ void setup()
// if Artnet packet comes to this universe, this function is called // 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(universe, [&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote)
{ {
/*Serial.print("lambda : artnet data from "); for (size_t i = 0; i < size; ++i)
Serial.print(remote.ip); {
Serial.print(":"); dmx.write((i + 1), data[i]);
Serial.print(remote.port); }
Serial.print(", universe = ");
Serial.print(universe);
Serial.print(", size = ");
Serial.print(size);
Serial.print(") :");*/
for (size_t i = 0; i < size; ++i) dmx.update(); });
{
dmx.write((i + 1), data[i]);
// Serial.print(data[i]);
// Serial.print(",");
}
// Serial.println();
dmx.update(); });
// if Artnet packet comes, this function is called to every universe // 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) artnet.subscribeArtDmx([&](const uint8_t *data, uint16_t size, const ArtDmxMetadata &metadata, const ArtNetRemoteInfo &remote) {});
{
/*Serial.print("received ArtNet data from "); if (!SPIFFS.begin(true))
Serial.print(remote.ip); {
Serial.print(":"); Serial.println("An Error has occurred while mounting SPIFFS");
Serial.print(remote.port); return;
Serial.print(", net = "); }
Serial.print(metadata.net);
Serial.print(", subnet = "); server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
Serial.print(metadata.subnet);
Serial.print(", universe = "); server.on("/config", HTTP_GET, [&, defaultIp, ssid, pwd, universe](AsyncWebServerRequest *request)
Serial.print(metadata.universe); {
Serial.print(", sequence = "); DynamicJsonDocument doc(1024);
Serial.print(metadata.sequence);
Serial.print(", size = "); doc["ssid"] = ssid;
Serial.print(size); doc["pwd"] = pwd;
Serial.println(")");*/ }); doc["ip"] = defaultIp;
doc["universe"] = universe;
String jsonString;
serializeJson(doc, jsonString);
request->send(200, "application/json", jsonString); });
delay(1000);
server.begin();
Serial.println("Server started!");
} }
void loop() void loop()
{ {
artnet.parse(); // check if artnet packet has come and execute callback artnet.parse(); // check if artnet packet has come and execute callback
/*value = (millis() / 4) % 256;
memset(data, value, size);
artnet.setArtDmxData(data, size);
artnet.streamArtDmxTo(target_ip, universe); // automatically send set data in 40fps
// artnet.streamArtDmxTo(target_ip, net, subnet, univ); // or you can set net, subnet, and universe */
} }