mirror of
https://github.com/HendrikRauh/dmx-interface.git
synced 2025-05-19 10:32:56 +00:00
Merge pull request #4 from HendrikRauh/DMX-3-configuration-WebUI
Dmx 3 configuration web UI
This commit is contained in:
commit
3df5f2e020
7 changed files with 441 additions and 61 deletions
144
data/index.html
Normal file
144
data/index.html
Normal 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
23
data/input-visibility.js
Normal 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
35
data/load-data.js
Normal 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
128
data/style.css
Normal 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
55
data/submit.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
102
src/main.cpp
102
src/main.cpp
|
@ -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 ");
|
|
||||||
Serial.print(remote.ip);
|
|
||||||
Serial.print(":");
|
|
||||||
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)
|
for (size_t i = 0; i < size; ++i)
|
||||||
{
|
{
|
||||||
dmx.write((i + 1), data[i]);
|
dmx.write((i + 1), data[i]);
|
||||||
// Serial.print(data[i]);
|
|
||||||
// Serial.print(",");
|
|
||||||
}
|
}
|
||||||
// Serial.println();
|
|
||||||
|
|
||||||
dmx.update(); });
|
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) {});
|
||||||
|
|
||||||
|
if (!SPIFFS.begin(true))
|
||||||
{
|
{
|
||||||
/*Serial.print("received ArtNet data from ");
|
Serial.println("An Error has occurred while mounting SPIFFS");
|
||||||
Serial.print(remote.ip);
|
return;
|
||||||
Serial.print(":");
|
}
|
||||||
Serial.print(remote.port);
|
|
||||||
Serial.print(", net = ");
|
server.serveStatic("/", SPIFFS, "/").setDefaultFile("index.html");
|
||||||
Serial.print(metadata.net);
|
|
||||||
Serial.print(", subnet = ");
|
server.on("/config", HTTP_GET, [&, defaultIp, ssid, pwd, universe](AsyncWebServerRequest *request)
|
||||||
Serial.print(metadata.subnet);
|
{
|
||||||
Serial.print(", universe = ");
|
DynamicJsonDocument doc(1024);
|
||||||
Serial.print(metadata.universe);
|
|
||||||
Serial.print(", sequence = ");
|
doc["ssid"] = ssid;
|
||||||
Serial.print(metadata.sequence);
|
doc["pwd"] = pwd;
|
||||||
Serial.print(", size = ");
|
doc["ip"] = defaultIp;
|
||||||
Serial.print(size);
|
doc["universe"] = universe;
|
||||||
Serial.println(")");*/ });
|
|
||||||
|
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 */
|
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue