Compare commits

..
4 Commits
Author SHA1 Message Date
toptah 2f23c784ee Blinken der LEDs und des WebInterface in Sync 2026-03-12 21:22:12 +01:00
toptah c39fd04b07 Anpassung
- Readme mit Beschreibug der Software
- style.css mit modernerem look und HOBBYHIMMEL Farben
2026-03-12 21:09:02 +01:00
toptah 1bf8c6545f Hinzufügen HTML Seiten zur Konfiguration der Ports und Darstellung des jeweiligen Status 2026-03-09 22:59:57 +01:00
toptah 2348fc59c7 Hinzufügen WebServer und AP zur SSID Konfiguration 2026-03-09 20:05:35 +01:00
20 changed files with 2264 additions and 12 deletions
+50
View File
@@ -0,0 +1,50 @@
---
# Workspace instructions for the KeyPatch ESP8266 project
This project is a PlatformIO/Arduino sketch for an ESP8266 (Wemos D1 mini) that
implements a captive-portal Wi‑Fi configurator, filesystem manager, and NeoPixel
handler. The code is organised into multiple `.ino` tabs under `src/` and uses
several libraries (LittleFS, ESP8266WebServer, DNSServer, Adafruit NeoPixel, PCF8575,
etc.).
## Building and flashing
- Build with PlatformIO via the task or from the command line:
```powershell
& "C:\Users\User\.platformio\penv\Scripts\platformio.exe" run
```
or `pio run`/`platformio run` from a shell that has the `platformio` command.
- Upload to the board using `platformio run -t upload` or through the built-in VS Code task.
- If you see `exit code 1` from the upload task, the problem is generally a connection issue to the
device, not a compilation error; the project compiles cleanly with the current sources.
## Code conventions
- All Arduino headers are included in the main tab (`KeyPatch.ino`). Helper tabs
such as `Connect.ino`, `LittleFS.ino`, etc. depend on those includes being present.
- `setup()` must call `connectWifi()` and `setupFS()` as indicated in the comments.
- Serial debugging is used heavily; the baud rate is 115200 by default.
- When the soft‑AP named `EspConfig` is started, the IP address is printed to the
serial monitor (e.g. `AP-IP-Adresse: 172.217.28.1`).
## Common tasks
1. Change Wi‑Fi parameters in `Connect.ino` or use the captive portal.
2. Upload `fs.html` via the web interface to manage LittleFS content.
3. Edit hardware configurations under `hardware/` (FreeCAD files).
## Troubleshooting
- Compilation errors are rare; if you encounter them, run the verbose build
(`platformio run -v`) and inspect `compile.log` for `error:` messages.
- LittleFS operations require `LittleFS.begin()` to succeed; the helper tab
`LittleFS.ino` includes debug prints.
## Personalisation
This file is intended to help any contributor or future self understand the
project layout, build commands, and where to look for the various features.
Feel free to update it with new instructions as the sketch evolves.
---
+132
View File
@@ -57,6 +57,138 @@ Codierung der Ports:
| 23 | 5 | 6 | weiß-blau | grün | 0x22-P06 |
| 24 | 5 | 7 | weiß-blau | weiß-braun | 0x22-P07 |
---
## Software-Features
### 🌐 Webserver & Benutzeroberfläche
Das System stellt einen vollständig konfigurierbaren Webserver bereit mit folgenden Seiten:
- **Dashboard** (`/`) — Übersicht und Kontrolzentrum
- **Admin-Panel** (`/admin`) — Systeminfo und Verwaltungsfunktionen
- **Dateisystem-Manager** (`/fs.html`) — Upload, Download und Verwaltung von Dateien auf der SD
- **Port-Konfiguration** (`/portconfig`) — Konfigurieren und Benennen der 16 RJ45-Ports
### 🔌 Port Management & LED-Anzeige
**Automatische Portüberwachung:**
- Echtzeit-Überwachung aller 16 Ports via PCF8575 IO-Expander
- Automatische Erkennung von korrekten/fehlerhaften Kabelverbindungen
- Polling-Zyklus alle ~50ms
**NeoPixel LED-Codierung:**
- **Grün**: Port-Zustand korrekt (Kabel richtig eingesteckt)
- **Rot (blinkend)**: Port-Zustand falsch (Kabel falsch oder nicht eingesteckt)
- **Aus**: Port ist deaktiviert
**Port-Konfiguration:**
- Individuelle Benennung jedes Ports möglich
- Aktivieren/Deaktivieren von Ports
- Globale Blinkfrequenz für fehlerhafte Ports einstellbar (in ms)
- Persistente Speicherung der Konfiguration in `portconfig.json`, inklusive Blinkintervall
### 📡 WiFi & Netzwerkverbindung
**Hauptmodi:**
1. **Station-Modus** — Verbindung zu bestehendem WiFi-Netzwerk
2. **Soft-AP Modus (Captive Portal)** — Fallback-Konfigurationsmodus mit SSID `EspConfig`
**Automatische Verbindungsverwaltung:**
- Timeout nach 30 Sekunden bei fehlgeschlagener Verbindung
- Automatischer Fallback zur Soft-AP (Captive Portal)
- Auto-Reconnect alle 5 Minuten
- LED-Rückmeldung: Blinken während Verbindungsaufbau, leuchten im AP-Modus
**Captive Portal:**
- Automatische Umleitung auf Konfigurationsseite
- Sicheres Speichern von WiFi-Credentials (XOR-Verschlüsselte Speicherung in `/wifi.dat`)
- Validierung: Passwort 8-64 Zeichen erforderlich
### 💾 Dateisystem (LittleFS)
**Funktionen:**
- Upload von Dateien (Drag & Drop, Mehrfach)
- Löschen von Dateien und Ordnern (rekursiv)
- Erstellen neuer Ordner
- Speicherübersicht (genutzt/verfügbar/gesamt)
- Formatierung des gesamten Filesystems möglich
**Sortierung:**
- Nach Dateiname (A-Z)
- Nach Dateigröße
**Vordefinierte HTML-Assets:**
Alle HTML/CSS-Dateien sind ins Filesystem eingebunden und können über die Web-UI verwaltet werden:
- `index.html` — Dashboard
- `admin.html` — Admin-Panel
- `fs.html` — Dateisystem-Manager
- `portconfig.html` — Port-Konfiguration
- `style.css` — Styling
### ⚙️ Admin & System-Funktionen
**Systemübersicht:**
- Live-Laufzeit (Tage, Stunden, Minuten, Sekunden)
- WiFi-Signalstärke (RSSI in dBm)
- Heap-Speicher und Fragmentierung
- Flash-Speicher (Größe, Mode, Speed)
- CPU-Frequenz
- Reset-Grund
- Sketch-Build-Zeit
- ESP Core und SDK Version
**Systemfunktionen:**
- **WiFi-Reconnect** — Manuelle Neuverbindung zum Netzwerk
- **ESP-Restart** — Neustart des Gerätes
- **OTA-Updates** — Wireless Sketch-Updates über Arduino IDE oder PlatformIO
### 🔗 REST-API Endpoints
**Admin-Informationen:**
- `GET /admin/renew` — Laufzeit und WiFi-Signal
- `GET /admin/once` — Detaillierte Systeminfo
**Port-Verwaltung:**
- `GET /portconfig/data` — Konfiguration aller 16 Ports (JSON)
- `POST /portconfig` — Speichern von Port-Namen und Enable-Status
- `GET /status/data` — Live-Status aller Ports (Name, Enable, aktueller Zustand)
**Dateisystem:**
- `POST /upload` — Datei-Upload
- `GET /format` — Filesystem formatieren
**Netzwerk:**
- `GET /reconnect` — WiFi-Reconnect triggern
---
## Technische Architektur
### Hardware-Kommunikation
**I2C-Bus (Datenleitung D1/D2):**
- PCF8575 IO-Expander @ Adresse 0x21
- Boot-Scan aller I2C-Adressen
**GPIO/Schnittstellen:**
- D6 — NeoPixel DIN (16 RGB-LEDs, 800 kHz)
- D1 — I2C SCL (mit 10kΩ PullUp)
- D2 — I2C SDA (mit 10kΩ PullUp)
**Serielle Schnittstelle:**
- Baudrate: 115200
- Umfangreiches Debug-Output für Troubleshooting
### Code-Organisation
Das Projekt ist in 7 Arduino-Tabs organisiert:
- `KeyPatch.ino` — Hauptsketch und Setup
- `Connect.ino` — WiFi-Management und Verbindungslogik
- `Webserver.ino` — HTTP-Endpoints und Web-UI
- `Admin.ino` — Adminpanel und Systeminfo
- `LittleFS.ino` — Dateisystem-Verwaltung
- `NeoPixelHandler.ino` — LED-Steuerung und Farb-Codierung
- `Config.ino` — Port-Konfiguration und Persistierung
---
BIN
View File
Binary file not shown.
+146
View File
@@ -0,0 +1,146 @@
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>ESP8266 Admin</title>
<script>
addEventListener('load', () => {
renew(), once();
let output = document.querySelector('#note');
let btn = document.querySelectorAll('button');
let span = document.querySelectorAll('#right span');
btn[0].addEventListener('click', () => {
location = '/fs.html';
});
btn[1].addEventListener('click', () => {
location = '/';
});
btn[2].addEventListener('click', () => {
location = '/portconfig';
});
btn[3].addEventListener('click', check.bind(this, document.querySelector('input')));
btn[4].addEventListener('click', re.bind(this, 'reconnect'));
btn[5].addEventListener('click', () => {
if (confirm('Bist du sicher!')) re('restart');
});
async function once(val = '',arg) {
try {
let resp = await fetch('/admin/once', { method: 'POST', body: val});
let obj = await resp.json();
output.innerHTML = '';
output.classList.remove('note');
document.querySelector('form').reset();
if (val.length == 0) myIv = setInterval(renew, 1000);
if (arg == 'reconnect') re(arg);
document.getElementById('file').innerHTML = obj['File'];
document.getElementById('build').innerHTML = obj['Build'];
document.getElementById('size').innerHTML = obj['SketchSize'];
document.getElementById('space').innerHTML = obj['SketchSpace'];
document.getElementById('ip').innerHTML = obj['LocalIP'];
document.getElementById('hostname').innerHTML = obj['Hostname'];
document.getElementById('ssid').innerHTML = obj['SSID'];
document.getElementById('gateway').innerHTML = obj['GatewayIP'];
document.getElementById('channel').innerHTML = obj['Channel'];
document.getElementById('mac').innerHTML = obj['MacAddress'];
document.getElementById('subnet').innerHTML = obj['SubnetMask'];
document.getElementById('bssid').innerHTML = obj['BSSID'];
document.getElementById('clientip').innerHTML = obj['ClientIP'];
document.getElementById('dnsip').innerHTML = obj['DnsIP'];
document.getElementById('reset').innerHTML = obj['ResetReason'];
document.getElementById('cpu').innerHTML = obj['CpuFreqMHz'] + " MHz";
document.getElementById('heap').innerHTML = obj['FreeHeap'];
document.getElementById('frag').innerHTML = obj['HeapFrag'] + "%";
document.getElementById('flashsize').innerHTML = obj['ChipSize'];
document.getElementById('flashspeed').innerHTML = obj['ChipSpeed'] + " MHz";
document.getElementById('flashmode').innerHTML = obj['ChipMode'];
document.getElementById('ide').innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
document.getElementById('core').innerHTML = obj['CoreVersion'].replace(/_/g,'.');
document.getElementById('sdk').innerHTML = obj['SdkVersion'];
} catch(err) {
re();
}
}
async function renew() {
const resp = await fetch('admin/renew');
const array = await resp.json();
document.getElementById('runtime').innerHTML = array[0];
document.getElementById('rssi').innerHTML = array[1];
document.getElementById('adc').innerHTML = array[2];
}
function check(inObj) {
!inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
}
function re(arg = '') {
clearInterval(myIv);
fetch(arg);
output.classList.add('note');
if (arg == 'restart') {
output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 15 Sekunden neu geladen.';
setTimeout(once, 15000);
}
else if (arg == 'reconnect'){
output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 10 Sekunden neu geladen.';
setTimeout(once, 10000);
}
else {
output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
setTimeout(once, 3000);
}
}
});
</script>
</head>
<body>
<h1>ESP8266 Admin Page</h1>
<main>
<table>
<tr><td>Runtime ESP:</td><td><span id="runtime">0</span></td></tr>
<tr><td>WiFi RSSI:</td><td><div><span id="rssi"></span> dBm</div></td></tr>
<tr><td>ADC/VCC:</td><td><span id="adc">0</span></td></tr>
<tr><td>Sketch Name:</td><td><span id="file">?</span></td></tr>
<tr><td>Sketch Build:</td><td><span id="build">0</span></td></tr>
<tr><td>SketchSize:</td><td><span id="size">0</span></td></tr>
<tr><td>FreeSketchSpace:</td><td><span id="space">0</span></td></tr>
<tr><td>IPv4 Address:</td><td><span id="ip">0</span></td></tr>
<tr><td>Hostname:</td><td><span id="hostname">?</span></td></tr>
<tr><td>Connected to:</td><td><span id="ssid">?</span></td></tr>
<tr><td>Gateway IP:</td><td><span id="gateway">0</span></td></tr>
<tr><td>Channel:</td><td><span id="channel">0</span></td></tr>
<tr><td>MacAddress:</td><td><span id="mac">0</span></td></tr>
<tr><td>SubnetMask:</td><td><span id="subnet">0</span></td></tr>
<tr><td>BSSID:</td><td><span id="bssid">0</span></td></tr>
<tr><td>Client IP:</td><td><span id="clientip">0</span></td></tr>
<tr><td>DnsIP:</td><td><span id="dnsip">0</span></td></tr>
<tr><td>Reset Ground:</td><td><span id="reset">?</span></td></tr>
<tr><td>CPU Freq:</td><td><span id="cpu">0</span> MHz</td></tr>
<tr><td>FreeHeap:</td><td><span id="heap">0</span></td></tr>
<tr><td>Heap Fragmentation:</td><td><span id="frag">0</span>%</td></tr>
<tr><td>FlashSize:</td><td><span id="flashsize">0</span></td></tr>
<tr><td>FlashSpeed:</td><td><span id="flashspeed">0</span> MHz</td></tr>
<tr><td>FlashMode:</td><td><span id="flashmode">0</span></td></tr>
<tr><td>Arduino IDE Version:</td><td><span id="ide">0</span></td></tr>
<tr><td>Esp Core Version:</td><td><span id="core">0</span></td></tr>
<tr><td>SDK Version:</td><td><span id="sdk">0</span></td></tr>
</table>
</main>
<div>
<button>Filesystem</button>
<button>Startseite</button>
<button>Port Konfiguration</button>
</div>
<div id="note"></div>
<div>
<form>
<input placeholder="neuer Hostname" pattern="([A-Za-z0-9\-]{1,32})" title="Es dürfen nur Buchstaben (a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
<button type="button">Name Senden</button>
</form>
</div>
<div>
<button>WiFi Reconnect</button>
<button>ESP Restart</button>
</div>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Filesystem Manager</title>
<script>
document.addEventListener('DOMContentLoaded', () => {
list(JSON.parse(localStorage.getItem('sortBy')));
btn.addEventListener('click', () => {
if (!confirm(`Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault();
});
});
async function list(to){
let resp = await fetch(`?sort=${to}`);
let json = await resp.json();
let myList = document.querySelector('main'), noted = '';
myList.innerHTML = '<nav><input type="radio" id="/" name="group" checked="checked"><label for="/"> &#128193;</label><span id="cr">+&#128193;</nav></span><span id="si"></span>';
for (var i = 0; i < json.length - 1; i++) {
let dir = '', f = json[i].folder, n = json[i].name;
if (f != noted) {
noted = f;
dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> &#128193; ${f} <a href="?delete=/${f}">&#x1f5d1;&#xfe0f;</a></nav>`;
}
if (n != '') dir += `<li><a href="${f}/${n}">${n}</a><small> ${json[i].size}</small><a href="${f}/${n}"download="${n}"> Download</a> or<a href="?delete=${f}/${n}"> Delete</a>`;
myList.insertAdjacentHTML('beforeend', dir);
}
myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '&#9660;' : '&#9650;'} LittleFS</b> belegt ${json[i].usedBytes.replace(".00", "")} von ${json[i].totalBytes.replace(".00", "")}`);
var free = json[i].freeBytes;
cr.addEventListener('click', () => {
document.getElementById('no').classList.toggle('no');
});
so.addEventListener('click', () => {
list(to=++to%2);
localStorage.setItem('sortBy', JSON.stringify(to));
});
document.addEventListener('change', (e) => {
if (e.target.id == 'fs') {
for (var bytes = 0, i = 0; i < event.target.files.length; i++) bytes += event.target.files[i].size;
for (var output = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) output = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
if (bytes > free) {
si.innerHTML = `<li><b> ${output}</b><strong> Ungenügend Speicher frei</strong></li>`;
up.setAttribute('disabled', 'disabled');
}
else {
si.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
up.removeAttribute('disabled');
}
}
document.querySelectorAll(`input[type=radio]`).forEach(el => { if (el.checked) document.querySelector('form').setAttribute('action', '/upload?f=' + el.id)});
});
document.querySelectorAll('[href^="?delete=/"]').forEach(node => {
node.addEventListener('click', () => {
if (!confirm('Sicher!')) event.preventDefault();
});
});
}
</script>
</head>
<body>
<h2>ESP8266 Filesystem Manager</h2>
<form method="post" enctype="multipart/form-data" action="/upload?f=/">
<input id="fs" type="file" name="up[]" multiple>
<button id="up" disabled>Upload</button>
</form>
<form id="no" class="no" method="POST">
<input name="new" placeholder="Ordner Name" pattern="[^\x22\/%&\\:;]{0,31}[^\x22\/%&\\:;\s]{1}" title="Zeichen &#8220; % & / : ; \ sind nicht erlaubt." required="">
<button>Create</button>
</form>
<main></main>
<form action="/format" method="POST">
<button id="btn">Format LittleFS</button>
</form>
</body>
</html>
+91
View File
@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Port Status</title>
<style>
:root {
--blink-duration: 1s;
}
.port {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
display: inline-block;
width: 200px;
}
.disabled {
background-color: lightgray;
color: gray;
}
.ok {
background-color: lightgreen;
}
.missing {
background-color: red;
animation: blink var(--blink-duration) infinite;
}
@keyframes blink {
0%, 50% { background-color: red; }
51%, 100% { background-color: white; }
}
</style>
</head>
<body>
<h1>Port Status Übersicht</h1>
<div id="portsContainer"></div>
<br>
<button onclick="location.href='/portconfig.html'">Zur Konfiguration</button>
<button onclick="location.href='/admin.html'">Zur Admin Seite</button>
<script>
async function loadStatus() {
try {
const response = await fetch('/status/data');
const data = await response.json();
if (data.blink_interval !== undefined) {
updateBlinkDuration(data.blink_interval);
}
const container = document.getElementById('portsContainer');
container.innerHTML = '';
data.ports.forEach((port, index) => {
const div = document.createElement('div');
div.className = 'port';
let statusClass = '';
let statusText = '';
if (!port.enabled) {
statusClass = 'disabled';
statusText = 'disabled';
} else if (port.state === 0) {
statusClass = 'ok';
statusText = 'OK';
} else {
statusClass = 'missing';
statusText = 'Fehlt';
}
div.classList.add(statusClass);
div.innerHTML = `
<strong>Port ${index}: ${port.name}</strong><br>
Status: ${statusText}
`;
container.appendChild(div);
});
} catch (error) {
console.error('Fehler beim Laden des Status:', error);
}
}
// blink length updater must be defined before starting
async function updateBlinkDuration(blinkInterval) {
const period = blinkInterval * 2;
document.documentElement.style.setProperty('--blink-duration', period + 'ms');
}
loadStatus();
setInterval(loadStatus, 1000); // Aktualisiere jede Sekunde
</script>
</body>
</html>
+74
View File
@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Port Konfiguration</title>
</head>
<body>
<h1>Port Konfiguration</h1>
<form id="portForm">
<label>
Blinkintervall (ms): <input type="number" id="blinkInput" name="blink_interval" min="1" value="">
</label><br>
<div id="portsContainer"></div>
<input type="submit" value="Speichern">
</form>
<a href="/admin.html">Zurück</a>
<script>
async function loadConfig() {
try {
const response = await fetch('/portconfig/data');
const data = await response.json();
if (data.blink_interval !== undefined) {
document.getElementById('blinkInput').value = data.blink_interval;
}
const container = document.getElementById('portsContainer');
container.innerHTML = '';
data.ports.forEach((port, index) => {
const div = document.createElement('div');
div.innerHTML = `
<label>
Name: <input type="text" name="name${index}" value="${port.name}">
Aktiviert: <input type="checkbox" name="enabled${index}" ${port.enabled ? 'checked' : ''}>
</label>
`;
container.appendChild(div);
});
} catch (error) {
console.error('Fehler beim Laden der Konfiguration:', error);
}
}
document.getElementById('portForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const ports = [];
for (let i = 0; i < 16; i++) {
ports.push({
name: formData.get(`name${i}`) || `Port ${i}`,
enabled: formData.has(`enabled${i}`)
});
}
const blink = parseInt(formData.get('blink_interval')) || null;
const payload = { ports };
if (blink !== null) payload.blink_interval = blink;
try {
await fetch('/portconfig', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
alert('Konfiguration gespeichert!');
loadConfig(); // Reload
} catch (error) {
console.error('Fehler beim Speichern:', error);
}
});
loadConfig();
</script>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
/* HOBBYHIMMEL KeyPatch - Modern CSS */
:root {
--color-primary: #76B043;
--color-dark: #3F4242;
--color-gray: #6D6E71;
--color-light: #F5F5F5;
--color-white: #FFFFFF;
--border-radius: 12px;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: var(--color-light);
color: var(--color-dark);
display: flex;
flex-flow: column;
align-items: center;
margin: 0;
padding: 20px;
line-height: 1.6;
}
h1, h2 {
color: var(--color-dark);
font-weight: 600;
margin: 20px 0 15px 0;
text-shadow: none;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
li {
background-color: var(--color-white);
list-style-type: none;
margin-bottom: 12px;
padding: 12px 16px;
box-shadow: var(--shadow);
border-radius: var(--border-radius);
border-left: 4px solid var(--color-primary);
transition: all 0.3s ease;
}
li:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
li a:first-child, li b {
background-color: var(--color-primary);
font-weight: 600;
color: var(--color-white);
text-decoration: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
display: inline-block;
transition: all 0.3s ease;
text-shadow: none;
margin-right: 8px;
}
li a:first-child:hover, li b:hover {
background-color: var(--color-dark);
transform: scale(1.05);
}
li strong {
color: var(--color-primary);
font-weight: 600;
}
input {
height: 40px;
font-size: 14px;
padding: 10px 12px;
border: 2px solid var(--color-gray);
border-radius: var(--border-radius);
font-family: 'Open Sans', sans-serif;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(118, 176, 67, 0.1);
}
label + a {
text-decoration: none;
}
h1 + main {
display: flex;
width: 100%;
gap: 20px;
}
aside {
display: flex;
flex-direction: column;
padding: 0;
background-color: var(--color-white);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
min-width: 200px;
}
button {
height: 40px;
font-size: 16px;
margin-top: 1em;
box-shadow: var(--shadow);
border: none;
border-radius: var(--border-radius);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Open Sans', sans-serif;
padding: 0 20px;
}
button:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
div button {
background-color: var(--color-primary);
color: var(--color-white);
}
div button:hover {
background-color: #5FA03A;
}
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
background-color: var(--color-white);
padding: 15px 20px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 100%;
margin-bottom: 20px;
}
#left {
align-items: flex-end;
text-shadow: none;
color: var(--color-dark);
}
#cr {
font-weight: 600;
cursor: pointer;
font-size: 1.5em;
color: var(--color-primary);
}
#up {
width: auto;
}
.note {
background-color: #E8F5E9;
padding: 15px;
margin-top: 1em;
text-align: center;
max-width: 400px;
border-radius: var(--border-radius);
border-left: 4px solid var(--color-primary);
box-shadow: var(--shadow);
color: var(--color-dark);
}
.no {
display: none;
}
form [title] {
background-color: var(--color-primary);
color: var(--color-white);
font-size: 1em;
padding: 10px 12px;
border: none;
border-radius: var(--border-radius);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
form [title]:hover {
background-color: #5FA03A;
}
form:nth-of-type(2) {
margin-bottom: 1em;
}
[value*=Format] {
margin-top: 1em;
box-shadow: var(--shadow);
border-radius: var(--border-radius);
background-color: var(--color-white);
border: 2px solid var(--color-primary);
padding: 12px;
}
[name="group"] {
display: none;
}
[name="group"] + label {
font-size: 1.1em;
margin-right: 10px;
font-weight: 600;
color: var(--color-dark);
cursor: pointer;
user-select: none;
}
[name="group"] + label::before {
content: "\002610";
margin-right: 8px;
color: var(--color-primary);
}
[name="group"]:checked + label::before {
content: '\002611';
color: var(--color-primary);
}
@media only screen and (max-width: 500px) {
body {
padding: 10px;
}
h1 + main {
flex-direction: column;
}
.ip {
position: relative;
right: 0;
}
aside {
max-width: 100%;
width: 100%;
}
nav {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
button {
width: 100%;
}
.note {
max-width: 100%;
}
}
+3 -1
View File
@@ -2,6 +2,8 @@
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
lib_deps =
adafruit/Adafruit NeoPixel @ ^1.11.0
https://github.com/RobTillaart/PCF8575
https://github.com/RobTillaart/PCF8575
bblanchon/ArduinoJson @ ^6.21.0
+101
View File
@@ -0,0 +1,101 @@
// ****************************************************************
// Sketch Esp8266 Admin Modular(Tab)
// created: Jens Fleischer, 2019-12-17
// last mod: Jens Fleischer, 2021-06-09
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.1 - 3.1.0
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Admin sollte als Tab eingebunden werden.
// #include <LittleFS.h>/#include <FS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Spiffs.ino muss im ESP8266 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach setupFS()/spiffs() und dem Verbindungsaufbau aufgerufen werden.
// Die Funktion "runtime();" muss mindestens zweimal innerhalb 49 Tage aufgerufen werden.
// Entweder durch den Client(Webseite) oder zur Sicherheit im "loop();"
/**************************************************************************************/
//#define LittleFS SPIFFS // Einkommentieren wenn SPIFFS als Filesystem genutzt wird
const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};
void admin() { // Funktionsaufruf "admin();" muss im Setup eingebunden werden
File file = LittleFS.open("/config.json", "r");
if (file) {
String newhostname = file.readStringUntil('\n');
if (newhostname != "") {
WiFi.hostname(newhostname.substring(1, newhostname.length() - 1));
file.close();
ArduinoOTA.setHostname(WiFi.hostname().c_str());
}
}
server.on("/admin/renew", handlerenew);
server.on("/admin/once", handleonce);
server.on("/reconnect", []() {
server.send(304, "message/http");
WiFi.reconnect();
});
server.on("/restart", []() {
server.send(304, "message/http");
//save(); //Wenn Werte vor dem Neustart gespeichert werden sollen
ESP.restart();
});
}
//Es kann entweder die Spannung am ADC-Pin oder die Modulversorgungsspannung (VCC) ausgegeben werden.
void handlerenew() { // Um die am ADC-Pin anliegende externe Spannung zu lesen, verwende analogRead (A0)
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + analogRead(A0) + "\"]"); // Json als Array
}
/*
ADC_MODE(ADC_VCC);
void handlerenew() { // Zum Lesen der Modulversorgungsspannung (VCC), verwende ESP.getVcc()
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + ESP.getVcc() / 1024.0 + " V" + "\"]");
}
*/
void handleonce() {
if (server.arg(0) != "") {
WiFi.hostname(server.arg(0));
File f = LittleFS.open("/config.json", "w"); // Datei zum schreiben öffnen
f.printf("\"%s\"\n", WiFi.hostname().c_str());
f.close();
}
String temp = "{\"File\":\"" + sketchName() + "\", \"Build\":\"" + __DATE__ + " " + __TIME__ + "\", \"SketchSize\":\"" + formatBytes(ESP.getSketchSize()) +
"\", \"SketchSpace\":\"" + formatBytes(ESP.getFreeSketchSpace()) + "\", \"LocalIP\":\"" + WiFi.localIP().toString() +
"\", \"Hostname\":\"" + WiFi.hostname() + "\", \"SSID\":\"" + WiFi.SSID() + "\", \"GatewayIP\":\"" + WiFi.gatewayIP().toString() +
"\", \"Channel\":\"" + WiFi.channel() + "\", \"MacAddress\":\"" + WiFi.macAddress() + "\", \"SubnetMask\":\"" + WiFi.subnetMask().toString() +
"\", \"BSSID\":\"" + WiFi.BSSIDstr() + "\", \"ClientIP\":\"" + server.client().remoteIP().toString() + "\", \"DnsIP\":\"" + WiFi.dnsIP().toString() +
"\", \"ResetReason\":\"" + ESP.getResetReason() + "\", \"CpuFreqMHz\":\"" + F_CPU / 1000000 + "\", \"FreeHeap\":\"" + formatBytes(ESP.getFreeHeap()) +
"\", \"HeapFrag\":\"" + ESP.getHeapFragmentation() + "\", \"ChipSize\":\"" + formatBytes(ESP.getFlashChipSize()) +
"\", \"ChipSpeed\":\"" + ESP.getFlashChipSpeed() / 1000000 + "\", \"ChipMode\":\"" + flashChipMode[ESP.getFlashChipMode()] +
"\", \"IdeVersion\":\"" + ARDUINO + "\", \"CoreVersion\":\"" + ESP.getCoreVersion() + "\", \"SdkVersion\":\"" + ESP.getSdkVersion() + "\"}";
server.send(200, "application/json", temp); // Json als Objekt
}
String runtime() {
static uint8_t rolloverCounter;
static uint32_t previousMillis;
uint32_t currentMillis {millis()};
if (currentMillis < previousMillis) rolloverCounter++; // prüft millis() auf Überlauf
previousMillis = currentMillis;
uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
char buf[20];
snprintf(buf, sizeof(buf), "%*.d %.*s %02d:%02d:%02d",
sec < 86400 ? 0 : 1, sec / 86400, sec < 86400 ? 0 : sec >= 172800 ? 4 : 3, "Tage", sec / 3600 % 24, sec / 60 % 60, sec % 60);
return buf;
}
+5 -1
View File
@@ -4,5 +4,9 @@
#define NEOPIXEL_PIN D6
#define BRIGHTNESS 100
#define BLINK_INTERVAL 500 // Blink interval in milliseconds (0.5 seconds)
#define DEFAULT_BLINK_INTERVAL 500 // Blink interval in milliseconds (0.5 seconds)
#define SERIAL_SPEED 115200 // Serial Baudrate for ESP8266
// runtime variable - can be modified via web configuration
uint16_t blinkInterval = DEFAULT_BLINK_INTERVAL;
+185
View File
@@ -0,0 +1,185 @@
// ****************************************************************
// Sketch Esp8266 Login Manager mit Captive Portal und optischer Anzeige
// created: Jens Fleischer, 2021-01-05
// last mod: Jens Fleischer, 2021-11-29
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.3 / 2.7.4 / 3.0.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2021 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Login Manager sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers und des LittleFS Tab ist erforderlich.
// Die Funktion "connectWifi();" muss im Setup eingebunden werden.
// Die Oneboard LED blinkt beim Verbindungsaufbau zum Netzwerk und leuchtet im AP Modus dauerhaft.
// Die Zugangsdaten werden nicht menschenlesbar im Dateisystem gespeichert.
/**************************************************************************************/
/**
Folgendes muss im Webserver Tab vor dem "setup()" eingefügt werden.
#include <DNSServer.h>
const byte DNS_PORT = 53;
DNSServer dnsServer;
Der DNS Server muss im loop aufgerufen werden.
void loop() {
dnsServer.processNextRequest();
reStation();
}
*/
//#define CONFIG // Einkommentieren wenn der ESP dem Router die IP mitteilen soll.
#ifdef CONFIG
IPAddress staticIP(192, 168, 178, 99); // statische IP des NodeMCU ESP8266
IPAddress gateway(192, 168, 178, 1); // IP-Adresse des Router
IPAddress subnet(255, 255, 255, 0); // Subnetzmaske des Netzwerkes
IPAddress dns(192, 168, 178, 1); // DNS Server
#endif
const char HTML[] PROGMEM = R"(<!DOCTYPE HTML>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
button{width:11em;height:2.5em}
body{background: #87cefa; text-align: center;}
</style>
<title>Login Manager</title>
</head>
<body>
<h2>Zugangsdaten</h2>
<form>
<p>
<label>SSID:<br>
<input name="ssid" placeholder="Name vom Netzwerk" required>
</label>
</p>
<p>
<label>Passwort:<br>
<input name="passwort" pattern="[!-~]{8,64}" placeholder="PW vom Netzwerk" required>
</label>
</p>
</form>
<button>
Absenden
</button>
<script>
document.querySelector('button').addEventListener('click', async () =>{
let elem = document.querySelector('form');
if(elem.checkValidity() && document.querySelector('[pattern]').checkValidity()){
let resp = await fetch('/wifisave', {method: 'post', body: new FormData(elem)});
let json = await resp.json();
document.body.innerHTML = json;
}
});
</script>
</body>
</html>)";
const char JSON[] PROGMEM = R"("<h3>Die Zugangsdaten wurden übertragen. Eine Verbindung zum Netzwerk wird hergestellt.</h3>")";
char ssid[33] {" "};
char password[65];
constexpr char key {129};
void connectWifi() {
IPAddress apIP(172, 217, 28, 1);
IPAddress netMsk(255, 255, 255, 0);
File file = LittleFS.open("/wifi.dat", "r");
if (file) {
file.read(reinterpret_cast<uint8_t*>(&ssid), sizeof(ssid));
file.read(reinterpret_cast<uint8_t*>(&password), sizeof(password));
file.close();
for (auto &c : ssid) c ^= key; // Dechiffrierung SSID
for (auto &c : password) c ^= key; // Dechiffrierung Passwort
}
WiFi.disconnect();
WiFi.persistent(false); // Auskommentieren wenn Netzwerkname und Passwort in den Flash geschrieben werden sollen.
WiFi.mode(WIFI_STA); // Station-Modus
WiFi.begin(ssid, password);
#ifdef CONFIG
WiFi.config(staticIP, gateway, subnet, dns);
#endif
uint8_t i {0};
while (WiFi.status() != WL_CONNECTED) {
pinMode(LED_BUILTIN, OUTPUT); // OnBoardLed Nodemcu, Wemos D1 Mini Pro
digitalWrite(LED_BUILTIN, 0); // Led blinkt während des Verbindungsaufbaus
delay(500);
digitalWrite(LED_BUILTIN, 1);
delay(500);
Serial.printf(" %i sek\n", ++i);
if (WiFi.status() == WL_NO_SSID_AVAIL || i > 29) { // Ist die SSID nicht erreichbar, wird ein eigenes Netzwerk erstellt.
digitalWrite(LED_BUILTIN, 0); // Dauerleuchten der Led zeigt den AP Modus an.
WiFi.disconnect();
WiFi.mode(WIFI_AP); // Soft-Access-Point-Modus
Serial.println(PSTR("\nVerbindung zum Router fehlgeschlagen !\nStarte Soft AP"));
WiFi.softAPConfig(apIP, apIP, netMsk);
if (WiFi.softAP("EspConfig")) {
Serial.println(PSTR("Verbinde dich mit dem Netzwerk \"EspConfig\".\n"));
}
break;
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf(PSTR("\nVerbunden mit: %s\nEsp8266 IP: %s\n"), WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
}
dnsServer.start(DNS_PORT, "*", apIP);
server.on("/wifisave", HTTP_POST, handleWifiSave);
server.onNotFound(handleRoot);
}
void handleWifiSave() {
if (server.hasArg("ssid") && server.hasArg("passwort")) {
strcpy(ssid, server.arg(0).c_str());
strcpy(password, server.arg(1).c_str());
for (auto &c : ssid) c ^= key; // Chiffrierung SSID
for (auto &c : password) c ^= key; // Chiffrierung Passwort
File file = LittleFS.open("/wifi.dat", "w");
file.write(reinterpret_cast<uint8_t*>(&ssid), sizeof(ssid));
file.write(reinterpret_cast<uint8_t*>(&password), sizeof(password));
file.close();
server.send(200, "application/json", JSON);
delay(500);
connectWifi();
}
}
void handleRoot() {
if (WiFi.status() != WL_CONNECTED) { // Besteht keine Verbindung zur Station wird das Formular gesendet.
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "text/html", HTML);
}
else {
if (!handleFile(server.urlDecode(server.uri()))) {
if (server.urlDecode(server.uri()).endsWith("/")) sendResponce();
server.send(404, "text/plain", "FileNotFound");
}
}
}
void reStation() { // Der Funktionsaufruf "reStation();" sollte im "loop" stehen.
static unsigned long previousMillis; // Nach Stromausfall startet der Esp.. schneller als der Router.
constexpr unsigned long INTERVAL (3e5); // Im AP Modus aller 5 Minuten prüfen ob der Router verfügbar ist.
if (millis() - previousMillis >= INTERVAL) {
previousMillis += INTERVAL;
if (WiFi.status() != WL_CONNECTED) connectWifi();
}
}
+211 -10
View File
@@ -1,21 +1,103 @@
#include <Wire.h>
#include <PCF8575.h>
#include <Adafruit_NeoPixel.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h> // for web-based firmware upload
#include <ArduinoOTA.h>
#include <LittleFS.h>
#include "Config.ino"
#include <DNSServer.h>
#include <ArduinoJson.h>
#define AMOUNTOFPORTS 8 // currently max 16 ports are supported
// Globale Deklarationen für alle Tabs
ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater; // provides /update endpoint
const byte DNS_PORT = 53;
DNSServer dnsServer;
String sketchName() { // Dateiname für den Admin Tab ab EspCoreVersion 2.6.0
char file[sizeof(__FILE__)] = __FILE__;
char * pos = strrchr(file, '.'); *pos = '\0';
return file;
}
#define AMOUNTOFPORTS 16 // currently max 16 ports are supported
#define EXPANDER1ADDRESS 0x21 // address of expander 1
struct PortConfig {
String name;
bool enabled;
};
// global blink interval stored along with port configuration
// defined in Config.ino as a variable so it can be changed at runtime
extern uint16_t blinkInterval;
PCF8575 expander1(EXPANDER1ADDRESS);
uint8_t portStates[AMOUNTOFPORTS];
uint8_t lastStates[AMOUNTOFPORTS] = {0};
PortConfig portConfigs[AMOUNTOFPORTS];
byte number=0;
extern Adafruit_NeoPixel* pixels;
void loadPortConfig() {
File file = LittleFS.open("/portconfig.json", "r");
if (file) {
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, file);
file.close();
if (!error) {
JsonArray ports = doc["ports"];
for (size_t i = 0; i < AMOUNTOFPORTS && i < ports.size(); i++) {
portConfigs[i].name = ports[i]["name"] | ("Port " + String(i));
portConfigs[i].enabled = ports[i]["enabled"] | true;
}
// read blink interval if present
if (doc.containsKey("blink_interval")) {
blinkInterval = doc["blink_interval"] | blinkInterval;
}
} else {
// Default
for (int i = 0; i < AMOUNTOFPORTS; i++) {
portConfigs[i].name = "Port " + String(i);
portConfigs[i].enabled = true;
}
}
} else {
// Default all enabled
for (int i = 0; i < AMOUNTOFPORTS; i++) {
portConfigs[i].name = "Port " + String(i);
portConfigs[i].enabled = true;
}
}
}
void savePortConfig() {
DynamicJsonDocument doc(1024);
JsonArray ports = doc.createNestedArray("ports");
for (int i = 0; i < AMOUNTOFPORTS; i++) {
JsonObject port = ports.createNestedObject();
port["name"] = portConfigs[i].name;
port["enabled"] = portConfigs[i].enabled;
}
// store blink interval as well
doc["blink_interval"] = blinkInterval;
File file = LittleFS.open("/portconfig.json", "w");
if (file) {
serializeJson(doc, file);
file.close();
}
}
void setup() {
Serial.begin(9600);
Serial.println("Starte 16-Port IO Erweiterung...");
Serial.begin(SERIAL_SPEED);
delay(100);
Serial.printf("\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n%s\n\n",
(__FILE__), (__TIMESTAMP__), ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 100 / 10 ? ARDUINO % 100 : ARDUINO % 10, ESP.getFullVersion().c_str());
Serial.printf("\nKeyPatch ESP8266 WebServer\n");
//Set UP 12C Communication
Wire.begin();
for (byte adress=8; adress<120; adress++)
@@ -57,10 +139,125 @@ void setup() {
};
pixels->show();
// SetUp WebServer
setupFS(); // setupFS(); oder spiffs(); je nach Dateisystem
loadPortConfig(); // Lade Port-Konfiguration
connectWifi();
admin();
// configure OTA service (IDE/network update)
ArduinoOTA.setHostname("KeyPatch"); // optional, default is esp8266-[ChipID]
ArduinoOTA.setPassword((const char *)"esp8266"); // change to a strong password or read from config
ArduinoOTA.onStart([]() {
Serial.println("OTA start");
//save(); // Wenn Werte vor dem Neustart gespeichert werden sollen
});
ArduinoOTA.onEnd([]() {
Serial.println("OTA end");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("OTA progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("OTA Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
else if (error == OTA_END_ERROR) Serial.println("End Failed");
});
ArduinoOTA.begin();
// allow firmware upload via HTTP (web page)
httpUpdater.setup(&server); // no auth
// httpUpdater.setup(&server, "admin", "secret"); // with basic auth
server.begin();
// Handler für Port-Konfiguration
server.on("/portconfig", HTTP_GET, []() {
File file = LittleFS.open("/html/portconfig.html", "r");
if (file) {
server.streamFile(file, "text/html");
file.close();
} else {
server.send(404, "text/plain", "File not found");
}
});
server.on("/portconfig/data", HTTP_GET, []() {
DynamicJsonDocument doc(1024);
JsonArray ports = doc.createNestedArray("ports");
for (int i = 0; i < AMOUNTOFPORTS; i++) {
JsonObject port = ports.createNestedObject();
port["name"] = portConfigs[i].name;
port["enabled"] = portConfigs[i].enabled;
}
// include current blink interval
doc["blink_interval"] = blinkInterval;
String json;
serializeJson(doc, json);
server.send(200, "application/json", json);
});
server.on("/portconfig", HTTP_POST, []() {
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, server.arg("plain"));
if (!error) {
JsonArray ports = doc["ports"];
for (size_t i = 0; i < AMOUNTOFPORTS && i < ports.size(); i++) {
portConfigs[i].name = ports[i]["name"] | ("Port " + String(i));
portConfigs[i].enabled = ports[i]["enabled"] | true;
}
// update blink interval if provided
if (doc.containsKey("blink_interval")) {
blinkInterval = doc["blink_interval"] | blinkInterval;
}
savePortConfig();
}
server.sendHeader("Location", "/portconfig");
server.send(303);
});
// Handler für Startseite
server.on("/", HTTP_GET, []() {
File file = LittleFS.open("/index.html", "r");
if (file) {
server.streamFile(file, "text/html");
file.close();
} else {
server.send(404, "text/plain", "File not found");
}
});
// Handler für Status-Daten
server.on("/status/data", HTTP_GET, []() {
DynamicJsonDocument doc(1024);
JsonArray ports = doc.createNestedArray("ports");
for (int i = 0; i < AMOUNTOFPORTS; i++) {
JsonObject port = ports.createNestedObject();
port["name"] = portConfigs[i].name;
port["enabled"] = portConfigs[i].enabled;
port["state"] = portStates[i];
}
// include blink interval so the webpage can synchronize its animation
doc["blink_interval"] = blinkInterval;
String json;
serializeJson(doc, json);
server.send(200, "application/json", json);
});
Serial.println("Setup End...");
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) { // Die Funktion "runtime()" wird nur für den Admin Tab gebraucht.
runtime(); // Auskommentieren falls du den Admin Tab nicht nutzen möchtest.
}
dnsServer.processNextRequest();
reStation();
// Erst 16 Ports aus PCF8575 einlesen
for (uint8_t i = 0; i < AMOUNTOFPORTS; i++) {
portStates[i] = expander1.read(i);
@@ -76,15 +273,19 @@ void loop() {
lastStates[i] = portStates[i];
}
// LED aktualisieren
if (portStates[i] == 0) {
pixels->setPixelColor(i, pixels->Color(0, 150, 0)); // grün
} else {
bool globalBlinkState = (millis() / BLINK_INTERVAL) % 2;
if (globalBlinkState) {
pixels->setPixelColor(i, pixels->Color(150, 0, 0)); // rot
if (portConfigs[i].enabled) {
if (portStates[i] == 0) {
pixels->setPixelColor(i, pixels->Color(0, 150, 0)); // grün
} else {
pixels->setPixelColor(i, pixels->Color(0, 0, 0)); // aus
bool globalBlinkState = (millis() / blinkInterval) % 2;
if (globalBlinkState) {
pixels->setPixelColor(i, pixels->Color(150, 0, 0)); // rot
} else {
pixels->setPixelColor(i, pixels->Color(0, 0, 0)); // aus
}
}
} else {
pixels->setPixelColor(i, pixels->Color(0, 0, 0)); // aus
}
}
pixels->show();
+173
View File
@@ -0,0 +1,173 @@
// ****************************************************************
// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab)
// created: Jens Fleischer, 2020-06-08
// last mod: Jens Fleischer, 2020-09-02
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 2.7.4
// Getestet auf: Nodemcu
/******************************************************************
Copyright (c) 2020 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von LittleFS sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Die Funktion "setupFS();" muss im Setup aufgerufen werden.
/**************************************************************************************/
#include <list>
#include <tuple>
const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "FS:none" kompilliert!)";
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die fs.html hoch.)";
void setupFS() { // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden
LittleFS.begin();
server.on("/format", formatFS);
server.on("/upload", HTTP_POST, sendResponce, handleUpload);
server.onNotFound([]() {
if (!handleFile(server.urlDecode(server.uri())))
server.send(404, "text/plain", "FileNotFound");
});
}
bool handleList() { // Senden aller Daten an den Client
FSInfo fs_info; LittleFS.info(fs_info); // Füllt FSInfo Struktur mit Informationen über das Dateisystem
Dir dir = LittleFS.openDir("/");
using namespace std;
typedef tuple<String, String, int> records;
list<records> dirList;
while (dir.next()) { // Ordner und Dateien zur Liste hinzufügen
if (dir.isDirectory()) {
uint8_t ran {0};
Dir fold = LittleFS.openDir(dir.fileName());
while (fold.next()) {
ran++;
dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize());
}
if (!ran) dirList.emplace_back(dir.fileName(), "", 0);
}
else {
dirList.emplace_back("", dir.fileName(), dir.fileSize());
}
}
dirList.sort([](const records & f, const records & l) { // Dateien sortieren
if (server.arg(0) == "1") {
return get<2>(f) > get<2>(l);
} else {
for (uint8_t i = 0; i < 31; i++) {
if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) return true;
else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) return false;
}
return false;
}
});
dirList.sort([](const records & f, const records & l) { // Ordner sortieren
if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) {
for (uint8_t i = 0; i < 31; i++) {
if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) return true;
else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) return false;
}
}
return false;
});
String temp = "[";
for (auto& t : dirList) {
if (temp != "[") temp += ',';
temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + formatBytes(get<2>(t)) + "\"}";
}
temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes) + // Berechnet den verwendeten Speicherplatz
"\",\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) + // Zeigt die Größe des Speichers
"\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]"; // Berechnet den freien Speicherplatz
server.send(200, "application/json", temp);
return true;
}
void deleteRecursive(const String &path) {
if (LittleFS.remove(path)) {
LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w");
return;
}
Dir dir = LittleFS.openDir(path);
while (dir.next()) {
deleteRecursive(path + '/' + dir.fileName());
}
LittleFS.rmdir(path);
}
bool handleFile(String &&path) {
if (server.hasArg("new")) {
String folderName {server.arg("new")};
for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : folderName) if (e == c) e = 95; // Ersetzen der nicht erlaubten Zeichen
LittleFS.mkdir(folderName);
}
if (server.hasArg("sort")) return handleList();
if (server.hasArg("delete")) {
deleteRecursive(server.arg("delete"));
sendResponce();
return true;
}
if (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermöglicht das hochladen der fs.html
if (path.endsWith("/")) path += "index.html";
if (path == "/spiffs.html") sendResponce(); // Vorrübergehend für den Admin Tab
return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); server.streamFile(f, getContentType(path)); f.close(); true;}) : false;
}
void handleUpload() { // Dateien ins Filesystem schreiben
static File fsUploadFile;
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (upload.filename.length() > 31) { // Dateinamen kürzen
upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length());
}
printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str());
fsUploadFile = LittleFS.open(server.arg(0) + "/" + server.urlDecode(upload.filename), "w");
} else if (upload.status == UPLOAD_FILE_WRITE) {
printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize);
fsUploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize);
fsUploadFile.close();
}
}
void formatFS() { // Formatiert das Filesystem
LittleFS.format();
sendResponce();
}
void sendResponce() {
server.sendHeader("Location", "fs.html");
server.send(303, "message/http");
}
const String formatBytes(size_t const& bytes) { // lesbare Anzeige der Speichergrößen
return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}
const String getContentType(const String & path) { // ermittelt den MIME-Type
using namespace mime;
char buff[sizeof(mimeTable[0].mimeType)];
for (size_t i = 0; i < maxType - 1; i++) {
strcpy_P(buff, mimeTable[i].endsWith);
if (path.endsWith(buff)) {
strcpy_P(buff, mimeTable[i].mimeType);
return static_cast<String>(buff);
}
}
strcpy_P(buff, mimeTable[maxType - 1].mimeType);
return static_cast<String>(buff);
}
+54
View File
@@ -0,0 +1,54 @@
// ****************************************************************
// Sketch Esp8266 Webserver Modular(Tab)
// created: Jens Fleischer, 2018-05-16
// last mod: Jens Fleischer, 2020-12-28
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Der WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include <LittleFS.h> bzw. #include <FS.h> und #include <ESP8266WebServer.h>
// müssen im Haupttab aufgerufen werden.
// Ein Connect Tab ist erforderlich.
// Inklusive Arduino OTA-Updates (Erfordert freien Flash-Speicher)
/**************************************************************************************/
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h> // https://arduino-esp8266.readthedocs.io/en/latest/ota_updates/readme.html
#include <LittleFS.h> // Library für Dateisystem LittleFS
#include <DNSServer.h>
//#include <FS.h> // Library für Dateisystem Spiffs einkommentieren wenn erforderlich
// ESP8266WebServer server(80); // Jetzt in KeyPatch.ino deklariert
// DNSServer dnsServer; // Jetzt in KeyPatch.ino deklariert
// const byte DNS_PORT = 53; // Jetzt in KeyPatch.ino deklariert
void setupWebserver() {
setupFS(); // Filesystem setup
ArduinoOTA.onStart([]() {
// Hier können Werte vor dem Neustart gespeichert werden
});
ArduinoOTA.begin();
}
void handleWebserver() {
ArduinoOTA.handle();
server.handleClient();
dnsServer.processNextRequest();
reStation();
}
+146
View File
@@ -0,0 +1,146 @@
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>ESP8266 Admin</title>
<script>
addEventListener('load', () => {
renew(), once();
let output = document.querySelector('#note');
let btn = document.querySelectorAll('button');
let span = document.querySelectorAll('#right span');
btn[0].addEventListener('click', () => {
location = '/fs.html';
});
btn[1].addEventListener('click', () => {
location = '/';
});
btn[2].addEventListener('click', () => {
location = '/portconfig.html';
});
btn[3].addEventListener('click', check.bind(this, document.querySelector('input')));
btn[4].addEventListener('click', re.bind(this, 'reconnect'));
btn[5].addEventListener('click', () => {
if (confirm('Bist du sicher!')) re('restart');
});
async function once(val = '',arg) {
try {
let resp = await fetch('/admin/once', { method: 'POST', body: val});
let obj = await resp.json();
output.innerHTML = '';
output.classList.remove('note');
document.querySelector('form').reset();
if (val.length == 0) myIv = setInterval(renew, 1000);
if (arg == 'reconnect') re(arg);
document.getElementById('file').innerHTML = obj['File'];
document.getElementById('build').innerHTML = obj['Build'];
document.getElementById('size').innerHTML = obj['SketchSize'];
document.getElementById('space').innerHTML = obj['SketchSpace'];
document.getElementById('ip').innerHTML = obj['LocalIP'];
document.getElementById('hostname').innerHTML = obj['Hostname'];
document.getElementById('ssid').innerHTML = obj['SSID'];
document.getElementById('gateway').innerHTML = obj['GatewayIP'];
document.getElementById('channel').innerHTML = obj['Channel'];
document.getElementById('mac').innerHTML = obj['MacAddress'];
document.getElementById('subnet').innerHTML = obj['SubnetMask'];
document.getElementById('bssid').innerHTML = obj['BSSID'];
document.getElementById('clientip').innerHTML = obj['ClientIP'];
document.getElementById('dnsip').innerHTML = obj['DnsIP'];
document.getElementById('reset').innerHTML = obj['ResetReason'];
document.getElementById('cpu').innerHTML = obj['CpuFreqMHz'] + " MHz";
document.getElementById('heap').innerHTML = obj['FreeHeap'];
document.getElementById('frag').innerHTML = obj['HeapFrag'] + "%";
document.getElementById('flashsize').innerHTML = obj['ChipSize'];
document.getElementById('flashspeed').innerHTML = obj['ChipSpeed'] + " MHz";
document.getElementById('flashmode').innerHTML = obj['ChipMode'];
document.getElementById('ide').innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
document.getElementById('core').innerHTML = obj['CoreVersion'].replace(/_/g,'.');
document.getElementById('sdk').innerHTML = obj['SdkVersion'];
} catch(err) {
re();
}
}
async function renew() {
const resp = await fetch('admin/renew');
const array = await resp.json();
document.getElementById('runtime').innerHTML = array[0];
document.getElementById('rssi').innerHTML = array[1];
document.getElementById('adc').innerHTML = array[2];
}
function check(inObj) {
!inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
}
function re(arg = '') {
clearInterval(myIv);
fetch(arg);
output.classList.add('note');
if (arg == 'restart') {
output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 15 Sekunden neu geladen.';
setTimeout(once, 15000);
}
else if (arg == 'reconnect'){
output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 10 Sekunden neu geladen.';
setTimeout(once, 10000);
}
else {
output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
setTimeout(once, 3000);
}
}
});
</script>
</head>
<body>
<h1>ESP8266 Admin Page</h1>
<main>
<table>
<tr><td>Runtime ESP:</td><td><span id="runtime">0</span></td></tr>
<tr><td>WiFi RSSI:</td><td><div><span id="rssi"></span> dBm</div></td></tr>
<tr><td>ADC/VCC:</td><td><span id="adc">0</span></td></tr>
<tr><td>Sketch Name:</td><td><span id="file">?</span></td></tr>
<tr><td>Sketch Build:</td><td><span id="build">0</span></td></tr>
<tr><td>SketchSize:</td><td><span id="size">0</span></td></tr>
<tr><td>FreeSketchSpace:</td><td><span id="space">0</span></td></tr>
<tr><td>IPv4 Address:</td><td><span id="ip">0</span></td></tr>
<tr><td>Hostname:</td><td><span id="hostname">?</span></td></tr>
<tr><td>Connected to:</td><td><span id="ssid">?</span></td></tr>
<tr><td>Gateway IP:</td><td><span id="gateway">0</span></td></tr>
<tr><td>Channel:</td><td><span id="channel">0</span></td></tr>
<tr><td>MacAddress:</td><td><span id="mac">0</span></td></tr>
<tr><td>SubnetMask:</td><td><span id="subnet">0</span></td></tr>
<tr><td>BSSID:</td><td><span id="bssid">0</span></td></tr>
<tr><td>Client IP:</td><td><span id="clientip">0</span></td></tr>
<tr><td>DnsIP:</td><td><span id="dnsip">0</span></td></tr>
<tr><td>Reset Ground:</td><td><span id="reset">?</span></td></tr>
<tr><td>CPU Freq:</td><td><span id="cpu">0</span> MHz</td></tr>
<tr><td>FreeHeap:</td><td><span id="heap">0</span></td></tr>
<tr><td>Heap Fragmentation:</td><td><span id="frag">0</span>%</td></tr>
<tr><td>FlashSize:</td><td><span id="flashsize">0</span></td></tr>
<tr><td>FlashSpeed:</td><td><span id="flashspeed">0</span> MHz</td></tr>
<tr><td>FlashMode:</td><td><span id="flashmode">0</span></td></tr>
<tr><td>Arduino IDE Version:</td><td><span id="ide">0</span></td></tr>
<tr><td>Esp Core Version:</td><td><span id="core">0</span></td></tr>
<tr><td>SDK Version:</td><td><span id="sdk">0</span></td></tr>
</table>
</main>
<div>
<button>Filesystem</button>
<button>Startseite</button>
<button>Port Konfiguration</button>
</div>
<div id="note"></div>
<div>
<form>
<input placeholder="neuer Hostname" pattern="([A-Za-z0-9\-]{1,32})" title="Es dürfen nur Buchstaben (a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
<button type="button">Name Senden</button>
</form>
</div>
<div>
<button>WiFi Reconnect</button>
<button>ESP Restart</button>
</div>
</body>
</html>
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Filesystem Manager</title>
<script>
document.addEventListener('DOMContentLoaded', () => {
list(JSON.parse(localStorage.getItem('sortBy')));
btn.addEventListener('click', () => {
if (!confirm(`Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault();
});
});
async function list(to){
let resp = await fetch(`?sort=${to}`);
let json = await resp.json();
let myList = document.querySelector('main'), noted = '';
myList.innerHTML = '<nav><input type="radio" id="/" name="group" checked="checked"><label for="/"> &#128193;</label><span id="cr">+&#128193;</nav></span><span id="si"></span>';
for (var i = 0; i < json.length - 1; i++) {
let dir = '', f = json[i].folder, n = json[i].name;
if (f != noted) {
noted = f;
dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> &#128193; ${f} <a href="?delete=/${f}">&#x1f5d1;&#xfe0f;</a></nav>`;
}
if (n != '') dir += `<li><a href="${f}/${n}">${n}</a><small> ${json[i].size}</small><a href="${f}/${n}"download="${n}"> Download</a> or<a href="?delete=${f}/${n}"> Delete</a>`;
myList.insertAdjacentHTML('beforeend', dir);
}
myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '&#9660;' : '&#9650;'} LittleFS</b> belegt ${json[i].usedBytes.replace(".00", "")} von ${json[i].totalBytes.replace(".00", "")}`);
var free = json[i].freeBytes;
cr.addEventListener('click', () => {
document.getElementById('no').classList.toggle('no');
});
so.addEventListener('click', () => {
list(to=++to%2);
localStorage.setItem('sortBy', JSON.stringify(to));
});
document.addEventListener('change', (e) => {
if (e.target.id == 'fs') {
for (var bytes = 0, i = 0; i < event.target.files.length; i++) bytes += event.target.files[i].size;
for (var output = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) output = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
if (bytes > free) {
si.innerHTML = `<li><b> ${output}</b><strong> Ungenügend Speicher frei</strong></li>`;
up.setAttribute('disabled', 'disabled');
}
else {
si.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
up.removeAttribute('disabled');
}
}
document.querySelectorAll(`input[type=radio]`).forEach(el => { if (el.checked) document.querySelector('form').setAttribute('action', '/upload?f=' + el.id)});
});
document.querySelectorAll('[href^="?delete=/"]').forEach(node => {
node.addEventListener('click', () => {
if (!confirm('Sicher!')) event.preventDefault();
});
});
}
</script>
</head>
<body>
<h2>ESP8266 Filesystem Manager</h2>
<form method="post" enctype="multipart/form-data" action="/upload?f=/">
<input id="fs" type="file" name="up[]" multiple>
<button id="up" disabled>Upload</button>
</form>
<form id="no" class="no" method="POST">
<input name="new" placeholder="Ordner Name" pattern="[^\x22\/%&\\:;]{0,31}[^\x22\/%&\\:;\s]{1}" title="Zeichen &#8220; % & / : ; \ sind nicht erlaubt." required="">
<button>Create</button>
</form>
<main></main>
<form action="/format" method="POST">
<button id="btn">Format LittleFS</button>
</form>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Port Status</title>
<style>
/* blink duration is defined via CSS variable so we can update it dynamically */
:root {
--blink-duration: 1s; /* default, will be overwritten by script */
}
.port {
margin: 10px;
padding: 10px;
border: 1px solid #ccc;
display: inline-block;
width: 200px;
}
.disabled {
background-color: lightgray;
color: gray;
}
.ok {
background-color: lightgreen;
}
.missing {
background-color: red;
animation: blink var(--blink-duration) infinite;
}
@keyframes blink {
0%, 50% { background-color: red; }
51%, 100% { background-color: white; }
}
</style>
</head>
<body>
<h1>Port Status Übersicht</h1>
<div id="portsContainer"></div>
<br>
<button onclick="location.href='/portconfig.html'">Zur Konfiguration</button>
<button onclick="location.href='/admin.html'">Zur Admin Seite</button>
<script>
// when status data is received we update the blink animation length
async function updateBlinkDuration(blinkInterval) {
// period is two intervals (on+off)
const period = blinkInterval * 2;
document.documentElement.style.setProperty('--blink-duration', period + 'ms');
}
async function loadStatus() {
try {
const response = await fetch('/status/data');
const data = await response.json();
// synchronize blink rate on each update
if (data.blink_interval !== undefined) {
updateBlinkDuration(data.blink_interval);
}
const container = document.getElementById('portsContainer');
container.innerHTML = '';
data.ports.forEach((port, index) => {
const div = document.createElement('div');
div.className = 'port';
let statusClass = '';
let statusText = '';
if (!port.enabled) {
statusClass = 'disabled';
statusText = 'disabled';
} else if (port.state === 0) {
statusClass = 'ok';
statusText = 'OK';
} else {
statusClass = 'missing';
statusText = 'Fehlt';
}
div.classList.add(statusClass);
div.innerHTML = `
<strong>Port ${index}: ${port.name}</strong><br>
Status: ${statusText}
`;
container.appendChild(div);
});
} catch (error) {
console.error('Fehler beim Laden des Status:', error);
}
}
loadStatus();
setInterval(loadStatus, 1000); // Aktualisiere jede Sekunde
</script>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Port Konfiguration</title>
</head>
<body>
<h1>Port Konfiguration</h1>
<form id="portForm">
<label>
Blinkintervall (ms): <input type="number" id="blinkInput" name="blink_interval" min="1" value="">
</label><br>
<div id="portsContainer"></div>
<input type="submit" value="Speichern">
</form>
<button onclick="location.href='/'">Zurück</button>
<script>
async function loadConfig() {
try {
const response = await fetch('/portconfig/data');
const data = await response.json();
// fill blink interval field if provided
if (data.blink_interval !== undefined) {
document.getElementById('blinkInput').value = data.blink_interval;
}
const container = document.getElementById('portsContainer');
container.innerHTML = '';
data.ports.forEach((port, index) => {
const div = document.createElement('div');
div.innerHTML = `
<label>
Name: <input type="text" name="name${index}" value="${port.name}">
Aktiviert: <input type="checkbox" name="enabled${index}" ${port.enabled ? 'checked' : ''}>
</label>
`;
container.appendChild(div);
});
} catch (error) {
console.error('Fehler beim Laden der Konfiguration:', error);
}
}
document.getElementById('portForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const ports = [];
for (let i = 0; i < 16; i++) {
ports.push({
name: formData.get(`name${i}`) || `Port ${i}`,
enabled: formData.has(`enabled${i}`)
});
}
const blink = parseInt(formData.get('blink_interval')) || null;
const payload = { ports };
if (blink !== null) payload.blink_interval = blink;
try {
await fetch('/portconfig', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
alert('Konfiguration gespeichert!');
loadConfig(); // Reload
} catch (error) {
console.error('Fehler beim Speichern:', error);
}
});
loadConfig();
</script>
</body>
</html>
+284
View File
@@ -0,0 +1,284 @@
/* HOBBYHIMMEL KeyPatch - Modern CSS */
:root {
--color-primary: #76B043;
--color-dark: #3F4242;
--color-gray: #6D6E71;
--color-light: #F5F5F5;
--color-white: #FFFFFF;
--border-radius: 12px;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
--shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.15);
}
* {
box-sizing: border-box;
}
body {
font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: var(--color-light);
color: var(--color-dark);
display: flex;
flex-flow: column;
align-items: center;
margin: 0;
padding: 20px;
line-height: 1.6;
}
h1, h2 {
color: var(--color-dark);
font-weight: 600;
margin: 20px 0 15px 0;
text-shadow: none;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
li {
background-color: var(--color-white);
list-style-type: none;
margin-bottom: 12px;
padding: 12px 16px;
box-shadow: var(--shadow);
border-radius: var(--border-radius);
border-left: 4px solid var(--color-primary);
transition: all 0.3s ease;
}
li:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
li a:first-child, li b {
background-color: var(--color-primary);
font-weight: 600;
color: var(--color-white);
text-decoration: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
display: inline-block;
transition: all 0.3s ease;
text-shadow: none;
margin-right: 8px;
}
li a:first-child:hover, li b:hover {
background-color: var(--color-dark);
transform: scale(1.05);
}
li strong {
color: var(--color-primary);
font-weight: 600;
}
input {
height: 40px;
font-size: 14px;
padding: 10px 12px;
border: 2px solid var(--color-gray);
border-radius: var(--border-radius);
font-family: 'Open Sans', sans-serif;
transition: border-color 0.3s ease;
}
input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(118, 176, 67, 0.1);
}
label + a {
text-decoration: none;
}
h1 + main {
display: flex;
width: 100%;
gap: 20px;
}
aside {
display: flex;
flex-direction: column;
padding: 0;
background-color: var(--color-white);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
min-width: 200px;
}
button {
height: 40px;
font-size: 16px;
margin-top: 1em;
box-shadow: var(--shadow);
border: none;
border-radius: var(--border-radius);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Open Sans', sans-serif;
padding: 0 20px;
}
button:hover {
box-shadow: var(--shadow-hover);
transform: translateY(-2px);
}
button:active {
transform: translateY(0);
}
div button {
background-color: var(--color-primary);
color: var(--color-white);
}
div button:hover {
background-color: #5FA03A;
}
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
background-color: var(--color-white);
padding: 15px 20px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 100%;
margin-bottom: 20px;
}
#left {
align-items: flex-end;
text-shadow: none;
color: var(--color-dark);
}
#cr {
font-weight: 600;
cursor: pointer;
font-size: 1.5em;
color: var(--color-primary);
}
#up {
width: auto;
}
.note {
background-color: #E8F5E9;
padding: 15px;
margin-top: 1em;
text-align: center;
max-width: 400px;
border-radius: var(--border-radius);
border-left: 4px solid var(--color-primary);
box-shadow: var(--shadow);
color: var(--color-dark);
}
.no {
display: none;
}
form [title] {
background-color: var(--color-primary);
color: var(--color-white);
font-size: 1em;
padding: 10px 12px;
border: none;
border-radius: var(--border-radius);
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
form [title]:hover {
background-color: #5FA03A;
}
form:nth-of-type(2) {
margin-bottom: 1em;
}
[value*=Format] {
margin-top: 1em;
box-shadow: var(--shadow);
border-radius: var(--border-radius);
background-color: var(--color-white);
border: 2px solid var(--color-primary);
padding: 12px;
}
[name="group"] {
display: none;
}
[name="group"] + label {
font-size: 1.1em;
margin-right: 10px;
font-weight: 600;
color: var(--color-dark);
cursor: pointer;
user-select: none;
}
[name="group"] + label::before {
content: "\002610";
margin-right: 8px;
color: var(--color-primary);
}
[name="group"]:checked + label::before {
content: '\002611';
color: var(--color-primary);
}
@media only screen and (max-width: 500px) {
body {
padding: 10px;
}
h1 + main {
flex-direction: column;
}
.ip {
position: relative;
right: 0;
}
aside {
max-width: 100%;
width: 100%;
}
nav {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
button {
width: 100%;
}
.note {
max-width: 100%;
}
}