2624 lines
118 KiB
HTML
2624 lines
118 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>CharGraph Konfiguration</title>
|
||
<style>
|
||
:root {
|
||
--bg-gradient-start: #667eea;
|
||
--bg-gradient-end: #764ba2;
|
||
--container-bg: white;
|
||
--text-primary: #333;
|
||
--text-secondary: #555;
|
||
--text-tertiary: #666;
|
||
--border-color: #ddd;
|
||
--hover-bg: #f5f5f5;
|
||
--info-bg: #e8eaf6;
|
||
--accent-color: #667eea;
|
||
--input-bg: white;
|
||
--shadow: rgba(0,0,0,0.3);
|
||
}
|
||
|
||
body.dark-mode {
|
||
--bg-gradient-start: #1a1a2e;
|
||
--bg-gradient-end: #16213e;
|
||
--container-bg: #0f3460;
|
||
--text-primary: #e0e0e0;
|
||
--text-secondary: #b0b0b0;
|
||
--text-tertiary: #999;
|
||
--border-color: #444;
|
||
--hover-bg: #1a4d7a;
|
||
--info-bg: #1a3a5a;
|
||
--accent-color: #667eea;
|
||
--input-bg: #1a1a2e;
|
||
--shadow: rgba(0,0,0,0.6);
|
||
}
|
||
|
||
body {
|
||
font-family: Arial, sans-serif;
|
||
max-width: 500px;
|
||
margin: 50px auto;
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||
transition: background 0.3s ease;
|
||
}
|
||
.container {
|
||
background: var(--container-bg);
|
||
padding: 30px;
|
||
border-radius: 15px;
|
||
box-shadow: 0 8px 20px var(--shadow);
|
||
position: relative;
|
||
transition: background 0.3s ease;
|
||
}
|
||
h1 {
|
||
color: var(--text-primary);
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
transition: color 0.3s ease;
|
||
}
|
||
h2 {
|
||
color: var(--text-secondary);
|
||
font-size: 18px;
|
||
margin-top: 25px;
|
||
margin-bottom: 15px;
|
||
border-bottom: 2px solid var(--accent-color);
|
||
padding-bottom: 5px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
user-select: none;
|
||
transition: color 0.3s ease;
|
||
}
|
||
h2:hover {
|
||
background: var(--hover-bg);
|
||
padding-left: 10px;
|
||
padding-right: 10px;
|
||
margin-left: -10px;
|
||
margin-right: -10px;
|
||
border-radius: 8px;
|
||
}
|
||
.theme-toggle {
|
||
position: absolute;
|
||
top: 15px;
|
||
right: 15px;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.3s ease;
|
||
padding: 0;
|
||
}
|
||
.theme-toggle:hover {
|
||
transform: scale(1.15) rotate(20deg);
|
||
}
|
||
.theme-toggle svg {
|
||
width: 40px;
|
||
height: 40px;
|
||
fill: var(--text-primary);
|
||
stroke: var(--text-primary);
|
||
stroke-width: 2;
|
||
stroke-linecap: round;
|
||
stroke-linejoin: round;
|
||
transition: fill 0.3s ease, stroke 0.3s ease;
|
||
}
|
||
.section-toggle {
|
||
font-size: 20px;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
.section-toggle.collapsed {
|
||
transform: rotate(-90deg);
|
||
}
|
||
.section-content {
|
||
max-height: 10000px;
|
||
overflow: hidden;
|
||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||
opacity: 1;
|
||
}
|
||
.section-content.collapsed {
|
||
max-height: 0;
|
||
opacity: 0;
|
||
}
|
||
.form-group {
|
||
margin-bottom: 25px;
|
||
}
|
||
label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: bold;
|
||
color: var(--text-secondary);
|
||
transition: color 0.3s ease;
|
||
}
|
||
input[type="color"] {
|
||
width: 100%;
|
||
height: 60px;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
}
|
||
input[type="range"] {
|
||
width: 100%;
|
||
height: 8px;
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
background: var(--border-color);
|
||
border-radius: 5px;
|
||
outline: none;
|
||
}
|
||
input[type="range"]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 25px;
|
||
height: 25px;
|
||
background: var(--accent-color);
|
||
cursor: pointer;
|
||
border-radius: 50%;
|
||
}
|
||
input[type="datetime-local"], input[type="number"], input[type="text"], input[type="password"], select {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
box-sizing: border-box;
|
||
background: var(--input-bg);
|
||
color: var(--text-primary);
|
||
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||
}
|
||
textarea {
|
||
width: 100%;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 16px;
|
||
padding: 10px;
|
||
border: 2px solid var(--border-color);
|
||
border-radius: 8px;
|
||
resize: none;
|
||
letter-spacing: 2px;
|
||
line-height: 1.5;
|
||
background: var(--input-bg);
|
||
color: var(--text-primary);
|
||
box-sizing: border-box;
|
||
transition: background 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||
}
|
||
body.dark-mode textarea {
|
||
background: linear-gradient(0deg, transparent 24px, #444 25px),
|
||
linear-gradient(90deg, transparent 24px, #444 25px),
|
||
var(--input-bg);
|
||
background-size: 25px 25px;
|
||
}
|
||
body:not(.dark-mode) textarea {
|
||
background: linear-gradient(0deg, transparent 24px, #e0e0e0 25px),
|
||
linear-gradient(90deg, transparent 24px, #e0e0e0 25px),
|
||
white;
|
||
background-size: 25px 25px;
|
||
}
|
||
.range-value {
|
||
text-align: center;
|
||
font-size: 24px;
|
||
font-weight: bold;
|
||
color: var(--accent-color);
|
||
margin-top: 10px;
|
||
}
|
||
.time-option, .test-option {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
.test-option {
|
||
padding: 10px;
|
||
background: var(--hover-bg);
|
||
border-radius: 8px;
|
||
transition: background 0.3s ease;
|
||
}
|
||
.time-option input[type="radio"], .test-option input[type="radio"] {
|
||
margin-right: 10px;
|
||
width: 20px;
|
||
height: 20px;
|
||
}
|
||
.time-option label, .test-option label {
|
||
margin: 0;
|
||
font-weight: normal;
|
||
cursor: pointer;
|
||
}
|
||
.manual-time {
|
||
display: none;
|
||
margin-top: 15px;
|
||
padding: 15px;
|
||
background: var(--hover-bg);
|
||
border-radius: 8px;
|
||
transition: background 0.3s ease;
|
||
}
|
||
.test-modes {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
button {
|
||
width: 100%;
|
||
padding: 18px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
margin-top: 10px;
|
||
transition: transform 0.2s;
|
||
}
|
||
button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||
}
|
||
button.secondary {
|
||
width: auto;
|
||
padding: 10px 20px;
|
||
font-size: 14px;
|
||
background: #999;
|
||
}
|
||
.info {
|
||
background: var(--info-bg);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
margin-top: 25px;
|
||
border-left: 4px solid var(--accent-color);
|
||
transition: background 0.3s ease;
|
||
}
|
||
.info h3 {
|
||
margin-top: 0;
|
||
color: var(--text-primary);
|
||
transition: color 0.3s ease;
|
||
}
|
||
.time-display {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-bottom: 15px;
|
||
}
|
||
.time-box {
|
||
flex: 1;
|
||
text-align: center;
|
||
}
|
||
.time-box strong {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-size: 14px;
|
||
}
|
||
.time-value {
|
||
font-size: 22px;
|
||
color: var(--accent-color);
|
||
font-weight: bold;
|
||
}
|
||
#driftInfo {
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
margin-top: 15px;
|
||
display: none;
|
||
}
|
||
.drift-value {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
margin-bottom: 8px;
|
||
}
|
||
.drift-status {
|
||
font-size: 14px;
|
||
color: var(--text-tertiary);
|
||
transition: color 0.3s ease;
|
||
}
|
||
.drift-good {
|
||
background: #c8e6c9;
|
||
border-left: 4px solid #4caf50;
|
||
}
|
||
.drift-warning {
|
||
background: #fff9c4;
|
||
border-left: 4px solid #ffc107;
|
||
}
|
||
.drift-bad {
|
||
background: #ffcdd2;
|
||
border-left: 4px solid #f44336;
|
||
}
|
||
.details {
|
||
margin-top: 15px;
|
||
font-size: 13px;
|
||
color: var(--text-tertiary);
|
||
padding-top: 15px;
|
||
border-top: 1px solid var(--border-color);
|
||
transition: color 0.3s ease, border-color 0.3s ease;
|
||
}
|
||
.details div {
|
||
margin: 5px 0;
|
||
}
|
||
.color-preview {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 10px;
|
||
}
|
||
.color-box {
|
||
width: 48%;
|
||
height: 40px;
|
||
border-radius: 5px;
|
||
border: 2px solid var(--border-color);
|
||
transition: border-color 0.3s ease;
|
||
}
|
||
.chargraph-icon {
|
||
display: inline-block;
|
||
width: 28px;
|
||
height: 28px;
|
||
margin-right: 8px;
|
||
vertical-align: middle;
|
||
}
|
||
.chargraph-icon.small {
|
||
width: 22px;
|
||
height: 22px;
|
||
margin-right: 6px;
|
||
}
|
||
.password-toggle-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
fill: none;
|
||
stroke: #999;
|
||
stroke-width: 2;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- SVG Definitions -->
|
||
<svg style="display: none;">
|
||
<defs>
|
||
<!-- CharGraph Icon -->
|
||
<symbol id="icon-chargraph" viewBox="0 0 44 44">
|
||
<rect width="44" height="44" fill="#1a1a1a" rx="2"/>
|
||
<g fill="#fff">
|
||
<rect x="2" y="2" width="3" height="3"/>
|
||
<rect x="6" y="2" width="3" height="3"/>
|
||
<rect x="14" y="2" width="3" height="3"/>
|
||
<rect x="18" y="2" width="3" height="3"/>
|
||
<rect x="22" y="2" width="3" height="3"/>
|
||
<rect x="2" y="34" width="3" height="3"/>
|
||
<rect x="6" y="34" width="3" height="3"/>
|
||
<rect x="10" y="34" width="3" height="3"/>
|
||
<rect x="14" y="34" width="3" height="3"/>
|
||
<rect x="34" y="39" width="3" height="3"/>
|
||
<rect x="38" y="39" width="3" height="3"/>
|
||
</g>
|
||
<g fill="#222">
|
||
<rect x="2" y="6" width="3" height="3"/>
|
||
<rect x="6" y="6" width="3" height="3"/>
|
||
<rect x="10" y="6" width="3" height="3"/>
|
||
</g>
|
||
</symbol>
|
||
|
||
<!-- Sun Icon (8 rays) -->
|
||
<symbol id="icon-sun" viewBox="0 0 24 24">
|
||
<circle cx="12" cy="12" r="4"/>
|
||
<line x1="12" y1="2" x2="12" y2="4"/>
|
||
<line x1="12" y1="20" x2="12" y2="22"/>
|
||
<line x1="4" y1="12" x2="6" y2="12"/>
|
||
<line x1="18" y1="12" x2="20" y2="12"/>
|
||
<line x1="5.64" y1="5.64" x2="7.05" y2="7.05"/>
|
||
<line x1="16.95" y1="16.95" x2="18.36" y2="18.36"/>
|
||
<line x1="5.64" y1="18.36" x2="7.05" y2="16.95"/>
|
||
<line x1="16.95" y1="7.05" x2="18.36" y2="5.64"/>
|
||
</symbol>
|
||
|
||
<!-- Moon Icon -->
|
||
<symbol id="icon-moon" viewBox="0 0 24 24">
|
||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||
</symbol>
|
||
|
||
<!-- Eye Icon (visible) -->
|
||
<symbol id="icon-eye-visible" viewBox="0 0 24 24">
|
||
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
</symbol>
|
||
|
||
<!-- Eye Icon (hidden) -->
|
||
<symbol id="icon-eye-hidden" viewBox="0 0 24 24">
|
||
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<line x1="4" y1="4" x2="20" y2="20"/>
|
||
</symbol>
|
||
</defs>
|
||
</svg>
|
||
|
||
<div class="container">
|
||
<button class="theme-toggle" onclick="toggleTheme()" title="Dark Mode umschalten">
|
||
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
|
||
<use href="#icon-sun"/>
|
||
</svg>
|
||
<svg id="theme-icon-moon" viewBox="0 0 24 24">
|
||
<use href="#icon-moon"/>
|
||
</svg>
|
||
</button>
|
||
<h1><svg class="chargraph-icon"><use href="#icon-chargraph"/></svg>CharGraph Einstellungen</h1>
|
||
|
||
<div id="specialWordsDisplay" style="text-align: center; font-size: 18px; color: var(--text-secondary); margin: 10px 0 20px 0; font-weight: 500;"></div>
|
||
|
||
<h2 onclick="toggleSection('section-info')">
|
||
<span>ℹ️ Projekt-Information</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-info" class="section-content collapsed">
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; margin-bottom: 15px;">
|
||
<h3 style="margin-top: 0; color: white; border-bottom: 2px solid rgba(255,255,255,0.3); padding-bottom: 10px;">
|
||
<svg class="chargraph-icon small" style="filter: brightness(0) invert(1);"><use href="#icon-chargraph"/></svg>
|
||
CharGraph-Projekt
|
||
</h3>
|
||
<p style="margin: 10px 0; line-height: 1.6;">
|
||
<strong>Projekt der Karl Kübel Schule in Bensheim</strong><br>
|
||
<a href="https://www.karlkuebelschule.de" target="_blank" style="color: #ffd700; text-decoration: none;">🏫 www.karlkuebelschule.de</a><br>
|
||
📅 Januar 2026
|
||
</p>
|
||
</div>
|
||
<div style="background: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #ffc107;">
|
||
<h4 style="margin-top: 0; color: #d97706;">💰 Finanzierung</h4>
|
||
<p style="margin: 5px 0; line-height: 1.6;">
|
||
Finanziert durch Spenden an den<br>
|
||
<a href="https://www.karlkuebelschule.de/schulleben/foerderverein/" target="_blank"><strong>Förderverein der Karlkübelschule e.V.</strong></a>
|
||
</p>
|
||
</div>
|
||
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #667eea;">
|
||
<h4 style="margin-top: 0; color: #667eea;">👥 Projektteam</h4>
|
||
<p style="margin: 5px 0; line-height: 1.8;">
|
||
<strong>Projektidee und Durchführung:</strong><br>
|
||
• Simone Konrad<br>
|
||
• Philip Benz<br>
|
||
• Rainer Wieland
|
||
</p>
|
||
</div>
|
||
|
||
<div style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 15px; border-left: 4px solid #4caf50;">
|
||
<h4 style="margin-top: 0; color: #4caf50;">🌐 Web & Code</h4>
|
||
<p style="margin: 5px 0; line-height: 1.8;">
|
||
<strong>Webseite des Projektes:</strong> <a href="https://chargraph.karlkuebelschule.de" target="_blank" style="color: #667eea;">chargraph.karlkuebelschule.de</a><br>
|
||
<strong>Git Repository:</strong> <a href="https://gitea.karlkuebelschule.de/CharGraph" target="_blank" style="color: #667eea;">gitea.karlkuebelschule.de/CharGraph</a>
|
||
</p>
|
||
</div>
|
||
|
||
<div style="background: #e3f2fd; padding: 15px; border-radius: 8px; border-left: 4px solid #2196f3;">
|
||
<h4 style="margin-top: 0; color: #2196f3;">🎓 Projektbeteiligte</h4>
|
||
<p style="margin: 5px 0; line-height: 1.8; font-size: 14px;">
|
||
<strong>Schülerinnen und Schüler:</strong><br>
|
||
• Anna Müller<br>
|
||
• Ben Schmidt<br>
|
||
• Clara Weber<br>
|
||
• David Fischer<br>
|
||
• Emma Wagner<br>
|
||
• Felix Becker<br>
|
||
• Greta Hoffmann<br>
|
||
• Henry Koch<br>
|
||
• Ida Richter<br>
|
||
• Jonas Klein<br>
|
||
• Klara Schröder<br>
|
||
• Leon Neumann<br>
|
||
• Mia Braun<br>
|
||
• Noah Zimmermann<br>
|
||
• Olivia Krüger<br>
|
||
• Paul Hofmann<br>
|
||
• Quinn Schmitt<br>
|
||
• Rosa Lange<br>
|
||
• Simon Werner<br>
|
||
• Tara Schmitz
|
||
</p>
|
||
<p style="margin: 5px 0; line-height: 1.8; font-size: 14px;">
|
||
<strong>Betatester des Funkamateurclubs Weinhein FACW e.V.:</strong><br>
|
||
• DM4IM<br>
|
||
• DL9SAD<br>
|
||
• DL4ZAO<br>
|
||
• DL7UKM<br>
|
||
• DL2IAX<br>
|
||
• DJ8LC<br>
|
||
• DB2FQ<br>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 onclick="toggleSection('section-time')">
|
||
<span>🕒 Zeit einstellen</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-time" class="section-content collapsed">
|
||
|
||
<div class="time-option">
|
||
<input type="radio" id="ntpTime" name="timeMode" value="ntp"
|
||
onchange="toggleTimeMode()" disabled>
|
||
<label for="ntpTime" title="Die Zeit wird über NTP aus dem Internet eingestellt">
|
||
Automatisch über NTP (Internet)
|
||
<span id="ntpStatusIcon" style="margin-left: 5px;">📡</span>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="time-option">
|
||
<input type="radio" id="autoTime" name="timeMode" value="auto" checked
|
||
onchange="toggleTimeMode()">
|
||
<label for="autoTime">Automatisch von diesem Gerät</label>
|
||
</div>
|
||
|
||
<div class="time-option">
|
||
<input type="radio" id="manualTime" name="timeMode" value="manual"
|
||
onchange="toggleTimeMode()">
|
||
<label for="manualTime">Manuell eingeben</label>
|
||
</div>
|
||
|
||
<div class="manual-time" id="manualTimeInputs">
|
||
<div class="form-group">
|
||
<label>📅 Datum und Uhrzeit:</label>
|
||
<input type="datetime-local" id="manualDateTime">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info">
|
||
<h3><svg class="chargraph-icon small"><use href="#icon-chargraph"/></svg>CharGraph Status</h3>
|
||
|
||
<div class="time-display">
|
||
<div class="time-box">
|
||
<strong>Zeit</strong>
|
||
<div class="time-value" id="espTime">--:--:--</div>
|
||
</div>
|
||
<div class="time-box">
|
||
<strong>Ihre Geräte-Zeit</strong>
|
||
<div class="time-value" id="browserTime">--:--:--</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="driftInfo">
|
||
<div class="drift-value">
|
||
Abweichung: <span id="driftValue">0</span> Sekunden
|
||
</div>
|
||
<div class="drift-status" id="driftStatus"></div>
|
||
</div>
|
||
|
||
<div class="details">
|
||
<div>📅 Letzter Sync: <span id="lastSync">--</span></div>
|
||
<div id="driftRateRow">📊 Drift-Rate: <span id="driftRate">--</span></div>
|
||
<div id="syncCountRow">🔢 Anzahl Syncs: <span id="syncCount">--</span></div>
|
||
<div>⏱ Laufzeit: <span id="uptime">--</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<button onclick="saveTime()" style="background: #4caf50; margin-bottom: 10px;">🕒 Aktuelle Uhrzeit speichern</button>
|
||
|
||
</div><!-- Ende section-time -->
|
||
<!-- ════════════════════════════════════════════════════════════════ -->
|
||
<!-- HELLIGKEITSANPASSUNG -->
|
||
<!-- ════════════════════════════════════════════════════════════════ -->
|
||
<h2 onclick="toggleSection('section-autobrightness')">
|
||
📊 Helligkeitsanpassung
|
||
<span class="section-toggle">▼</span>
|
||
</h2>
|
||
<div id="section-autobrightness" class="section-content collapsed">
|
||
|
||
<div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
|
||
<strong>ℹ️ Info:</strong> Die Helligkeit wird über einen Fotowiderstand unter dem dritten Buchstaben auf dem Panel gesteuert.
|
||
Wenn es im Raum dunkler ist, werden auch die Buchstaben weniger hell leuchten.<br>Die maximale Helligkeit ist aus Sicherheitsgründen auf 80% begrenzt.<br>
|
||
Kalibrieren Sie die Min/Max-Werte entsprechend.<br>
|
||
</div>
|
||
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; border-radius: 8px;">
|
||
<strong>📝 Kalibrierung in 4 Schritten:</strong><br><br>
|
||
<strong>1. Dunkle Umgebung herstellen</strong><br>
|
||
• Licht ausschalten oder abdecken<br>
|
||
• Warte 2 Sekunden bis Messwert aktualisiert wird<br>
|
||
• Drücke <strong>🔽</strong> beim Minimum-Feld<br>
|
||
• LEDs leuchten mit minimaler Helligkeit<br><br>
|
||
|
||
<strong>2. Helle Umgebung herstellen</strong><br>
|
||
• Licht einschalten oder Taschenlampe verwenden<br>
|
||
• Warte 2 Sekunden bis Messwert aktualisiert wird<br>
|
||
• Drücke <strong>🔼</strong> beim Maximum-Feld<br>
|
||
• LEDs leuchten mit maximaler Helligkeit<br><br>
|
||
|
||
<strong>3. Feinabstimmung (optional)</strong><br>
|
||
• Schieberegler oder Zahleneingabe verwenden<br>
|
||
• LEDs zeigen sofort die Änderung (alle 2 Sekunden)<br>
|
||
• Eingabefelder werden während der Bearbeitung nicht überschrieben<br><br>
|
||
|
||
<strong>4. Speichern</strong><br>
|
||
• Aktiviere die Checkbox "Automatische Helligkeitsregelung"<br>
|
||
• Drücke <strong>📊 Helligkeitsanpassung speichern</strong><br>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>📈 Lichtintensität:</label>
|
||
<div style="font-size: 24px; font-weight: bold; padding: 15px; background: #f5f5f5; border-radius: 8px; text-align: center;">
|
||
<span id="currentADCPercent" style="color: #2196f3;">---</span>
|
||
<span style="font-size: 18px; color: #666;">%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 20px; color: #666;">🔧 Umgebungslicht-Kalibrierung</h3>
|
||
|
||
<div class="form-group">
|
||
<label>Minimum (bei wenig Licht):</label>
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="range" id="autoBrightnessMinADC" min="0" max="1023" value="200"
|
||
oninput="setUserAdjusting(); validateADCMin(); updateADCPreview(); sendLivePreview();"
|
||
style="flex: 1; height: 30px;">
|
||
<input type="number" id="autoBrightnessMinADCValue" min="0" max="100" value="20" readonly
|
||
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center; background: #f5f5f5;">
|
||
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
|
||
<button onclick="autoSetMin()" style="width: 40px; min-width: 40px; padding: 6px; margin: 0; background: none; border: none; cursor: pointer; font-size: 22px;" title="Aktuellen Wert als Minimum setzen">
|
||
🔽
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Maximum (bei viel Licht):</label>
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="range" id="autoBrightnessMaxADC" min="0" max="1023" value="820"
|
||
oninput="setUserAdjusting(); validateADCMax(); updateADCPreview(); sendLivePreview();"
|
||
style="flex: 1; height: 30px;">
|
||
<input type="number" id="autoBrightnessMaxADCValue" min="0" max="100" value="80" readonly
|
||
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center; background: #f5f5f5;">
|
||
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
|
||
<button onclick="autoSetMax()" style="width: 40px; min-width: 40px; padding: 6px; margin: 0; background: none; border: none; cursor: pointer; font-size: 22px;" title="Aktuellen Wert als Maximum setzen">
|
||
🔼
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px; color: #666;">💡 Helligkeits-Bereich</h3>
|
||
<div style="background: #e8f5e9; border-left: 4px solid #4caf50; padding: 15px; margin-bottom: 15px; border-radius: 8px; font-size: 14px;">
|
||
<strong>ℹ️ Info:</strong> Legen Sie fest, wie hell die LEDs bei wenig bzw. viel Licht leuchten sollen.<br>
|
||
• Bei Minimum ADC wird <strong>Minimale Helligkeit</strong> verwendet<br>
|
||
• Bei Maximum ADC wird <strong>Maximale Helligkeit</strong> verwendet<br>
|
||
• Werte dazwischen werden linear interpoliert
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Minimale Anzeigehelligkeit (bei wenig Licht):</label>
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="range" id="autoBrightnessMin" min="0" max="80" value="0"
|
||
oninput="document.getElementById('autoBrightnessMinValue').value = this.value; updateBrightnessDisplay(); validateBrightnessMin(); updateADCPreview();"
|
||
style="flex: 1; height: 30px;">
|
||
<input type="number" id="autoBrightnessMinValue" min="0" max="80" value="0"
|
||
oninput="document.getElementById('autoBrightnessMin').value = this.value; updateBrightnessDisplay(); validateBrightnessMin(); updateADCPreview();"
|
||
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center;">
|
||
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
|
||
<div style="width: 40px; min-width: 40px;"></div>
|
||
</div>
|
||
<small style="color: #666;">0 = Aus (0%), 80 = Maximum (80%)</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Maximale Anzeigehelligkeit (bei viel Licht):</label>
|
||
<div style="display: flex; align-items: center; gap: 10px;">
|
||
<input type="range" id="autoBrightnessMax" min="0" max="80" value="80"
|
||
oninput="document.getElementById('autoBrightnessMaxValue').value = this.value; updateBrightnessDisplay(); validateBrightnessMax(); updateADCPreview();"
|
||
style="flex: 1; height: 30px;">
|
||
<input type="number" id="autoBrightnessMaxValue" min="0" max="80" value="80"
|
||
oninput="document.getElementById('autoBrightnessMax').value = this.value; updateBrightnessDisplay(); validateBrightnessMax(); updateADCPreview();"
|
||
style="width: 70px; padding: 8px; border: 2px solid #ddd; border-radius: 5px; text-align: center;">
|
||
<span style="width: 40px; text-align: center; color: #666; font-weight: bold;">%</span>
|
||
<div style="width: 40px; min-width: 40px;"></div>
|
||
</div>
|
||
<small style="color: #666;">Empfohlen: Maximal (80%)</small>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="autoBrightnessEnabled" style="width: auto; margin-right: 10px;">
|
||
Automatische Helligkeitsregelung aktivieren
|
||
</label>
|
||
<div id="rangeWarning" style="display: none; margin-top: 10px; padding: 10px; background: #ffebee; border-left: 4px solid #f44336; border-radius: 5px; color: #c62828; font-size: 14px;">
|
||
</div>
|
||
</div>
|
||
<button onclick="saveAutoBrightness()" style="margin-top: 10px; background: #4caf50; width: 100%;">
|
||
📊 Helligkeitsanpassung speichern
|
||
</button>
|
||
|
||
</div><!-- Ende section-autobrightness -->
|
||
<h2 onclick="toggleSection('section-wifi')">
|
||
<span>📡 WiFi & Zeit-Server Konfiguration</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-wifi" class="section-content collapsed">
|
||
|
||
<div style="background: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
|
||
<strong>ℹ️ Hinweis:</strong> Nach dem Speichern ist ein Neustart erforderlich, damit die WiFi-Einstellungen aktiv werden!
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>📊 WiFi Status:</label>
|
||
<div id="wifiStatus" style="font-size: 16px; padding: 10px; background: #f5f5f5; border-radius: 5px;">
|
||
<div>Verbindung: <span id="staConnected" style="font-weight: bold;">—</span></div>
|
||
<div>IP-Adresse: <span id="staLocalIP">—</span></div>
|
||
<div>Signalstärke: <span id="staRSSI">—</span> dBm</div>
|
||
<div style="margin-top: 10px;">Letzter NTP-Sync: <span id="ntpLastSync">—</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="staEnabled" style="width: auto; margin-right: 10px;">
|
||
WiFi Station aktivieren (Verbindung zum WLAN)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>WLAN-Name (SSID):</label>
|
||
<select id="staSsid" onchange="handleSsidSelection()" style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
<option value="">-- SSID auswählen --</option>
|
||
</select>
|
||
<input type="text" id="staSsidManual" placeholder="SSID manuell eingeben..." maxlength="31"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box; margin-top: 10px; display: none;">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>WLAN-Passwort:</label>
|
||
<div style="position: relative; display: inline-block; width: 100%;">
|
||
<input type="password" id="staPassword" placeholder="********" maxlength="63"
|
||
style="width: 100%; padding: 12px 45px 12px 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
<span onclick="togglePasswordVisibility()" id="passwordToggleIcon"
|
||
style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); cursor: pointer; user-select: none;"
|
||
title="Passwort anzeigen">
|
||
<svg class="password-toggle-icon" viewBox="0 0 24 24">
|
||
<use href="#icon-eye-hidden"/>
|
||
</svg>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>
|
||
<input type="checkbox" id="staDhcp" checked style="width: auto; margin-right: 10px;">
|
||
DHCP verwenden (automatische IP-Adresse)
|
||
</label>
|
||
</div>
|
||
|
||
<div id="staticIPConfig" style="display: none; margin-left: 20px; padding: 15px; background: #f9f9f9; border-radius: 8px;">
|
||
<div class="form-group">
|
||
<label>IP-Adresse:</label>
|
||
<input type="text" id="staIP" placeholder="192.168.1.100" pattern="^(\d{1,3}\.){3}\d{1,3}$"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Gateway:</label>
|
||
<input type="text" id="staGateway" placeholder="192.168.1.1" pattern="^(\d{1,3}\.){3}\d{1,3}$"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Subnetzmaske:</label>
|
||
<input type="text" id="staSubnet" placeholder="255.255.255.0" pattern="^(\d{1,3}\.){3}\d{1,3}$" value="255.255.255.0"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>DNS-Server:</label>
|
||
<input type="text" id="staDNS" placeholder="8.8.8.8" pattern="^(\d{1,3}\.){3}\d{1,3}$"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-top: 20px;">
|
||
<label>
|
||
<input type="checkbox" id="ntpEnabled" checked style="width: auto; margin-right: 10px;">
|
||
NTP-Zeitsynchronisation aktivieren (stündlich)
|
||
</label>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>NTP-Zeitserver:</label>
|
||
<input type="text" id="ntpServer" placeholder="ptbtime1.ptb.de" maxlength="31" value="ptbtime1.ptb.de"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
<small style="color: #666;">Standard: ptbtime1.ptb.de (Physikalisch-Technische Bundesanstalt)</small>
|
||
</div>
|
||
|
||
<button onclick="saveWiFiConfig()" style="margin-top: 10px; background: #4caf50; width: 100%;">
|
||
📡 WiFi/Zeit-Server Konfiguration speichern
|
||
</button>
|
||
|
||
</div><!-- Ende section-wifi -->
|
||
|
||
<h2 onclick="toggleSection('section-ota')">
|
||
<span>🔄 Firmware Update (OTA)</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-ota" class="section-content collapsed">
|
||
|
||
<div style="background: #fff9c4; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 20px; border-radius: 8px;">
|
||
<strong>⚠️ Wichtig:</strong> Das Update kann 2-5 Minuten dauern.
|
||
Unterbrechen Sie NICHT die Stromversorgung während des Updates!
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>📊 Aktuelle Version:</label>
|
||
<div id="currentVersion" style="font-size: 18px; font-weight: bold; color: #667eea;
|
||
padding: 10px; background: #f5f5f5; border-radius: 5px; text-align: center;">
|
||
Lädt...
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Firmware-Datei hochladen:</label>
|
||
<input type="file" id="firmwareFile" accept=".bin"
|
||
style="padding: 10px; border: 2px solid #ddd; border-radius: 8px; width: 100%; box-sizing: border-box;">
|
||
<button onclick="uploadFirmware()" style="margin-top: 10px; background: #4caf50;">
|
||
📤 Firmware hochladen
|
||
</button>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Firmware-URL:</label>
|
||
<input type="text" id="firmwareURL" placeholder="http://example.com/firmware.bin"
|
||
style="width: 100%; padding: 12px; border: 2px solid #ddd;
|
||
border-radius: 8px; font-size: 16px; box-sizing: border-box;">
|
||
<button onclick="updateFromURL()" style="margin-top: 10px; background: #2196f3;">
|
||
🌐 Von URL aktualisieren
|
||
</button>
|
||
</div>
|
||
|
||
<div id="otaProgress" style="display: none; margin-top: 20px;
|
||
padding: 20px; background: #e3f2fd; border-radius: 8px;
|
||
border-left: 4px solid #2196f3;">
|
||
<div style="font-size: 18px; font-weight: bold; margin-bottom: 10px;">
|
||
Update läuft...
|
||
</div>
|
||
<div style="width: 100%; background: #ddd; border-radius: 10px;
|
||
height: 30px; overflow: hidden;">
|
||
<div id="otaProgressBar" style="width: 0%; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||
height: 100%; transition: width 0.3s; text-align: center;
|
||
line-height: 30px; color: white; font-weight: bold;">
|
||
0%
|
||
</div>
|
||
</div>
|
||
<div id="otaMessage" style="margin-top: 10px; font-size: 14px; color: #666;">
|
||
Starte Update...
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- Ende section-ota -->
|
||
|
||
<h2 onclick="toggleSection('section-colors')">
|
||
<span>🎨 Farbeinstellungen</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-colors" class="section-content collapsed">
|
||
|
||
<div class="form-group">
|
||
<label>🔤 Grundfarbe der Zeichen:</label>
|
||
<input type="color" id="normalColor" value="#FFFFFF" onchange="updatePreview()">
|
||
<div class="color-preview">
|
||
<div class="color-box" id="previewNormal"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>✨ Farbe für Spezialtext:</label>
|
||
<input type="color" id="specialColor" value="#FFFFFF" onchange="updatePreview()">
|
||
<div class="color-preview">
|
||
<div class="color-box" id="previewSpecial"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>💡 Helligkeit:</label>
|
||
<input type="range" id="brightness" min="10" max="80" value="80"
|
||
oninput="document.getElementById('brightnessValue').innerHTML = this.value">
|
||
<div class="range-value" id="brightnessValue">80</div>
|
||
</div>
|
||
|
||
<button onclick="saveColors()" style="background: #ff9800; margin-top: 10px;">🎨 Farbeinstellungen speichern</button>
|
||
|
||
</div><!-- Ende section-colors -->
|
||
|
||
<h2 onclick="toggleSection('section-charsoap')">
|
||
<span>🔤 Wortmatrix</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-charsoap" class="section-content collapsed">
|
||
|
||
<div class="form-group">
|
||
<label>📝 Charsoap (11×10 Zeichen):</label>
|
||
<div style="font-size: 12px; color: #666; margin-bottom: 10px;">
|
||
Großbuchstaben (A-Z), Kleinbuchstaben (a-z) und Bindestriche (-). Genau 110 Zeichen.<br>
|
||
<strong style="color: #ff9800;">💡 Tipp:</strong> Umlaute werden zu lowercase: Ä→a, Ö→o, Ü→u
|
||
</div>
|
||
<textarea id="charsoap" rows="11" maxlength="110"
|
||
oninput="validateCharsoap()"
|
||
onkeypress="return /[A-ZÄÖÜa-zäöü-]/i.test(event.key)"></textarea>
|
||
<div id="charsoap-info" style="margin-top: 10px; font-size: 13px;">
|
||
<span id="charsoap-length" style="color: #666;">Länge: 0 / 110</span>
|
||
<span id="charsoap-valid" style="margin-left: 15px;"></span>
|
||
</div>
|
||
<div style="margin-top: 10px;">
|
||
<button type="button" onclick="resetCharsoap()" class="secondary">
|
||
🔄 Standard wiederherstellen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<button onclick="saveCharsoap()" style="background: #9c27b0; margin-top: 10px;">🔤 Wortmatrix speichern</button>
|
||
|
||
</div><!-- Ende section-charsoap -->
|
||
|
||
<h2 onclick="toggleSection('section-specialwords')">
|
||
<span>✨ Spezialwörter</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-specialwords" class="section-content collapsed">
|
||
<div style="font-size: 13px; color: #666; margin-bottom: 15px;">
|
||
Konfiguriere bis zu 3 Spezialwörter und deren Anzeigeintervall. Max. 11 Zeichen pro Wort.
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>⏱️ Anzeigeintervall:</label>
|
||
<select id="specialwordinterval" style="width: 100%; padding: 8px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--input-bg); color: var(--text-primary);">
|
||
<option value="1">Jede Minute</option>
|
||
<option value="5">Alle 5 Minuten</option>
|
||
<option value="10">Alle 10 Minuten</option>
|
||
<option value="15">Alle 15 Minuten (viertel Stunde)</option>
|
||
<option value="30">Alle 30 Minuten (halbe Stunde)</option>
|
||
<option value="60" selected>Alle 60 Minuten (jede Stunde)</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>🎯 Wort 1:</label>
|
||
<input type="text" id="specialword0" maxlength="11" placeholder="z.B. LIMONCIELLO">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>🎯 Wort 2:</label>
|
||
<input type="text" id="specialword1" maxlength="11" placeholder="Optional">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>🎯 Wort 3:</label>
|
||
<input type="text" id="specialword2" maxlength="11" placeholder="Optional">
|
||
</div>
|
||
|
||
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
||
<button onclick="saveSpecialWords()" style="flex: 1; background: #9c27b0;">
|
||
💾 Spezialwörter speichern
|
||
</button>
|
||
<button type="button" onclick="resetSpecialWords()" class="secondary" style="flex: 1;">
|
||
🔄 Zurücksetzen
|
||
</button>
|
||
</div>
|
||
</div><!-- Ende section-specialwords -->
|
||
|
||
<h2 onclick="toggleSection('section-minuteleds')">
|
||
<span>⏰ Minuten-LEDs</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-minuteleds" class="section-content collapsed">
|
||
<div style="font-size: 13px; color: #666; margin-bottom: 15px;">
|
||
Konfiguriere die 4 LEDs für die Minuten-Anzeige (Werte 0-120).
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>💡 LED 1 (1. Minute):</label>
|
||
<input type="number" id="minuteled0" min="0" max="120" placeholder="112">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>💡 LED 2 (2. Minute):</label>
|
||
<input type="number" id="minuteled1" min="0" max="120" placeholder="114">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>💡 LED 3 (3. Minute):</label>
|
||
<input type="number" id="minuteled2" min="0" max="120" placeholder="116">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>💡 LED 4 (4. Minute):</label>
|
||
<input type="number" id="minuteled3" min="0" max="120" placeholder="118">
|
||
</div>
|
||
|
||
<div style="margin-top: 10px; display: flex; gap: 10px;">
|
||
<button onclick="saveMinuteLeds()" style="flex: 1; background: #ff9800;">
|
||
💾 Minuten-LEDs speichern
|
||
</button>
|
||
<button type="button" onclick="resetMinuteLeds()" class="secondary" style="flex: 1;">
|
||
🔄 Zurücksetzen
|
||
</button>
|
||
</div>
|
||
</div><!-- Ende section-minuteleds -->
|
||
|
||
<h2 onclick="toggleSection('section-ledtest')">
|
||
<span>🔬 LED Test & Diagnose</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-ledtest" class="section-content collapsed">
|
||
|
||
<div class="form-group">
|
||
<label>Test-Modus:</label>
|
||
|
||
<div class="test-modes">
|
||
<div class="test-option">
|
||
<input type="radio" id="testSingle" name="testMode" value="single" checked>
|
||
<label for="testSingle">Einzelne LED</label>
|
||
</div>
|
||
|
||
<div class="test-option">
|
||
<input type="radio" id="testRange" name="testMode" value="range">
|
||
<label for="testRange">LED-Bereich</label>
|
||
</div>
|
||
|
||
<div class="test-option">
|
||
<input type="radio" id="testAll" name="testMode" value="all">
|
||
<label for="testAll">Alle LEDs</label>
|
||
</div>
|
||
|
||
<div class="test-option">
|
||
<input type="radio" id="testRow" name="testMode" value="row">
|
||
<label for="testRow">Zeile</label>
|
||
</div>
|
||
|
||
<div class="test-option">
|
||
<input type="radio" id="testCol" name="testMode" value="col">
|
||
<label for="testCol">Spalte</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" id="testSingleControls">
|
||
<label>LED-Nummer (0-113):</label>
|
||
<input type="number" id="ledNumber" min="0" max="113" value="0">
|
||
</div>
|
||
|
||
<div class="form-group" id="testRangeControls" style="display: none;">
|
||
<label>Von LED:</label>
|
||
<input type="number" id="ledFrom" min="0" max="113" value="0" style="margin-bottom: 10px;">
|
||
|
||
<label>Bis LED:</label>
|
||
<input type="number" id="ledTo" min="0" max="113" value="10">
|
||
</div>
|
||
|
||
<div class="form-group" id="testRowControls" style="display: none;">
|
||
<label>Zeile (0-9):</label>
|
||
<input type="number" id="rowNumber" min="0" max="9" value="0">
|
||
</div>
|
||
|
||
<div class="form-group" id="testColControls" style="display: none;">
|
||
<label>Spalte (0-10):</label>
|
||
<input type="number" id="colNumber" min="0" max="10" value="0">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Test-Farbe:</label>
|
||
<input type="color" id="testColor" value="#FFFFFF">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Test-Helligkeit:</label>
|
||
<input type="range" id="testBrightness" min="10" max="255" value="80"
|
||
oninput="document.getElementById('testBrightnessValue').innerHTML = this.value">
|
||
<div class="range-value" id="testBrightnessValue">80</div>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 10px;">
|
||
<button onclick="testLED()" style="flex: 1; background: #4caf50;">
|
||
✅ LED(s) einschalten
|
||
</button>
|
||
<button onclick="clearLEDs()" style="flex: 1; background: #f44336;">
|
||
⭕ Alle ausschalten
|
||
</button>
|
||
</div>
|
||
|
||
<div style="margin-top: 15px; padding: 15px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
|
||
<strong>💡 Tipps:</strong><br>
|
||
• Matrix: LEDs 0-109<br>
|
||
• Minuten: LEDs 110-113<br>
|
||
• Zeile 0 = oben, Zeile 9 = unten<br>
|
||
• Spalte 0 = links, Spalte 10 = rechts
|
||
</div>
|
||
|
||
</div><!-- Ende section-ledtest -->
|
||
|
||
<h2 onclick="toggleSection('section-patterntest')">
|
||
<span>🎯 Pattern-Test (Kritische Zeiten)</span>
|
||
<span class="section-toggle collapsed">▼</span>
|
||
</h2>
|
||
<div id="section-patterntest" class="section-content collapsed">
|
||
|
||
<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin-bottom: 15px; border-radius: 8px;">
|
||
<strong>ℹ️ Pattern-Test:</strong><br>
|
||
Zeigt alle kritischen Uhrzeiten nacheinander für jeweils 2 Sekunden an.<br>
|
||
<strong>Getestete Fälle:</strong><br>
|
||
• Stunde 0 & 1 komplett (EIN vs EINS, mit/ohne UHR)<br>
|
||
• VIER, FÜNF, ZEHN als Stunde (Wortüberlappungen)<br>
|
||
• VIERTEL VOR VIER, FÜNF VOR FÜNF, ZEHN VOR ZEHN<br>
|
||
• <strong>Dauer:</strong> ca. 2 Minuten | <strong>Anzahl:</strong> ~65 Zeiten
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<button onclick="startPatternTest()" style="padding: 12px 20px; font-size: 16px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; cursor: pointer;">
|
||
▶ Pattern-Test starten
|
||
</button>
|
||
</div>
|
||
|
||
<div id="patternTestStatus" style="margin-top: 15px; padding: 10px; border-radius: 5px; display: none;"></div>
|
||
|
||
</div><!-- Ende section-patterntest -->
|
||
|
||
</div><!-- Ende container -->
|
||
|
||
<script>
|
||
// ════════════════════════════════════════════════════════════════
|
||
// AUFKLAPPBARE BEREICHE
|
||
// ════════════════════════════════════════════════════════════════
|
||
function toggleSection(sectionId) {
|
||
const section = document.getElementById(sectionId);
|
||
const toggle = event.currentTarget.querySelector('.section-toggle');
|
||
|
||
const isCurrentlyCollapsed = section.classList.contains('collapsed');
|
||
|
||
// Alle anderen Sektionen schließen
|
||
if (isCurrentlyCollapsed) {
|
||
const allSections = ['section-time', 'section-wifi', 'section-ota', 'section-colors', 'section-charsoap', 'section-ledtest', 'section-patterntest', 'section-autobrightness'];
|
||
allSections.forEach(id => {
|
||
if (id !== sectionId) {
|
||
const otherSection = document.getElementById(id);
|
||
if (otherSection && !otherSection.classList.contains('collapsed')) {
|
||
otherSection.classList.add('collapsed');
|
||
// Toggle-Pfeil der anderen Sektion auch schließen
|
||
const otherHeader = otherSection.previousElementSibling;
|
||
if (otherHeader && otherHeader.tagName === 'H2') {
|
||
const otherToggle = otherHeader.querySelector('.section-toggle');
|
||
if (otherToggle) {
|
||
otherToggle.classList.add('collapsed');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Aktuelle Sektion umschalten
|
||
section.classList.toggle('collapsed');
|
||
toggle.classList.toggle('collapsed');
|
||
|
||
// Helligkeits-Sektion Status tracken
|
||
if (sectionId === 'section-autobrightness') {
|
||
brightnessectionOpen = !section.classList.contains('collapsed');
|
||
console.log('Helligkeits-Sektion:', brightnessectionOpen ? 'geöffnet' : 'geschlossen');
|
||
} else {
|
||
// Wenn andere Sektion geöffnet wird, Helligkeits-Sektion ist geschlossen
|
||
brightnessectionOpen = false;
|
||
}
|
||
|
||
// Beim Öffnen des WiFi-Bereichs automatisch scannen
|
||
if (sectionId === 'section-wifi' && !section.classList.contains('collapsed') && !wifiScanned) {
|
||
scanWiFi(savedSsidFromConfig);
|
||
}
|
||
}
|
||
|
||
// Beim Laden alle Bereiche einklappen
|
||
window.addEventListener('load', function() {
|
||
const sections = ['section-time', 'section-wifi', 'section-ota', 'section-colors', 'section-charsoap', 'section-ledtest', 'section-patterntest', 'section-autobrightness'];
|
||
sections.forEach(id => {
|
||
const section = document.getElementById(id);
|
||
if (section) {
|
||
section.classList.add('collapsed');
|
||
// Auch den Toggle-Pfeil einklappen
|
||
const header = section.previousElementSibling;
|
||
if (header && header.tagName === 'H2') {
|
||
const toggle = header.querySelector('.section-toggle');
|
||
if (toggle) {
|
||
toggle.classList.add('collapsed');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// ZEIT & DRIFT
|
||
// ════════════════════════════════════════════════════════════════
|
||
let espTimestamp = 0;
|
||
let browserTimestamp = 0;
|
||
|
||
function updateBrowserTime() {
|
||
const now = new Date();
|
||
document.getElementById('browserTime').innerHTML =
|
||
now.toLocaleTimeString('de-DE');
|
||
|
||
const utc = Math.floor(now.getTime() / 1000);
|
||
const offset = now.getTimezoneOffset() * 60;
|
||
browserTimestamp = utc - offset;
|
||
|
||
if (espTimestamp > 0) calculateDrift();
|
||
}
|
||
|
||
function updatePreview() {
|
||
document.getElementById('previewNormal').style.backgroundColor =
|
||
document.getElementById('normalColor').value;
|
||
document.getElementById('previewSpecial').style.backgroundColor =
|
||
document.getElementById('specialColor').value;
|
||
}
|
||
|
||
function toggleTimeMode() {
|
||
const isManual = document.getElementById('manualTime').checked;
|
||
const manualInputs = document.getElementById('manualTimeInputs');
|
||
|
||
if (isManual) {
|
||
manualInputs.style.display = 'block';
|
||
const now = new Date();
|
||
const offset = now.getTimezoneOffset() * 60000;
|
||
const localISOTime = (new Date(now - offset)).toISOString().slice(0, 16);
|
||
document.getElementById('manualDateTime').value = localISOTime;
|
||
} else {
|
||
manualInputs.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
function fetchEspTime() {
|
||
fetch('/gettime')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
document.getElementById('espTime').innerHTML = data.time;
|
||
espTimestamp = data.timestamp;
|
||
calculateDrift();
|
||
updateSyncInfo(data);
|
||
updateNTPStatus(); // NTP-Status aktualisieren
|
||
})
|
||
.catch(error => {
|
||
console.error('Fehler:', error);
|
||
document.getElementById('espTime').innerHTML = 'Fehler';
|
||
});
|
||
}
|
||
|
||
function updateNTPStatus() {
|
||
// WiFi-Config abrufen, um NTP-Status zu prüfen
|
||
fetch('/wifi/config')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const ntpRadio = document.getElementById('ntpTime');
|
||
const ntpIcon = document.getElementById('ntpStatusIcon');
|
||
const driftRateRow = document.getElementById('driftRateRow');
|
||
const syncCountRow = document.getElementById('syncCountRow');
|
||
|
||
if (data.ntpEnabled && data.staConnected) {
|
||
// NTP ist aktiviert und WiFi verbunden
|
||
ntpRadio.disabled = false;
|
||
ntpRadio.checked = true;
|
||
ntpIcon.innerHTML = data.ntpSyncOk ? '✅' : '🌐';
|
||
ntpRadio.parentElement.style.opacity = '1';
|
||
|
||
// Drift-Rate und Sync-Count ausblenden (bei NTP nicht relevant)
|
||
if (driftRateRow) driftRateRow.style.display = 'none';
|
||
if (syncCountRow) syncCountRow.style.display = 'none';
|
||
} else if (data.ntpEnabled && !data.staConnected) {
|
||
// NTP aktiviert, aber WiFi nicht verbunden
|
||
ntpRadio.disabled = true;
|
||
ntpRadio.checked = false;
|
||
ntpIcon.innerHTML = '⚠️';
|
||
ntpRadio.parentElement.style.opacity = '0.6';
|
||
document.getElementById('autoTime').checked = true;
|
||
|
||
// Drift-Rate und Sync-Count anzeigen (manuelle Synchronisation)
|
||
if (driftRateRow) driftRateRow.style.display = 'block';
|
||
if (syncCountRow) syncCountRow.style.display = 'block';
|
||
} else {
|
||
// NTP deaktiviert
|
||
ntpRadio.disabled = true;
|
||
ntpRadio.checked = false;
|
||
ntpIcon.innerHTML = '❌';
|
||
ntpRadio.parentElement.style.opacity = '0.6';
|
||
document.getElementById('autoTime').checked = true;
|
||
|
||
// Drift-Rate und Sync-Count anzeigen (manuelle Synchronisation)
|
||
if (driftRateRow) driftRateRow.style.display = 'block';
|
||
if (syncCountRow) syncCountRow.style.display = 'block';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('NTP Status Fehler:', error);
|
||
});
|
||
}
|
||
|
||
function calculateDrift() {
|
||
if (espTimestamp > 0 && browserTimestamp > 0) {
|
||
const drift = espTimestamp - browserTimestamp;
|
||
const driftAbs = Math.abs(drift);
|
||
|
||
const driftInfo = document.getElementById('driftInfo');
|
||
const driftValue = document.getElementById('driftValue');
|
||
const driftStatus = document.getElementById('driftStatus');
|
||
|
||
driftInfo.style.display = 'block';
|
||
|
||
if (drift > 0) {
|
||
driftValue.innerHTML = '+' + drift;
|
||
driftStatus.innerHTML = 'Die Zeit geht ' + drift + ' Sekunden vor';
|
||
} else if (drift < 0) {
|
||
driftValue.innerHTML = drift;
|
||
driftStatus.innerHTML = 'Die Zeit geht ' + driftAbs + ' Sekunden nach';
|
||
} else {
|
||
driftValue.innerHTML = '0';
|
||
driftStatus.innerHTML = 'Perfekt synchronisiert! ✓';
|
||
}
|
||
|
||
driftInfo.className = '';
|
||
if (driftAbs <= 5) {
|
||
driftInfo.classList.add('drift-good');
|
||
} else if (driftAbs <= 30) {
|
||
driftInfo.classList.add('drift-warning');
|
||
} else {
|
||
driftInfo.classList.add('drift-bad');
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateSyncInfo(data) {
|
||
if (data.lastSync > 0) {
|
||
const syncDate = new Date(data.lastSync * 1000);
|
||
const daysSince = ((Date.now() / 1000 - data.lastSync) / 86400).toFixed(1);
|
||
document.getElementById('lastSync').innerHTML =
|
||
syncDate.toLocaleString('de-DE') + ' (vor ' + daysSince + ' Tagen)';
|
||
} else {
|
||
document.getElementById('lastSync').innerHTML = 'Noch nie';
|
||
}
|
||
|
||
if (data.driftRate != 0) {
|
||
const sign = data.driftRate > 0 ? '+' : '';
|
||
document.getElementById('driftRate').innerHTML =
|
||
sign + data.driftRate + ' Sek/Tag (' + data.syncCount + ' Messungen)';
|
||
} else {
|
||
document.getElementById('driftRate').innerHTML = 'Nicht kalibriert';
|
||
}
|
||
|
||
document.getElementById('syncCount').innerHTML = data.syncCount;
|
||
|
||
const uptimeDays = (data.uptime / 86400).toFixed(1);
|
||
const uptimeHours = Math.floor((data.uptime % 86400) / 3600);
|
||
const uptimeMinutes = Math.floor((data.uptime % 3600) / 60);
|
||
|
||
document.getElementById('uptime').innerHTML =
|
||
uptimeDays + ' Tage (' + uptimeHours + 'h ' + uptimeMinutes + 'm)';
|
||
}
|
||
|
||
function validateCharsoap() {
|
||
const textarea = document.getElementById('charsoap');
|
||
const lengthSpan = document.getElementById('charsoap-length');
|
||
const validSpan = document.getElementById('charsoap-valid');
|
||
|
||
let text = textarea.value.toUpperCase();
|
||
|
||
const hadUmlauts = /[ÄÖÜ]/.test(text);
|
||
text = text.replace(/Ä/g, 'a');
|
||
text = text.replace(/Ö/g, 'o');
|
||
text = text.replace(/Ü/g, 'u');
|
||
|
||
if (textarea.value !== text) {
|
||
const cursorPos = textarea.selectionStart;
|
||
textarea.value = text;
|
||
textarea.setSelectionRange(cursorPos, cursorPos);
|
||
|
||
if (hadUmlauts) {
|
||
validSpan.innerHTML = '💡 Umlaut(e) → lowercase';
|
||
validSpan.style.color = '#ff9800';
|
||
|
||
setTimeout(() => {
|
||
validateCharsoap();
|
||
}, 2000);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const length = textarea.value.length;
|
||
lengthSpan.innerHTML = 'Länge: ' + length + ' / 110';
|
||
|
||
//const valid = /^[A-Za-z\-]*$/.test(textarea.value);
|
||
//const valid = /^([A-Za-z\-]*|DL0WH)$/.test(textarea.value);
|
||
const valid = /^(?:[A-Z0-9ÄÖÜ\-]*|DL0WH)$/.test(textarea.value);
|
||
|
||
if (length === 110 && valid) {
|
||
lengthSpan.style.color = '#4caf50';
|
||
validSpan.innerHTML = '✓ Gültig';
|
||
validSpan.style.color = '#4caf50';
|
||
} else if (length > 110) {
|
||
lengthSpan.style.color = '#f44336';
|
||
validSpan.innerHTML = '❌ Zu lang!';
|
||
validSpan.style.color = '#f44336';
|
||
} else if (!valid) {
|
||
const invalidCharMatch = textarea.value.match(/[^A-Z0ÄÖÜ\-]/);
|
||
if (invalidCharMatch)
|
||
{
|
||
const position = invalidCharMatch.index;
|
||
const invalidChar = invalidCharMatch[0];
|
||
console.log(`Ungültiges Zeichen '${invalidChar}' an Position ${position}`);
|
||
}
|
||
lengthSpan.style.color = '#f44336';
|
||
validSpan.innerHTML = '❌ Ungültiges Zeichen "'+ invalidChar + '" an ' + position;
|
||
validSpan.style.color = '#f44336';
|
||
} else {
|
||
lengthSpan.style.color = '#ffc107';
|
||
validSpan.innerHTML = '⚠ Zu kurz';
|
||
validSpan.style.color = '#ffc107';
|
||
}
|
||
|
||
formatCharsoap();
|
||
}
|
||
|
||
function formatCharsoap() {
|
||
const textarea = document.getElementById('charsoap');
|
||
let text = textarea.value.replace(/\n/g, '');
|
||
|
||
if (text.length > 0) {
|
||
let formatted = '';
|
||
for (let i = 0; i < text.length; i += 11) {
|
||
if (i > 0) formatted += '\n';
|
||
formatted += text.substr(i, 11);
|
||
}
|
||
|
||
if (textarea.value !== formatted) {
|
||
const cursorPos = textarea.selectionStart;
|
||
textarea.value = formatted;
|
||
textarea.setSelectionRange(cursorPos, cursorPos);
|
||
}
|
||
}
|
||
}
|
||
|
||
function resetCharsoap() {
|
||
if (confirm('Charsoap auf Standard zurücksetzen?')) {
|
||
fetch('/resetcharsoap')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Zurückgesetzt!');
|
||
loadCharsoap();
|
||
})
|
||
.catch(error => alert('❌ Fehler: ' + error));
|
||
}
|
||
}
|
||
|
||
function loadCharsoap() {
|
||
fetch('/getcharsoap')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
document.getElementById('charsoap').value = data;
|
||
validateCharsoap();
|
||
})
|
||
.catch(error => console.error('Fehler:', error));
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// SPEICHER-FUNKTIONEN (GETRENNT)
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
function saveTime() {
|
||
let timestamp, timezoneOffset = 0;
|
||
const isManual = document.getElementById('manualTime').checked;
|
||
|
||
if (isManual) {
|
||
const manualDateTime = document.getElementById('manualDateTime').value;
|
||
if (!manualDateTime) {
|
||
alert('❌ Bitte Datum/Uhrzeit eingeben!');
|
||
return;
|
||
}
|
||
// Manuelle Eingabe wird als lokale Zeit interpretiert, zu UTC konvertieren
|
||
const localDate = new Date(manualDateTime);
|
||
const utcTimestamp = Math.floor(localDate.getTime() / 1000);
|
||
const offsetSeconds = localDate.getTimezoneOffset() * 60;
|
||
timestamp = utcTimestamp - offsetSeconds; // Zu UTC konvertieren
|
||
} else {
|
||
// Sende UTC-Zeit (Date.now() ist bereits UTC)
|
||
timestamp = Math.floor(Date.now() / 1000);
|
||
timezoneOffset = 0; // Keine Umrechnung nötig, ist bereits UTC
|
||
}
|
||
|
||
// Nur Zeit speichern (mit Dummy-Werten für Farben/Helligkeit)
|
||
const url = '/save?timestamp=' + timestamp +
|
||
'&tzoffset=' + timezoneOffset +
|
||
'&nr=255&ng=255&nb=255' + // Dummy
|
||
'&sr=255&sg=255&sb=255' + // Dummy
|
||
'&brightness=80' + // Dummy
|
||
'&charsoap='; // Leer = nicht ändern
|
||
|
||
fetch(url)
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Uhrzeit gespeichert!');
|
||
setTimeout(fetchEspTime, 2000);
|
||
})
|
||
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
|
||
}
|
||
|
||
function saveColors() {
|
||
const normalColor = document.getElementById('normalColor').value;
|
||
const specialColor = document.getElementById('specialColor').value;
|
||
const brightness = document.getElementById('brightness').value;
|
||
|
||
const normalRGB = hexToRgb(normalColor);
|
||
const specialRGB = hexToRgb(specialColor);
|
||
|
||
// Nur Farben & Helligkeit speichern
|
||
const url = '/save?timestamp=0' + // 0 = Zeit nicht ändern
|
||
'&tzoffset=0' +
|
||
'&nr=' + normalRGB.r + '&ng=' + normalRGB.g + '&nb=' + normalRGB.b +
|
||
'&sr=' + specialRGB.r + '&sg=' + specialRGB.g + '&sb=' + specialRGB.b +
|
||
'&brightness=' + brightness +
|
||
'&charsoap='; // Leer = nicht ändern
|
||
|
||
fetch(url)
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Farbeinstellungen gespeichert!');
|
||
})
|
||
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
|
||
}
|
||
|
||
function saveCharsoap() {
|
||
let charsoap = document.getElementById('charsoap').value;
|
||
|
||
charsoap = charsoap.replace(/\s/g, '');
|
||
|
||
if (charsoap.length !== 110) {
|
||
alert('❌ Charsoap muss 110 Zeichen lang sein!\nAktuell: ' + charsoap.length);
|
||
return;
|
||
}
|
||
|
||
if (!/^[A-Za-z0-9\-]*$/.test(charsoap)) {
|
||
alert('❌ Nur Buchstaben (A-Z, a-z) und Bindestriche (-)!');
|
||
return;
|
||
}
|
||
|
||
// Nur Charsoap speichern
|
||
const url = '/save?timestamp=0' + // 0 = Zeit nicht ändern
|
||
'&tzoffset=0' +
|
||
'&nr=255&ng=255&nb=255' + // Dummy
|
||
'&sr=255&sg=255&sb=255' + // Dummy
|
||
'&brightness=80' + // Dummy
|
||
'&charsoap=' + encodeURIComponent(charsoap);
|
||
|
||
fetch(url)
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Wortmatrix gespeichert!');
|
||
})
|
||
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
|
||
}
|
||
|
||
function testLED() {
|
||
const mode = document.querySelector('input[name="testMode"]:checked').value;
|
||
const color = document.getElementById('testColor').value;
|
||
const brightness = document.getElementById('testBrightness').value;
|
||
|
||
const rgb = hexToRgb(color);
|
||
|
||
let url = '/ledtest?mode=' + mode;
|
||
url += '&r=' + rgb.r + '&g=' + rgb.g + '&b=' + rgb.b;
|
||
url += '&brightness=' + brightness;
|
||
|
||
if (mode === 'single') {
|
||
const ledNum = document.getElementById('ledNumber').value;
|
||
url += '&led=' + ledNum;
|
||
} else if (mode === 'range') {
|
||
const from = document.getElementById('ledFrom').value;
|
||
const to = document.getElementById('ledTo').value;
|
||
url += '&from=' + from + '&to=' + to;
|
||
} else if (mode === 'row') {
|
||
const row = document.getElementById('rowNumber').value;
|
||
url += '&row=' + row;
|
||
} else if (mode === 'col') {
|
||
const col = document.getElementById('colNumber').value;
|
||
url += '&col=' + col;
|
||
}
|
||
|
||
fetch(url)
|
||
.then(response => response.text())
|
||
.then(data => console.log('LED Test:', data))
|
||
.catch(error => alert('❌ Fehler: ' + error));
|
||
}
|
||
|
||
function clearLEDs() {
|
||
fetch('/ledtest?mode=clear')
|
||
.then(response => response.text())
|
||
.catch(error => console.error('Error:', error));
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// PATTERN TEST
|
||
// ════════════════════════════════════════════════════════════════
|
||
function startPatternTest() {
|
||
const statusDiv = document.getElementById('patternTestStatus');
|
||
statusDiv.style.display = 'block';
|
||
statusDiv.style.background = '#d1ecf1';
|
||
statusDiv.style.color = '#0c5460';
|
||
statusDiv.innerHTML = '⏳ Pattern-Test läuft... Bitte warten (ca. 2-3 Minuten)';
|
||
|
||
fetch('/patterntest')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
statusDiv.style.background = '#d4edda';
|
||
statusDiv.style.color = '#155724';
|
||
statusDiv.innerHTML = '✓ Pattern-Test abgeschlossen! Die Uhr zeigt wieder die aktuelle Zeit an.';
|
||
setTimeout(() => {
|
||
statusDiv.style.display = 'none';
|
||
}, 5000);
|
||
})
|
||
.catch(error => {
|
||
statusDiv.style.background = '#f8d7da';
|
||
statusDiv.style.color = '#721c24';
|
||
statusDiv.innerHTML = '❌ Fehler: ' + error;
|
||
});
|
||
}
|
||
|
||
function hexToRgb(hex) {
|
||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||
return result ? {
|
||
r: parseInt(result[1], 16),
|
||
g: parseInt(result[2], 16),
|
||
b: parseInt(result[3], 16)
|
||
} : null;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
document.querySelectorAll('input[name="testMode"]').forEach(radio => {
|
||
radio.addEventListener('change', function() {
|
||
document.getElementById('testSingleControls').style.display = 'none';
|
||
document.getElementById('testRangeControls').style.display = 'none';
|
||
document.getElementById('testRowControls').style.display = 'none';
|
||
document.getElementById('testColControls').style.display = 'none';
|
||
|
||
if (this.value === 'single') {
|
||
document.getElementById('testSingleControls').style.display = 'block';
|
||
} else if (this.value === 'range') {
|
||
document.getElementById('testRangeControls').style.display = 'block';
|
||
} else if (this.value === 'row') {
|
||
document.getElementById('testRowControls').style.display = 'block';
|
||
} else if (this.value === 'col') {
|
||
document.getElementById('testColControls').style.display = 'block';
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// OTA UPDATE FUNCTIONS
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
function loadOTAInfo() {
|
||
fetch('/ota/info')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const versionText = data.version +
|
||
' (' + data.buildDate + ' ' + data.buildTime + ')';
|
||
document.getElementById('currentVersion').innerHTML = versionText;
|
||
|
||
console.log('OTA Info:', data);
|
||
console.log('Free Space:', Math.round(data.freeSpace / 1024) + ' KB');
|
||
})
|
||
.catch(error => {
|
||
console.error('OTA Info Error:', error);
|
||
document.getElementById('currentVersion').innerHTML = 'Fehler beim Laden';
|
||
});
|
||
}
|
||
|
||
function uploadFirmware() {
|
||
const fileInput = document.getElementById('firmwareFile');
|
||
const file = fileInput.files[0];
|
||
|
||
if (!file) {
|
||
alert('❌ Bitte wählen Sie eine Firmware-Datei aus!');
|
||
return;
|
||
}
|
||
|
||
if (!file.name.endsWith('.bin')) {
|
||
alert('❌ Nur .bin Dateien sind erlaubt!');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('Update wirklich starten?\n\nDas Gerät wird neu gestartet!')) {
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('firmware', file);
|
||
|
||
document.getElementById('otaProgress').style.display = 'block';
|
||
document.getElementById('otaMessage').innerHTML = 'Lade Firmware hoch...';
|
||
|
||
const xhr = new XMLHttpRequest();
|
||
|
||
xhr.upload.addEventListener('progress', function(e) {
|
||
if (e.lengthComputable) {
|
||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||
updateOTAProgress(percentComplete, 'Hochladen: ' + percentComplete + '%');
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('load', function() {
|
||
if (xhr.status === 200) {
|
||
updateOTAProgress(100, '✅ Update erfolgreich! Neustart in 3 Sekunden...');
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 5000);
|
||
} else {
|
||
const response = JSON.parse(xhr.responseText);
|
||
updateOTAProgress(0, '❌ Fehler: ' + response.message);
|
||
document.getElementById('otaProgress').style.display = 'none';
|
||
}
|
||
});
|
||
|
||
xhr.addEventListener('error', function() {
|
||
updateOTAProgress(0, '❌ Netzwerkfehler beim Upload');
|
||
setTimeout(() => {
|
||
document.getElementById('otaProgress').style.display = 'none';
|
||
}, 3000);
|
||
});
|
||
|
||
xhr.open('POST', '/ota/upload');
|
||
xhr.send(formData);
|
||
|
||
pollOTAStatus();
|
||
}
|
||
|
||
function updateFromURL() {
|
||
const urlInput = document.getElementById('firmwareURL');
|
||
const url = urlInput.value.trim();
|
||
|
||
if (!url) {
|
||
alert('❌ Bitte geben Sie eine URL ein!');
|
||
return;
|
||
}
|
||
|
||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||
alert('❌ URL muss mit http:// oder https:// beginnen!');
|
||
return;
|
||
}
|
||
|
||
if (!confirm('Update von URL starten?\n\n' + url + '\n\nDas Gerät wird neu gestartet!')) {
|
||
return;
|
||
}
|
||
|
||
document.getElementById('otaProgress').style.display = 'block';
|
||
document.getElementById('otaMessage').innerHTML = 'Verbinde zu URL...';
|
||
|
||
fetch('/ota/url?url=' + encodeURIComponent(url))
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
updateOTAProgress(0, 'Download gestartet...');
|
||
pollOTAStatus();
|
||
} else {
|
||
updateOTAProgress(0, '❌ Fehler: ' + data.message);
|
||
setTimeout(() => {
|
||
document.getElementById('otaProgress').style.display = 'none';
|
||
}, 3000);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
updateOTAProgress(0, '❌ Fehler: ' + error);
|
||
setTimeout(() => {
|
||
document.getElementById('otaProgress').style.display = 'none';
|
||
}, 3000);
|
||
});
|
||
}
|
||
|
||
function updateOTAProgress(percent, message) {
|
||
const progressBar = document.getElementById('otaProgressBar');
|
||
const messageDiv = document.getElementById('otaMessage');
|
||
|
||
progressBar.style.width = percent + '%';
|
||
progressBar.innerHTML = percent + '%';
|
||
|
||
if (message) {
|
||
messageDiv.innerHTML = message;
|
||
}
|
||
|
||
if (message.includes('❌')) {
|
||
progressBar.style.background = '#f44336';
|
||
} else if (message.includes('✅')) {
|
||
progressBar.style.background = '#4caf50';
|
||
} else {
|
||
progressBar.style.background = 'linear-gradient(90deg, #667eea 0%, #764ba2 100%)';
|
||
}
|
||
}
|
||
|
||
let otaStatusInterval = null;
|
||
|
||
function pollOTAStatus() {
|
||
if (otaStatusInterval) {
|
||
clearInterval(otaStatusInterval);
|
||
}
|
||
|
||
otaStatusInterval = setInterval(() => {
|
||
fetch('/ota/status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.inProgress) {
|
||
const elapsedSec = Math.floor(data.elapsed / 1000);
|
||
updateOTAProgress(data.progress,
|
||
'Fortschritt: ' + data.progress + '% (' + elapsedSec + 's)');
|
||
} else {
|
||
clearInterval(otaStatusInterval);
|
||
otaStatusInterval = null;
|
||
|
||
if (data.error) {
|
||
updateOTAProgress(100, '❌ Fehler: ' + data.error);
|
||
} else if (data.progress === 100) {
|
||
updateOTAProgress(100, '✅ Update erfolgreich! Neustart...');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Status poll error:', error);
|
||
});
|
||
}, 500);
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// WIFI / NTP CONFIG FUNCTIONS
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
function loadWiFiConfig() {
|
||
fetch('/wifi/config')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// WiFi Station Settings
|
||
document.getElementById('staEnabled').checked = data.staEnabled;
|
||
|
||
// Gespeicherte SSID merken
|
||
savedSsidFromConfig = data.staSsid || '';
|
||
|
||
// Passwort setzen
|
||
document.getElementById('staPassword').value = data.staPassword || '';
|
||
document.getElementById('staDhcp').checked = data.staDhcp;
|
||
document.getElementById('staIP').value = data.staIP || '';
|
||
document.getElementById('staGateway').value = data.staGateway || '';
|
||
document.getElementById('staSubnet').value = data.staSubnet || '255.255.255.0';
|
||
document.getElementById('staDNS').value = data.staDNS || '';
|
||
|
||
// WiFi Status
|
||
document.getElementById('staConnected').innerHTML = data.staConnected ?
|
||
'<span style="color: green;">✓ Verbunden</span>' :
|
||
'<span style="color: red;">✗ Nicht verbunden</span>';
|
||
document.getElementById('staLocalIP').innerHTML = data.staLocalIP || '—';
|
||
document.getElementById('staRSSI').innerHTML = data.staConnected ? data.staRSSI : '—';
|
||
|
||
// NTP Settings
|
||
document.getElementById('ntpEnabled').checked = data.ntpEnabled;
|
||
document.getElementById('ntpServer').value = data.ntpServer || 'ptbtime1.ptb.de';
|
||
|
||
// NTP Status
|
||
if (data.ntpLastSync > 0) {
|
||
const syncDate = new Date(data.ntpLastSync * 1000);
|
||
const syncStr = syncDate.toLocaleString('de-DE');
|
||
const syncStatus = data.ntpSyncOk ? '✓' : '⚠';
|
||
document.getElementById('ntpLastSync').innerHTML = syncStatus + ' ' + syncStr;
|
||
} else {
|
||
document.getElementById('ntpLastSync').innerHTML = '—';
|
||
}
|
||
|
||
// Statische IP-Felder ein/ausblenden
|
||
toggleStaticIPFields();
|
||
|
||
console.log('WiFi Config loaded:', data);
|
||
})
|
||
.catch(error => {
|
||
console.error('WiFi Config Error:', error);
|
||
});
|
||
}
|
||
|
||
let wifiScanned = false;
|
||
let savedSsidFromConfig = '';
|
||
|
||
// Hilfsfunktion: Signalstärke als Unicode-Symbol
|
||
function getSignalSymbol(rssi) {
|
||
if (rssi > -50) return '▰▰▰▰'; // Sehr gut
|
||
if (rssi > -60) return '▰▰▰▱'; // Gut
|
||
if (rssi > -70) return '▰▰▱▱'; // Mittel
|
||
return '▰▱▱▱'; // Schwach
|
||
}
|
||
|
||
// Hilfsfunktion: Verschlüsselung als Unicode-Symbol
|
||
function getEncryptionSymbol(encType) {
|
||
if (encType === 7 || encType === 8) {
|
||
return '●'; // WPA2/WPA3 (geschlossen/sicher)
|
||
}
|
||
return '○'; // Offen/WEP (unsicher)
|
||
}
|
||
|
||
function scanWiFi(savedSsid) {
|
||
const ssidSelect = document.getElementById('staSsid');
|
||
|
||
// Symbol während des Scannens
|
||
ssidSelect.innerHTML = '<option value="">⟳ Scanne verfügbare Netzwerke...</option>';
|
||
ssidSelect.disabled = true;
|
||
|
||
fetch('/wifi/scan')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// Dropdown leeren
|
||
ssidSelect.innerHTML = '<option value="">-- SSID auswählen --</option>';
|
||
|
||
let savedSsidFound = false;
|
||
|
||
if (data.networks && data.networks.length > 0) {
|
||
// Netzwerke nach Signalstärke sortieren (höchste zuerst)
|
||
data.networks.sort((a, b) => b.rssi - a.rssi);
|
||
|
||
// Netzwerke hinzufügen
|
||
data.networks.forEach(network => {
|
||
const option = document.createElement('option');
|
||
option.value = network.ssid;
|
||
|
||
// Unicode-Symbole für Signal und Verschlüsselung
|
||
const signalSymbol = getSignalSymbol(network.rssi);
|
||
const lockSymbol = getEncryptionSymbol(network.encryption);
|
||
|
||
// Nur Text (kein HTML) für <option>
|
||
option.text = `${signalSymbol} ${lockSymbol} ${network.ssid}`;
|
||
|
||
// Gespeicherte SSID vorselektieren
|
||
if (savedSsid && network.ssid === savedSsid) {
|
||
option.selected = true;
|
||
savedSsidFound = true;
|
||
}
|
||
|
||
ssidSelect.add(option);
|
||
});
|
||
}
|
||
|
||
// Wenn gespeicherte SSID nicht im Scan gefunden wurde, trotzdem hinzufügen
|
||
if (savedSsid && !savedSsidFound) {
|
||
const option = document.createElement('option');
|
||
option.value = savedSsid;
|
||
option.text = `${savedSsid} (gespeichert)`;
|
||
option.selected = true;
|
||
ssidSelect.insertBefore(option, ssidSelect.options[1]); // Nach "-- SSID auswählen --"
|
||
}
|
||
|
||
// "Erneut scannen" Option
|
||
const rescanOption = document.createElement('option');
|
||
rescanOption.value = '__rescan__';
|
||
rescanOption.text = '↻ Erneut scannen';
|
||
ssidSelect.add(rescanOption);
|
||
|
||
// "Manuell" Option als letzten Eintrag
|
||
const manualOption = document.createElement('option');
|
||
manualOption.value = '__manual__';
|
||
manualOption.text = '✎ Manuell eingeben...';
|
||
ssidSelect.add(manualOption);
|
||
|
||
ssidSelect.disabled = false;
|
||
wifiScanned = true;
|
||
|
||
console.log('✓ WiFi-Scan abgeschlossen: ' + (data.networks ? data.networks.length : 0) + ' Netzwerke gefunden');
|
||
})
|
||
.catch(error => {
|
||
console.error('WiFi-Scan Fehler:', error);
|
||
ssidSelect.innerHTML = '<option value="">✗ Scan fehlgeschlagen</option>';
|
||
|
||
// "Erneut scannen" und "Manuell" Option auch bei Fehler
|
||
const rescanOption = document.createElement('option');
|
||
rescanOption.value = '__rescan__';
|
||
rescanOption.text = '↻ Erneut scannen';
|
||
ssidSelect.add(rescanOption);
|
||
|
||
const manualOption = document.createElement('option');
|
||
manualOption.value = '__manual__';
|
||
manualOption.text = '✎ Manuell eingeben...';
|
||
ssidSelect.add(manualOption);
|
||
|
||
ssidSelect.disabled = false;
|
||
});
|
||
}
|
||
|
||
function handleSsidSelection() {
|
||
const ssidSelect = document.getElementById('staSsid');
|
||
const manualInput = document.getElementById('staSsidManual');
|
||
|
||
if (ssidSelect.value === '__rescan__') {
|
||
// Erneut scannen
|
||
scanWiFi(savedSsidFromConfig);
|
||
} else if (ssidSelect.value === '__manual__') {
|
||
// Manuelle Eingabe anzeigen
|
||
manualInput.style.display = 'block';
|
||
manualInput.focus();
|
||
} else {
|
||
// Manuelle Eingabe ausblenden
|
||
manualInput.style.display = 'none';
|
||
manualInput.value = '';
|
||
}
|
||
}
|
||
|
||
function togglePasswordVisibility() {
|
||
const passwordInput = document.getElementById('staPassword');
|
||
const toggleIcon = document.getElementById('passwordToggleIcon');
|
||
const svgUse = toggleIcon.querySelector('use');
|
||
|
||
if (passwordInput.type === 'password') {
|
||
// Passwort anzeigen (von Sternen zu Klartext)
|
||
passwordInput.type = 'text';
|
||
toggleIcon.title = 'Passwort verbergen';
|
||
svgUse.setAttribute('href', '#icon-eye-visible');
|
||
} else {
|
||
// Passwort verbergen (zu Sternen)
|
||
passwordInput.type = 'password';
|
||
toggleIcon.title = 'Passwort anzeigen';
|
||
svgUse.setAttribute('href', '#icon-eye-hidden');
|
||
}
|
||
}
|
||
|
||
function toggleStaticIPFields() {
|
||
const isDhcp = document.getElementById('staDhcp').checked;
|
||
document.getElementById('staticIPConfig').style.display = isDhcp ? 'none' : 'block';
|
||
}
|
||
|
||
function saveWiFiConfig() {
|
||
const params = new URLSearchParams();
|
||
|
||
// SSID: Wenn "Manuell" gewählt, dann aus manuellem Eingabefeld, sonst aus Dropdown
|
||
const ssidSelect = document.getElementById('staSsid');
|
||
const ssidManual = document.getElementById('staSsidManual');
|
||
let ssidValue = '';
|
||
|
||
if (ssidSelect.value === '__manual__') {
|
||
ssidValue = ssidManual.value;
|
||
} else if (ssidSelect.value === '__rescan__' || ssidSelect.value === '') {
|
||
// Ignoriere "Erneut scannen" und leere Auswahl
|
||
alert('⚠ Bitte wählen Sie eine SSID aus!');
|
||
return;
|
||
} else {
|
||
ssidValue = ssidSelect.value;
|
||
}
|
||
|
||
params.append('staEnabled', document.getElementById('staEnabled').checked ? '1' : '0');
|
||
params.append('staSsid', ssidValue);
|
||
params.append('staPassword', document.getElementById('staPassword').value);
|
||
params.append('staDhcp', document.getElementById('staDhcp').checked ? '1' : '0');
|
||
params.append('staIP', document.getElementById('staIP').value);
|
||
params.append('staGateway', document.getElementById('staGateway').value);
|
||
params.append('staSubnet', document.getElementById('staSubnet').value);
|
||
params.append('staDNS', document.getElementById('staDNS').value);
|
||
params.append('ntpEnabled', document.getElementById('ntpEnabled').checked ? '1' : '0');
|
||
params.append('ntpServer', document.getElementById('ntpServer').value);
|
||
|
||
fetch('/wifi/save?' + params.toString())
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
// Bestätigungsdialog für Neustart
|
||
if (confirm('✓ WiFi-Konfiguration gespeichert!\n\nMöchten Sie das Gerät jetzt neu starten?\n\n(Ohne Neustart werden die WiFi-Einstellungen nicht aktiv)')) {
|
||
// Neustart durchführen
|
||
alert('Gerät wird neu gestartet...\n\nBitte warten Sie ca. 10 Sekunden.');
|
||
fetch('/restart')
|
||
.then(() => {
|
||
// Seite nach 10 Sekunden neu laden
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 10000);
|
||
})
|
||
.catch(error => {
|
||
console.log('Neustart ausgelöst (Verbindung getrennt)');
|
||
// Seite nach 10 Sekunden neu laden
|
||
setTimeout(() => {
|
||
location.reload();
|
||
}, 10000);
|
||
});
|
||
} else {
|
||
loadWiFiConfig(); // Reload ohne Neustart
|
||
}
|
||
})
|
||
.catch(error => {
|
||
alert('❌ Fehler beim Speichern: ' + error);
|
||
});
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// AUTO-BRIGHTNESS FUNCTIONS
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
let inputHasFocus = false;
|
||
let lastLivePreviewTime = 0;
|
||
let currentADCValue = 0;
|
||
let userIsAdjusting = false;
|
||
let adjustingTimeout = null;
|
||
let brightnessectionOpen = false;
|
||
|
||
function setUserAdjusting() {
|
||
userIsAdjusting = true;
|
||
|
||
// Timeout zurücksetzen
|
||
if (adjustingTimeout) {
|
||
clearTimeout(adjustingTimeout);
|
||
}
|
||
|
||
// Nach 5 Sekunden ohne Änderung wieder erlauben, dass Werte aktualisiert werden
|
||
adjustingTimeout = setTimeout(() => {
|
||
userIsAdjusting = false;
|
||
console.log('Auto-Update wieder aktiviert');
|
||
}, 5000);
|
||
}
|
||
|
||
function loadAutoBrightnessConfig() {
|
||
fetch('/autobrightness/config')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
// Config laden
|
||
document.getElementById('autoBrightnessEnabled').checked = data.enabled;
|
||
|
||
// Nur aktualisieren, wenn:
|
||
// - Benutzer nicht gerade Werte anpasst
|
||
// - Helligkeits-Sektion nicht geöffnet ist
|
||
if (!inputHasFocus && !userIsAdjusting && !brightnessectionOpen) {
|
||
document.getElementById('autoBrightnessMinADC').value = data.minADC || 200;
|
||
document.getElementById('autoBrightnessMinADCValue').value = data.minADC || 200;
|
||
document.getElementById('autoBrightnessMaxADC').value = data.maxADC || 820;
|
||
document.getElementById('autoBrightnessMaxADCValue').value = data.maxADC || 820;
|
||
|
||
// Helligkeits-Bereich laden
|
||
document.getElementById('autoBrightnessMin').value = data.minBrightness || 0;
|
||
document.getElementById('autoBrightnessMinValue').value = data.minBrightness || 0;
|
||
document.getElementById('autoBrightnessMax').value = data.maxBrightness || 80;
|
||
document.getElementById('autoBrightnessMaxValue').value = data.maxBrightness || 80;
|
||
|
||
// Spannungen und Prozente aktualisieren
|
||
updateADCPreview();
|
||
updateBrightnessDisplay();
|
||
}
|
||
|
||
// Aktuellen ADC-Wert als Prozent anzeigen
|
||
currentADCValue = data.currentADC || 0;
|
||
const adcPercent = Math.round(currentADCValue / 1023 * 100);
|
||
document.getElementById('currentADCPercent').innerHTML = currentADCValue > 0 ? adcPercent : '---';
|
||
|
||
console.log('Auto-Brightness Config loaded:', data);
|
||
})
|
||
.catch(error => {
|
||
console.error('Auto-Brightness Config Error:', error);
|
||
});
|
||
}
|
||
|
||
function saveAutoBrightness() {
|
||
const minADC = parseInt(document.getElementById('autoBrightnessMinADC').value);
|
||
const maxADC = parseInt(document.getElementById('autoBrightnessMaxADC').value);
|
||
const enabled = document.getElementById('autoBrightnessEnabled').checked;
|
||
|
||
// Validierung: Mindestdifferenz erforderlich
|
||
const adcRange = maxADC - minADC;
|
||
const MIN_RANGE = 20;
|
||
|
||
if (enabled && adcRange < MIN_RANGE) {
|
||
alert('⚠️ Automatische Helligkeitsregelung kann nicht aktiviert werden!\n\n' +
|
||
'Der Bereich (Max - Min) ist zu klein: ' + adcRange + '\n' +
|
||
'Mindestens erforderlich: ' + MIN_RANGE + '\n\n' +
|
||
'Bitte passen Sie die Min/Max-Werte an oder deaktivieren Sie die automatische Regelung.');
|
||
|
||
// Checkbox automatisch deaktivieren
|
||
document.getElementById('autoBrightnessEnabled').checked = false;
|
||
return;
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
params.append('enabled', document.getElementById('autoBrightnessEnabled').checked ? '1' : '0');
|
||
params.append('minADC', minADC);
|
||
params.append('maxADC', maxADC);
|
||
params.append('minBrightness', parseInt(document.getElementById('autoBrightnessMin').value));
|
||
params.append('maxBrightness', parseInt(document.getElementById('autoBrightnessMax').value));
|
||
|
||
fetch('/autobrightness/save?' + params.toString())
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✓ ' + data);
|
||
|
||
// Nach Speichern: Auto-Update wieder erlauben
|
||
userIsAdjusting = false;
|
||
if (adjustingTimeout) {
|
||
clearTimeout(adjustingTimeout);
|
||
adjustingTimeout = null;
|
||
}
|
||
|
||
loadAutoBrightnessConfig(); // Reload
|
||
})
|
||
.catch(error => {
|
||
alert('❌ Fehler beim Speichern: ' + error);
|
||
});
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// BRIGHTNESS VALIDIERUNG & ANZEIGE
|
||
// ════════════════════════════════════════════════════════════════
|
||
function updateBrightnessDisplay() {
|
||
// Diese Funktion wird nicht mehr benötigt, da die %-Anzeigen statisch sind
|
||
// Wird aus Kompatibilität beibehalten, aber macht nichts mehr
|
||
}
|
||
|
||
function validateBrightnessMin() {
|
||
const minInput = document.getElementById('autoBrightnessMin');
|
||
const maxInput = document.getElementById('autoBrightnessMax');
|
||
const minVal = parseInt(minInput.value);
|
||
const maxVal = parseInt(maxInput.value);
|
||
|
||
// Wenn Min >= Max: Max automatisch erhöhen
|
||
if (minVal >= maxVal) {
|
||
const newMax = Math.min(80, minVal + 5); // Max 80
|
||
maxInput.value = newMax;
|
||
document.getElementById('autoBrightnessMaxValue').value = newMax;
|
||
updateBrightnessDisplay();
|
||
}
|
||
}
|
||
|
||
function validateBrightnessMax() {
|
||
const minInput = document.getElementById('autoBrightnessMin');
|
||
const maxInput = document.getElementById('autoBrightnessMax');
|
||
const minVal = parseInt(minInput.value);
|
||
const maxVal = parseInt(maxInput.value);
|
||
|
||
// Max darf nicht > 80 sein
|
||
if (maxVal > 80) {
|
||
maxInput.value = 80;
|
||
document.getElementById('autoBrightnessMaxValue').value = 80;
|
||
}
|
||
|
||
// Wenn Max <= Min: Min automatisch verringern
|
||
if (maxVal <= minVal && maxVal > 0) {
|
||
const newMin = Math.max(0, maxVal - 5);
|
||
minInput.value = newMin;
|
||
document.getElementById('autoBrightnessMinValue').value = newMin;
|
||
}
|
||
updateBrightnessDisplay();
|
||
}
|
||
|
||
function validateADCMin() {
|
||
const minInput = document.getElementById('autoBrightnessMinADC');
|
||
const maxInput = document.getElementById('autoBrightnessMaxADC');
|
||
const minVal = parseInt(minInput.value);
|
||
const maxVal = parseInt(maxInput.value);
|
||
|
||
// Wenn Min >= Max: Max automatisch erhöhen
|
||
if (minVal >= maxVal) {
|
||
const newMax = Math.min(1023, minVal + 20);
|
||
maxInput.value = newMax;
|
||
document.getElementById('autoBrightnessMaxADCValue').value = newMax;
|
||
}
|
||
}
|
||
|
||
function validateADCMax() {
|
||
const minInput = document.getElementById('autoBrightnessMinADC');
|
||
const maxInput = document.getElementById('autoBrightnessMaxADC');
|
||
const minVal = parseInt(minInput.value);
|
||
const maxVal = parseInt(maxInput.value);
|
||
|
||
// Wenn Max <= Min: Min automatisch verringern
|
||
if (maxVal <= minVal && maxVal > 0) {
|
||
const newMin = Math.max(0, maxVal - 20);
|
||
minInput.value = newMin;
|
||
document.getElementById('autoBrightnessMinADCValue').value = newMin;
|
||
}
|
||
}
|
||
|
||
function updateADCPreview() {
|
||
// Slider und Number-Input synchronisieren
|
||
const minSlider = document.getElementById('autoBrightnessMinADC');
|
||
const minValue = document.getElementById('autoBrightnessMinADCValue');
|
||
const maxSlider = document.getElementById('autoBrightnessMaxADC');
|
||
const maxValue = document.getElementById('autoBrightnessMaxADCValue');
|
||
|
||
// Prozent berechnen (0-1023 = 0-100%)
|
||
const minPercent = Math.round(minSlider.value / 1023 * 100);
|
||
const maxPercent = Math.round(maxSlider.value / 1023 * 100);
|
||
|
||
minValue.value = minPercent;
|
||
maxValue.value = maxPercent;
|
||
|
||
// Bereich-Validierung anzeigen (nur Warnung, keine Auto-Deaktivierung)
|
||
const adcRange = parseInt(maxSlider.value) - parseInt(minSlider.value);
|
||
const MIN_RANGE = 20;
|
||
const rangeWarning = document.getElementById('rangeWarning');
|
||
|
||
if (adcRange < MIN_RANGE) {
|
||
// Nur Warnung anzeigen, Checkbox NICHT deaktivieren
|
||
if (rangeWarning) {
|
||
rangeWarning.style.display = 'block';
|
||
rangeWarning.innerHTML = '⚠️ Bereich zu klein (' + adcRange + ' < ' + MIN_RANGE + '). Automatische Regelung kann nicht aktiviert werden.';
|
||
}
|
||
} else {
|
||
// Warnung ausblenden
|
||
if (rangeWarning) {
|
||
rangeWarning.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
function autoSetMin() {
|
||
if (currentADCValue > 0) {
|
||
// Verhindere automatisches Überschreiben
|
||
setUserAdjusting();
|
||
|
||
document.getElementById('autoBrightnessMinADC').value = currentADCValue;
|
||
document.getElementById('autoBrightnessMinADCValue').value = currentADCValue;
|
||
updateADCPreview();
|
||
sendLivePreview();
|
||
|
||
// Visuelles Feedback
|
||
const btn = event.target;
|
||
const originalText = btn.innerHTML;
|
||
btn.innerHTML = '✓ Gesetzt';
|
||
btn.style.background = '#4caf50';
|
||
setTimeout(() => {
|
||
btn.innerHTML = originalText;
|
||
btn.style.background = '#999';
|
||
}, 1000);
|
||
|
||
console.log('Min-Wert gesetzt auf:', currentADCValue);
|
||
} else {
|
||
alert('⚠ Kein aktueller Wert verfügbar. Bitte warten Sie auf die nächste Messung.');
|
||
}
|
||
}
|
||
|
||
function autoSetMax() {
|
||
if (currentADCValue > 0) {
|
||
// Verhindere automatisches Überschreiben
|
||
setUserAdjusting();
|
||
|
||
document.getElementById('autoBrightnessMaxADC').value = currentADCValue;
|
||
document.getElementById('autoBrightnessMaxADCValue').value = currentADCValue;
|
||
updateADCPreview();
|
||
sendLivePreview();
|
||
|
||
// Visuelles Feedback
|
||
const btn = event.target;
|
||
const originalText = btn.innerHTML;
|
||
btn.innerHTML = '✓ Gesetzt';
|
||
btn.style.background = '#4caf50';
|
||
setTimeout(() => {
|
||
btn.innerHTML = originalText;
|
||
btn.style.background = '#999';
|
||
}, 1000);
|
||
|
||
console.log('Max-Wert gesetzt auf:', currentADCValue);
|
||
} else {
|
||
alert('⚠ Kein aktueller Wert verfügbar. Bitte warten Sie auf die nächste Messung.');
|
||
}
|
||
}
|
||
|
||
function sendLivePreview() {
|
||
// Throttle: Nur alle 500ms senden für schnelleres Feedback
|
||
const now = Date.now();
|
||
if (now - lastLivePreviewTime < 500) {
|
||
return;
|
||
}
|
||
lastLivePreviewTime = now;
|
||
|
||
// Berechne Helligkeit basierend auf eingestellten Werten
|
||
const minADC = parseInt(document.getElementById('autoBrightnessMinADC').value);
|
||
const maxADC = parseInt(document.getElementById('autoBrightnessMaxADC').value);
|
||
|
||
// Validierung
|
||
if (maxADC <= minADC) {
|
||
console.error('MaxADC muss größer als MinADC sein!');
|
||
return;
|
||
}
|
||
|
||
// Simuliere eine mittlere Helligkeit basierend auf den eingestellten Werten
|
||
// Verwende die Mitte des Bereichs für die Vorschau
|
||
let simulatedADC = Math.round((minADC + maxADC) / 2);
|
||
|
||
// Wenn currentADCValue verfügbar ist, verwende ihn stattdessen
|
||
let adc = (currentADCValue > 0) ? currentADCValue : simulatedADC;
|
||
if (adc < minADC) adc = minADC;
|
||
if (adc > maxADC) adc = maxADC;
|
||
|
||
// Helligkeit berechnen: geringer ADC = geringe Helligkeit
|
||
// Wenn minADC sehr klein ist (< 10), können LEDs komplett aus sein (für Schlafzimmer)
|
||
// Sonst Minimum 5% damit man noch was sieht
|
||
const minBrightness = (minADC < 10) ? 0 : 10;
|
||
const maxBrightness = 204; // 80% Maximum
|
||
let brightness = minBrightness + Math.round((adc - minADC) * (maxBrightness - minBrightness) / (maxADC - minADC));
|
||
|
||
// Sicherheitsprüfung
|
||
if (brightness < 0) brightness = 0;
|
||
if (brightness > maxBrightness) brightness = maxBrightness;
|
||
|
||
// Aktuelle Farben aus den Farbeinstellungen holen (falls vorhanden)
|
||
let r = 255, g = 255, b = 255;
|
||
const normalColorInput = document.getElementById('normalColor');
|
||
if (normalColorInput) {
|
||
const rgb = hexToRgb(normalColorInput.value);
|
||
if (rgb) {
|
||
r = rgb.r;
|
||
g = rgb.g;
|
||
b = rgb.b;
|
||
}
|
||
}
|
||
|
||
// Sende Vorschau-Befehl an ESP (alle LEDs)
|
||
fetch('/ledtest?mode=all&r=' + r + '&g=' + g + '&b=' + b + '&brightness=' + brightness)
|
||
.then(response => {
|
||
console.log('Live-Vorschau: ADC=' + adc + ', Helligkeit=' + brightness + ' (' + Math.round(brightness/204*100) + '%), RGB=(' + r + ',' + g + ',' + b + ')');
|
||
})
|
||
.catch(error => {
|
||
console.error('Live-Vorschau Fehler:', error);
|
||
});
|
||
}
|
||
|
||
window.addEventListener('load', function() {
|
||
updatePreview();
|
||
updateBrowserTime();
|
||
fetchEspTime();
|
||
loadCharsoap();
|
||
loadSpecialWords();
|
||
loadMinuteLeds();
|
||
loadOTAInfo();
|
||
loadWiFiConfig();
|
||
loadAutoBrightnessConfig();
|
||
|
||
setInterval(updateBrowserTime, 1000);
|
||
setInterval(fetchEspTime, 5000);
|
||
setInterval(loadOTAInfo, 60000);
|
||
setInterval(loadWiFiConfig, 30000);
|
||
setInterval(loadAutoBrightnessConfig, 2000); // Auto-Brightness alle 2s aktualisieren
|
||
|
||
// Event Listener für DHCP-Checkbox
|
||
document.getElementById('staDhcp').addEventListener('change', toggleStaticIPFields);
|
||
|
||
const now = new Date();
|
||
const offset = now.getTimezoneOffset() * 60000;
|
||
const localISOTime = (new Date(now - offset)).toISOString().slice(0, 16);
|
||
document.getElementById('manualDateTime').value = localISOTime;
|
||
|
||
// Dark Mode: Gespeicherten Modus laden
|
||
initTheme();
|
||
});
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// DARK MODE TOGGLE
|
||
// ════════════════════════════════════════════════════════════════
|
||
|
||
function initTheme() {
|
||
// Gespeicherten Modus aus localStorage laden
|
||
const savedTheme = localStorage.getItem('theme');
|
||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||
|
||
// Dark Mode aktivieren wenn:
|
||
// 1. Explizit gespeichert als 'dark' ODER
|
||
// 2. Nichts gespeichert UND System bevorzugt Dark Mode
|
||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||
document.body.classList.add('dark-mode');
|
||
updateThemeIcon(true);
|
||
} else {
|
||
updateThemeIcon(false);
|
||
}
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const isDark = document.body.classList.toggle('dark-mode');
|
||
|
||
// Im localStorage speichern
|
||
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||
|
||
// Icon aktualisieren
|
||
updateThemeIcon(isDark);
|
||
}
|
||
|
||
function updateThemeIcon(isDark) {
|
||
const sunIcon = document.getElementById('theme-icon-sun');
|
||
const moonIcon = document.getElementById('theme-icon-moon');
|
||
|
||
if (isDark) {
|
||
// Dark Mode aktiv → Sonne anzeigen (zum Umschalten zu Hell)
|
||
sunIcon.style.display = 'block';
|
||
moonIcon.style.display = 'none';
|
||
} else {
|
||
// Light Mode aktiv → Mond anzeigen (zum Umschalten zu Dark)
|
||
sunIcon.style.display = 'none';
|
||
moonIcon.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// SPECIAL WORD FUNKTIONEN
|
||
// ════════════════════════════════════════════════════════════════
|
||
function loadSpecialWords() {
|
||
fetch('/specialwords/get')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
for (let i = 0; i < 3; i++) {
|
||
document.getElementById('specialword' + i).value = data.words[i] || '';
|
||
}
|
||
// Intervall setzen
|
||
if (data.interval) {
|
||
document.getElementById('specialwordinterval').value = data.interval;
|
||
}
|
||
// Spezialwörter unter der Überschrift anzeigen
|
||
const displayElement = document.getElementById('specialWordsDisplay');
|
||
const nonEmptyWords = data.words.filter(word => word && word.trim() !== '');
|
||
if (nonEmptyWords.length > 0) {
|
||
displayElement.textContent = nonEmptyWords.join(' ');
|
||
} else {
|
||
displayElement.textContent = '';
|
||
}
|
||
})
|
||
.catch(error => console.error('Fehler beim Laden der Spezialwörter:', error));
|
||
}
|
||
|
||
function saveSpecialWords() {
|
||
const words = [];
|
||
for (let i = 0; i < 3; i++) {
|
||
const word = document.getElementById('specialword' + i).value.trim();
|
||
if (word.length > 11) {
|
||
alert('❌ Wort ' + (i+1) + ' zu lang (max 11 Zeichen)!');
|
||
return;
|
||
}
|
||
words.push(word);
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
for (let i = 0; i < 3; i++) {
|
||
params.append('word' + i, words[i]);
|
||
}
|
||
|
||
// Intervall hinzufügen
|
||
const interval = document.getElementById('specialwordinterval').value;
|
||
params.append('interval', interval);
|
||
|
||
fetch('/specialwords/save?' + params.toString())
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Spezialwörter gespeichert!');
|
||
loadSpecialWords(); // Anzeige aktualisieren
|
||
})
|
||
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
|
||
}
|
||
|
||
function resetSpecialWords() {
|
||
if (confirm('Spezialwörter auf Standard zurücksetzen?')) {
|
||
fetch('/specialwords/reset')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Zurückgesetzt!');
|
||
loadSpecialWords();
|
||
})
|
||
.catch(error => alert('❌ Fehler: ' + error));
|
||
}
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════
|
||
// MINUTE LEDS FUNKTIONEN
|
||
// ════════════════════════════════════════════════════════════════
|
||
function loadMinuteLeds() {
|
||
fetch('/minuteleds/get')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
for (let i = 0; i < 4; i++) {
|
||
document.getElementById('minuteled' + i).value = data.leds[i];
|
||
}
|
||
})
|
||
.catch(error => console.error('Fehler beim Laden der Minuten-LEDs:', error));
|
||
}
|
||
|
||
function saveMinuteLeds() {
|
||
const leds = [];
|
||
for (let i = 0; i < 4; i++) {
|
||
const value = parseInt(document.getElementById('minuteled' + i).value);
|
||
if (isNaN(value) || value < 0 || value > 120) {
|
||
alert('❌ LED ' + (i+1) + ' ungültig (0-120)!');
|
||
return;
|
||
}
|
||
leds.push(value);
|
||
}
|
||
|
||
const params = new URLSearchParams();
|
||
for (let i = 0; i < 4; i++) {
|
||
params.append('led' + i, leds[i]);
|
||
}
|
||
|
||
fetch('/minuteleds/save?' + params.toString())
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Minuten-LEDs gespeichert!');
|
||
})
|
||
.catch(error => alert('❌ Fehler beim Speichern: ' + error));
|
||
}
|
||
|
||
function resetMinuteLeds() {
|
||
if (confirm('Minuten-LEDs auf Standard zurücksetzen?')) {
|
||
fetch('/minuteleds/reset')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
alert('✅ Zurückgesetzt!');
|
||
loadMinuteLeds();
|
||
})
|
||
.catch(error => alert('❌ Fehler: ' + error));
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |