Anpassung

- Readme mit Beschreibug der Software
- style.css mit modernerem look und HOBBYHIMMEL Farben
This commit is contained in:
toptah 2026-03-12 21:09:02 +01:00
parent 1bf8c6545f
commit c39fd04b07
5 changed files with 581 additions and 100 deletions

131
README.md
View File

@ -57,6 +57,137 @@ Codierung der Ports:
| 23 | 5 | 6 | weiß-blau | grün | 0x22-P06 | | 23 | 5 | 6 | weiß-blau | grün | 0x22-P06 |
| 24 | 5 | 7 | weiß-blau | weiß-braun | 0x22-P07 | | 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
- Persistente Speicherung der Konfiguration in `portconfig.json`
### 📡 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
---

View File

@ -1,120 +1,284 @@
/* For more information visit:https://fipsok.de */ /* 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 { body {
font-family: sans-serif; font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #87cefa; background-color: var(--color-light);
color: var(--color-dark);
display: flex; display: flex;
flex-flow: column; flex-flow: column;
align-items: center; align-items: center;
margin: 0;
padding: 20px;
line-height: 1.6;
} }
h1,h2 {
color: #e1e1e1; h1, h2 {
text-shadow: 2px 2px 2px black; 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 { li {
background-color: #feb1e2; background-color: var(--color-white);
list-style-type: none; list-style-type: none;
margin-bottom: 10px; margin-bottom: 12px;
padding: 2px 5px 1px 0; padding: 12px 16px;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 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 { li a:first-child, li b {
background-color: #8f05a5; background-color: var(--color-primary);
font-weight: bold; font-weight: 600;
color: white; color: var(--color-white);
text-decoration:none; text-decoration: none;
padding: 2px 5px; padding: 6px 12px;
text-shadow: 2px 2px 1px black; border-radius: 6px;
cursor:pointer; 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 { li strong {
color: red; color: var(--color-primary);
font-weight: 600;
} }
input { input {
height:35px; height: 40px;
font-size:14px; font-size: 14px;
padding-left: .3em; 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 { label + a {
text-decoration: none; text-decoration: none;
} }
h1 + main { h1 + main {
display: flex; display: flex;
} width: 100%;
gap: 20px;
}
aside { aside {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.2em; padding: 0;
background-color: var(--color-white);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
min-width: 200px;
} }
button { button {
height:40px; height: 40px;
width:130px; font-size: 16px;
font-size:16px;
margin-top: 1em; margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 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 { div button {
background-color: #7bff97; background-color: var(--color-primary);
color: var(--color-white);
} }
div button:hover {
background-color: #5FA03A;
}
nav { nav {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; 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 { #left {
align-items:flex-end; align-items: flex-end;
text-shadow: 0.5px 0.5px 1px #757474; text-shadow: none;
color: var(--color-dark);
} }
#cr { #cr {
font-weight: bold; font-weight: 600;
cursor:pointer; cursor: pointer;
font-size: 1.5em; font-size: 1.5em;
color: var(--color-primary);
} }
#up { #up {
width: auto; width: auto;
} }
.note { .note {
background-color: #fecdee; background-color: #E8F5E9;
padding: 0.5em; padding: 15px;
margin-top: 1em; margin-top: 1em;
text-align: center; text-align: center;
max-width: 320px; max-width: 400px;
border-radius: 0.5em; border-radius: var(--border-radius);
border-left: 4px solid var(--color-primary);
box-shadow: var(--shadow);
color: var(--color-dark);
} }
.no { .no {
display: none; display: none;
} }
form [title] { form [title] {
background-color: skyblue; background-color: var(--color-primary);
color: var(--color-white);
font-size: 1em; font-size: 1em;
width: 120px; 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) { form:nth-of-type(2) {
margin-bottom: 1em; margin-bottom: 1em;
} }
[value*=Format] { [value*=Format] {
margin-top: 1em; margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7); box-shadow: var(--shadow);
border-radius: var(--border-radius);
background-color: var(--color-white);
border: 2px solid var(--color-primary);
padding: 12px;
} }
[name="group"] { [name="group"] {
display: none; display: none;
} }
[name="group"] + label { [name="group"] + label {
font-size: 1.5em; font-size: 1.1em;
margin-right: 5px; margin-right: 10px;
font-weight: 600;
color: var(--color-dark);
cursor: pointer;
user-select: none;
} }
[name="group"] + label::before { [name="group"] + label::before {
content: "\002610"; content: "\002610";
} margin-right: 8px;
color: var(--color-primary);
}
[name="group"]:checked + label::before { [name="group"]:checked + label::before {
content: '\002611\0027A5'; content: '\002611';
color: var(--color-primary);
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
.ip { body {
right: 6em; padding: 10px;
position: relative;
} }
h1 + main {
flex-direction: column;
}
.ip {
position: relative;
right: 0;
}
aside { aside {
max-width: 50vw; max-width: 100%;
width: 100%;
}
nav {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
button {
width: 100%;
}
.note {
max-width: 100%;
} }
} }

View File

@ -7,6 +7,3 @@ lib_deps =
adafruit/Adafruit NeoPixel @ ^1.11.0 adafruit/Adafruit NeoPixel @ ^1.11.0
https://github.com/RobTillaart/PCF8575 https://github.com/RobTillaart/PCF8575
bblanchon/ArduinoJson @ ^6.21.0 bblanchon/ArduinoJson @ ^6.21.0
board_build.filesystem = littlefs
board_upload.maximum_size = 1048576

View File

@ -2,6 +2,7 @@
#include <PCF8575.h> #include <PCF8575.h>
#include <Adafruit_NeoPixel.h> #include <Adafruit_NeoPixel.h>
#include <ESP8266WebServer.h> #include <ESP8266WebServer.h>
#include <ESP8266HTTPUpdateServer.h> // for web-based firmware upload
#include <ArduinoOTA.h> #include <ArduinoOTA.h>
#include <LittleFS.h> #include <LittleFS.h>
#include "Config.ino" #include "Config.ino"
@ -10,6 +11,7 @@
// Globale Deklarationen für alle Tabs // Globale Deklarationen für alle Tabs
ESP8266WebServer server(80); ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater; // provides /update endpoint
const byte DNS_PORT = 53; const byte DNS_PORT = 53;
DNSServer dnsServer; DNSServer dnsServer;
@ -131,10 +133,33 @@ void setup() {
loadPortConfig(); // Lade Port-Konfiguration loadPortConfig(); // Lade Port-Konfiguration
connectWifi(); connectWifi();
admin(); admin();
ArduinoOTA.onStart([]() {
// 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 //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(); ArduinoOTA.begin();
// allow firmware upload via HTTP (web page)
httpUpdater.setup(&server); // no auth
// httpUpdater.setup(&server, "admin", "secret"); // with basic auth
server.begin(); server.begin();
// Handler für Port-Konfiguration // Handler für Port-Konfiguration

View File

@ -1,120 +1,284 @@
/* For more information visit:https://fipsok.de */ /* 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 { body {
font-family: sans-serif; font-family: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background-color: #87cefa; background-color: var(--color-light);
color: var(--color-dark);
display: flex; display: flex;
flex-flow: column; flex-flow: column;
align-items: center; align-items: center;
margin: 0;
padding: 20px;
line-height: 1.6;
} }
h1,h2 {
color: #e1e1e1; h1, h2 {
text-shadow: 2px 2px 2px black; 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 { li {
background-color: #feb1e2; background-color: var(--color-white);
list-style-type: none; list-style-type: none;
margin-bottom: 10px; margin-bottom: 12px;
padding: 2px 5px 1px 0; padding: 12px 16px;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 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 { li a:first-child, li b {
background-color: #8f05a5; background-color: var(--color-primary);
font-weight: bold; font-weight: 600;
color: white; color: var(--color-white);
text-decoration:none; text-decoration: none;
padding: 2px 5px; padding: 6px 12px;
text-shadow: 2px 2px 1px black; border-radius: 6px;
cursor:pointer; 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 { li strong {
color: red; color: var(--color-primary);
font-weight: 600;
} }
input { input {
height:35px; height: 40px;
font-size:14px; font-size: 14px;
padding-left: .3em; 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 { label + a {
text-decoration: none; text-decoration: none;
} }
h1 + main { h1 + main {
display: flex; display: flex;
} width: 100%;
gap: 20px;
}
aside { aside {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0.2em; padding: 0;
background-color: var(--color-white);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
min-width: 200px;
} }
button { button {
height:40px; height: 40px;
width:130px; font-size: 16px;
font-size:16px;
margin-top: 1em; margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7); 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 { div button {
background-color: #7bff97; background-color: var(--color-primary);
color: var(--color-white);
} }
div button:hover {
background-color: #5FA03A;
}
nav { nav {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; 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 { #left {
align-items:flex-end; align-items: flex-end;
text-shadow: 0.5px 0.5px 1px #757474; text-shadow: none;
color: var(--color-dark);
} }
#cr { #cr {
font-weight: bold; font-weight: 600;
cursor:pointer; cursor: pointer;
font-size: 1.5em; font-size: 1.5em;
color: var(--color-primary);
} }
#up { #up {
width: auto; width: auto;
} }
.note { .note {
background-color: #fecdee; background-color: #E8F5E9;
padding: 0.5em; padding: 15px;
margin-top: 1em; margin-top: 1em;
text-align: center; text-align: center;
max-width: 320px; max-width: 400px;
border-radius: 0.5em; border-radius: var(--border-radius);
border-left: 4px solid var(--color-primary);
box-shadow: var(--shadow);
color: var(--color-dark);
} }
.no { .no {
display: none; display: none;
} }
form [title] { form [title] {
background-color: skyblue; background-color: var(--color-primary);
color: var(--color-white);
font-size: 1em; font-size: 1em;
width: 120px; 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) { form:nth-of-type(2) {
margin-bottom: 1em; margin-bottom: 1em;
} }
[value*=Format] { [value*=Format] {
margin-top: 1em; margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7); box-shadow: var(--shadow);
border-radius: var(--border-radius);
background-color: var(--color-white);
border: 2px solid var(--color-primary);
padding: 12px;
} }
[name="group"] { [name="group"] {
display: none; display: none;
} }
[name="group"] + label { [name="group"] + label {
font-size: 1.5em; font-size: 1.1em;
margin-right: 5px; margin-right: 10px;
font-weight: 600;
color: var(--color-dark);
cursor: pointer;
user-select: none;
} }
[name="group"] + label::before { [name="group"] + label::before {
content: "\002610"; content: "\002610";
} margin-right: 8px;
color: var(--color-primary);
}
[name="group"]:checked + label::before { [name="group"]:checked + label::before {
content: '\002611\0027A5'; content: '\002611';
color: var(--color-primary);
} }
@media only screen and (max-width: 500px) { @media only screen and (max-width: 500px) {
.ip { body {
right: 6em; padding: 10px;
position: relative;
} }
h1 + main {
flex-direction: column;
}
.ip {
position: relative;
right: 0;
}
aside { aside {
max-width: 50vw; max-width: 100%;
width: 100%;
}
nav {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
button {
width: 100%;
}
.note {
max-width: 100%;
} }
} }