From c39fd04b078aef1a99a6d7a1971911a31dcedd99 Mon Sep 17 00:00:00 2001 From: toptah Date: Thu, 12 Mar 2026 21:09:02 +0100 Subject: [PATCH] Anpassung - Readme mit Beschreibug der Software - style.css mit modernerem look und HOBBYHIMMEL Farben --- README.md | 131 +++++++++++++++++++++++ data/style.css | 260 ++++++++++++++++++++++++++++++++++++--------- platformio.ini | 3 - src/KeyPatch.ino | 27 ++++- src/html/style.css | 260 ++++++++++++++++++++++++++++++++++++--------- 5 files changed, 581 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index c9f035f..2288e4e 100644 --- a/README.md +++ b/README.md @@ -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 + +--- diff --git a/data/style.css b/data/style.css index 45838e8..3514187 100644 --- a/data/style.css +++ b/data/style.css @@ -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%; } } diff --git a/platformio.ini b/platformio.ini index e555431..eddfae6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 \ No newline at end of file diff --git a/src/KeyPatch.ino b/src/KeyPatch.ino index ad969df..43c37b3 100644 --- a/src/KeyPatch.ino +++ b/src/KeyPatch.ino @@ -2,6 +2,7 @@ #include #include #include +#include // for web-based firmware upload #include #include #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 diff --git a/src/html/style.css b/src/html/style.css index 45838e8..3514187 100644 --- a/src/html/style.css +++ b/src/html/style.css @@ -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%; } }