Compare commits
4
Commits
a1de1924e7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f23c784ee | ||
|
|
c39fd04b07 | ||
|
|
1bf8c6545f | ||
|
|
2348fc59c7 |
@@ -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.
|
||||||
|
---
|
||||||
@@ -57,6 +57,138 @@ 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
|
||||||
|
- 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
Binary file not shown.
+146
@@ -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>
|
||||||
@@ -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="/"> 📁</label><span id="cr">+📁</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> 📁 ${f} <a href="?delete=/${f}">🗑️</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 ? '▼' : '▲'} 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 “ % & / : ; \ sind nicht erlaubt." required="">
|
||||||
|
<button>Create</button>
|
||||||
|
</form>
|
||||||
|
<main></main>
|
||||||
|
<form action="/format" method="POST">
|
||||||
|
<button id="btn">Format LittleFS</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
platform = espressif8266
|
platform = espressif8266
|
||||||
board = d1_mini
|
board = d1_mini
|
||||||
framework = arduino
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
lib_deps =
|
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
|
||||||
|
|||||||
+101
@@ -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
@@ -4,5 +4,9 @@
|
|||||||
|
|
||||||
#define NEOPIXEL_PIN D6
|
#define NEOPIXEL_PIN D6
|
||||||
#define BRIGHTNESS 100
|
#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
@@ -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
@@ -1,21 +1,103 @@
|
|||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
#include <PCF8575.h>
|
#include <PCF8575.h>
|
||||||
#include <Adafruit_NeoPixel.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 "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
|
#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);
|
PCF8575 expander1(EXPANDER1ADDRESS);
|
||||||
uint8_t portStates[AMOUNTOFPORTS];
|
uint8_t portStates[AMOUNTOFPORTS];
|
||||||
uint8_t lastStates[AMOUNTOFPORTS] = {0};
|
uint8_t lastStates[AMOUNTOFPORTS] = {0};
|
||||||
|
PortConfig portConfigs[AMOUNTOFPORTS];
|
||||||
byte number=0;
|
byte number=0;
|
||||||
|
|
||||||
extern Adafruit_NeoPixel* pixels;
|
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() {
|
void setup() {
|
||||||
Serial.begin(9600);
|
Serial.begin(SERIAL_SPEED);
|
||||||
Serial.println("Starte 16-Port IO Erweiterung...");
|
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
|
//Set UP 12C Communication
|
||||||
Wire.begin();
|
Wire.begin();
|
||||||
for (byte adress=8; adress<120; adress++)
|
for (byte adress=8; adress<120; adress++)
|
||||||
@@ -57,10 +139,125 @@ void setup() {
|
|||||||
};
|
};
|
||||||
pixels->show();
|
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...");
|
Serial.println("Setup End...");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
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
|
// Erst 16 Ports aus PCF8575 einlesen
|
||||||
for (uint8_t i = 0; i < AMOUNTOFPORTS; i++) {
|
for (uint8_t i = 0; i < AMOUNTOFPORTS; i++) {
|
||||||
portStates[i] = expander1.read(i);
|
portStates[i] = expander1.read(i);
|
||||||
@@ -76,15 +273,19 @@ void loop() {
|
|||||||
lastStates[i] = portStates[i];
|
lastStates[i] = portStates[i];
|
||||||
}
|
}
|
||||||
// LED aktualisieren
|
// LED aktualisieren
|
||||||
if (portStates[i] == 0) {
|
if (portConfigs[i].enabled) {
|
||||||
pixels->setPixelColor(i, pixels->Color(0, 150, 0)); // grün
|
if (portStates[i] == 0) {
|
||||||
} else {
|
pixels->setPixelColor(i, pixels->Color(0, 150, 0)); // grün
|
||||||
bool globalBlinkState = (millis() / BLINK_INTERVAL) % 2;
|
|
||||||
if (globalBlinkState) {
|
|
||||||
pixels->setPixelColor(i, pixels->Color(150, 0, 0)); // rot
|
|
||||||
} else {
|
} 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();
|
pixels->show();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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="/"> 📁</label><span id="cr">+📁</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> 📁 ${f} <a href="?delete=/${f}">🗑️</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 ? '▼' : '▲'} 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 “ % & / : ; \ sind nicht erlaubt." required="">
|
||||||
|
<button>Create</button>
|
||||||
|
</form>
|
||||||
|
<main></main>
|
||||||
|
<form action="/format" method="POST">
|
||||||
|
<button id="btn">Format LittleFS</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user