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 |
| 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 {
font-family: sans-serif;
background-color: #87cefa;
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: #e1e1e1;
text-shadow: 2px 2px 2px black;
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: #feb1e2;
background-color: var(--color-white);
list-style-type: none;
margin-bottom: 10px;
padding: 2px 5px 1px 0;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
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: #8f05a5;
font-weight: bold;
color: white;
text-decoration:none;
padding: 2px 5px;
text-shadow: 2px 2px 1px black;
cursor:pointer;
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: red;
color: var(--color-primary);
font-weight: 600;
}
input {
height:35px;
font-size:14px;
padding-left: .3em;
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.2em;
padding: 0;
background-color: var(--color-white);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
min-width: 200px;
}
button {
height:40px;
width:130px;
font-size:16px;
height: 40px;
font-size: 16px;
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 {
background-color: #7bff97;
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: 0.5px 0.5px 1px #757474;
align-items: flex-end;
text-shadow: none;
color: var(--color-dark);
}
#cr {
font-weight: bold;
cursor:pointer;
font-weight: 600;
cursor: pointer;
font-size: 1.5em;
color: var(--color-primary);
}
#up {
width: auto;
width: auto;
}
.note {
background-color: #fecdee;
padding: 0.5em;
background-color: #E8F5E9;
padding: 15px;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
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: skyblue;
background-color: var(--color-primary);
color: var(--color-white);
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) {
margin-bottom: 1em;
}
[value*=Format] {
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"] {
display: none;
}
[name="group"] + label {
font-size: 1.5em;
margin-right: 5px;
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\0027A5';
content: '\002611';
color: var(--color-primary);
}
@media only screen and (max-width: 500px) {
.ip {
right: 6em;
position: relative;
body {
padding: 10px;
}
h1 + main {
flex-direction: column;
}
.ip {
position: relative;
right: 0;
}
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
https://github.com/RobTillaart/PCF8575
bblanchon/ArduinoJson @ ^6.21.0
board_build.filesystem = littlefs
board_upload.maximum_size = 1048576

View File

@ -2,6 +2,7 @@
#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"
@ -10,6 +11,7 @@
// Globale Deklarationen für alle Tabs
ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater; // provides /update endpoint
const byte DNS_PORT = 53;
DNSServer dnsServer;
@ -131,10 +133,33 @@ void setup() {
loadPortConfig(); // Lade Port-Konfiguration
connectWifi();
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
});
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

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 {
font-family: sans-serif;
background-color: #87cefa;
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: #e1e1e1;
text-shadow: 2px 2px 2px black;
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: #feb1e2;
background-color: var(--color-white);
list-style-type: none;
margin-bottom: 10px;
padding: 2px 5px 1px 0;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
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: #8f05a5;
font-weight: bold;
color: white;
text-decoration:none;
padding: 2px 5px;
text-shadow: 2px 2px 1px black;
cursor:pointer;
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: red;
color: var(--color-primary);
font-weight: 600;
}
input {
height:35px;
font-size:14px;
padding-left: .3em;
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.2em;
padding: 0;
background-color: var(--color-white);
border-radius: var(--border-radius);
padding: 20px;
box-shadow: var(--shadow);
min-width: 200px;
}
button {
height:40px;
width:130px;
font-size:16px;
height: 40px;
font-size: 16px;
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 {
background-color: #7bff97;
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: 0.5px 0.5px 1px #757474;
align-items: flex-end;
text-shadow: none;
color: var(--color-dark);
}
#cr {
font-weight: bold;
cursor:pointer;
font-weight: 600;
cursor: pointer;
font-size: 1.5em;
color: var(--color-primary);
}
#up {
width: auto;
width: auto;
}
.note {
background-color: #fecdee;
padding: 0.5em;
background-color: #E8F5E9;
padding: 15px;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
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: skyblue;
background-color: var(--color-primary);
color: var(--color-white);
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) {
margin-bottom: 1em;
}
[value*=Format] {
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"] {
display: none;
}
[name="group"] + label {
font-size: 1.5em;
margin-right: 5px;
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\0027A5';
content: '\002611';
color: var(--color-primary);
}
@media only screen and (max-width: 500px) {
.ip {
right: 6em;
position: relative;
body {
padding: 10px;
}
h1 + main {
flex-direction: column;
}
.ip {
position: relative;
right: 0;
}
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%;
}
}