Hinzufügen WebServer und AP zur SSID Konfiguration

This commit is contained in:
toptah 2026-03-09 20:05:35 +01:00
parent a1de1924e7
commit 2348fc59c7
12 changed files with 975 additions and 3 deletions

50
.github/copilot-instructions.md vendored Normal file
View File

@ -0,0 +1,50 @@
---
# Workspace instructions for the KeyPatch ESP8266 project
This project is a PlatformIO/Arduino sketch for an ESP8266 (Wemos D1 mini) that
implements a captive-portal WiFi 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 softAP 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 WiFi 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.
---

BIN
compile.log Normal file

Binary file not shown.

View File

@ -2,6 +2,7 @@
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
lib_deps =
adafruit/Adafruit NeoPixel @ ^1.11.0
https://github.com/RobTillaart/PCF8575

101
src/Admin.ino Normal file
View File

@ -0,0 +1,101 @@
// ****************************************************************
// Sketch Esp8266 Admin Modular(Tab)
// created: Jens Fleischer, 2019-12-17
// last mod: Jens Fleischer, 2021-06-09
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.1 - 3.1.0
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2019 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Admin sollte als Tab eingebunden werden.
// #include <LittleFS.h>/#include <FS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// Die Spiffs.ino muss im ESP8266 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach setupFS()/spiffs() und dem Verbindungsaufbau aufgerufen werden.
// Die Funktion "runtime();" muss mindestens zweimal innerhalb 49 Tage aufgerufen werden.
// Entweder durch den Client(Webseite) oder zur Sicherheit im "loop();"
/**************************************************************************************/
//#define LittleFS SPIFFS // Einkommentieren wenn SPIFFS als Filesystem genutzt wird
const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};
void admin() { // Funktionsaufruf "admin();" muss im Setup eingebunden werden
File file = LittleFS.open("/config.json", "r");
if (file) {
String newhostname = file.readStringUntil('\n');
if (newhostname != "") {
WiFi.hostname(newhostname.substring(1, newhostname.length() - 1));
file.close();
ArduinoOTA.setHostname(WiFi.hostname().c_str());
}
}
server.on("/admin/renew", handlerenew);
server.on("/admin/once", handleonce);
server.on("/reconnect", []() {
server.send(304, "message/http");
WiFi.reconnect();
});
server.on("/restart", []() {
server.send(304, "message/http");
//save(); //Wenn Werte vor dem Neustart gespeichert werden sollen
ESP.restart();
});
}
//Es kann entweder die Spannung am ADC-Pin oder die Modulversorgungsspannung (VCC) ausgegeben werden.
void handlerenew() { // Um die am ADC-Pin anliegende externe Spannung zu lesen, verwende analogRead (A0)
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + analogRead(A0) + "\"]"); // Json als Array
}
/*
ADC_MODE(ADC_VCC);
void handlerenew() { // Zum Lesen der Modulversorgungsspannung (VCC), verwende ESP.getVcc()
server.send(200, "application/json", "[\"" + runtime() + "\",\"" + WiFi.RSSI() + "\",\"" + ESP.getVcc() / 1024.0 + " V" + "\"]");
}
*/
void handleonce() {
if (server.arg(0) != "") {
WiFi.hostname(server.arg(0));
File f = LittleFS.open("/config.json", "w"); // Datei zum schreiben öffnen
f.printf("\"%s\"\n", WiFi.hostname().c_str());
f.close();
}
String temp = "{\"File\":\"" + sketchName() + "\", \"Build\":\"" + __DATE__ + " " + __TIME__ + "\", \"SketchSize\":\"" + formatBytes(ESP.getSketchSize()) +
"\", \"SketchSpace\":\"" + formatBytes(ESP.getFreeSketchSpace()) + "\", \"LocalIP\":\"" + WiFi.localIP().toString() +
"\", \"Hostname\":\"" + WiFi.hostname() + "\", \"SSID\":\"" + WiFi.SSID() + "\", \"GatewayIP\":\"" + WiFi.gatewayIP().toString() +
"\", \"Channel\":\"" + WiFi.channel() + "\", \"MacAddress\":\"" + WiFi.macAddress() + "\", \"SubnetMask\":\"" + WiFi.subnetMask().toString() +
"\", \"BSSID\":\"" + WiFi.BSSIDstr() + "\", \"ClientIP\":\"" + server.client().remoteIP().toString() + "\", \"DnsIP\":\"" + WiFi.dnsIP().toString() +
"\", \"ResetReason\":\"" + ESP.getResetReason() + "\", \"CpuFreqMHz\":\"" + F_CPU / 1000000 + "\", \"FreeHeap\":\"" + formatBytes(ESP.getFreeHeap()) +
"\", \"HeapFrag\":\"" + ESP.getHeapFragmentation() + "\", \"ChipSize\":\"" + formatBytes(ESP.getFlashChipSize()) +
"\", \"ChipSpeed\":\"" + ESP.getFlashChipSpeed() / 1000000 + "\", \"ChipMode\":\"" + flashChipMode[ESP.getFlashChipMode()] +
"\", \"IdeVersion\":\"" + ARDUINO + "\", \"CoreVersion\":\"" + ESP.getCoreVersion() + "\", \"SdkVersion\":\"" + ESP.getSdkVersion() + "\"}";
server.send(200, "application/json", temp); // Json als Objekt
}
String runtime() {
static uint8_t rolloverCounter;
static uint32_t previousMillis;
uint32_t currentMillis {millis()};
if (currentMillis < previousMillis) rolloverCounter++; // prüft millis() auf Überlauf
previousMillis = currentMillis;
uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
char buf[20];
snprintf(buf, sizeof(buf), "%*.d %.*s %02d:%02d:%02d",
sec < 86400 ? 0 : 1, sec / 86400, sec < 86400 ? 0 : sec >= 172800 ? 4 : 3, "Tage", sec / 3600 % 24, sec / 60 % 60, sec % 60);
return buf;
}

View File

@ -5,4 +5,5 @@
#define NEOPIXEL_PIN D6
#define BRIGHTNESS 100
#define BLINK_INTERVAL 500 // Blink interval in milliseconds (0.5 seconds)
#define SERIAL_SPEED 115200 // Serial Baudrate for ESP8266

185
src/Connect.ino Normal file
View File

@ -0,0 +1,185 @@
// ****************************************************************
// Sketch Esp8266 Login Manager mit Captive Portal und optischer Anzeige
// created: Jens Fleischer, 2021-01-05
// last mod: Jens Fleischer, 2021-11-29
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.3 / 2.7.4 / 3.0.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro
/******************************************************************
Copyright (c) 2021 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von Login Manager sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers und des LittleFS Tab ist erforderlich.
// Die Funktion "connectWifi();" muss im Setup eingebunden werden.
// Die Oneboard LED blinkt beim Verbindungsaufbau zum Netzwerk und leuchtet im AP Modus dauerhaft.
// Die Zugangsdaten werden nicht menschenlesbar im Dateisystem gespeichert.
/**************************************************************************************/
/**
Folgendes muss im Webserver Tab vor dem "setup()" eingefügt werden.
#include <DNSServer.h>
const byte DNS_PORT = 53;
DNSServer dnsServer;
Der DNS Server muss im loop aufgerufen werden.
void loop() {
dnsServer.processNextRequest();
reStation();
}
*/
//#define CONFIG // Einkommentieren wenn der ESP dem Router die IP mitteilen soll.
#ifdef CONFIG
IPAddress staticIP(192, 168, 178, 99); // statische IP des NodeMCU ESP8266
IPAddress gateway(192, 168, 178, 1); // IP-Adresse des Router
IPAddress subnet(255, 255, 255, 0); // Subnetzmaske des Netzwerkes
IPAddress dns(192, 168, 178, 1); // DNS Server
#endif
const char HTML[] PROGMEM = R"(<!DOCTYPE HTML>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
button{width:11em;height:2.5em}
body{background: #87cefa; text-align: center;}
</style>
<title>Login Manager</title>
</head>
<body>
<h2>Zugangsdaten</h2>
<form>
<p>
<label>SSID:<br>
<input name="ssid" placeholder="Name vom Netzwerk" required>
</label>
</p>
<p>
<label>Passwort:<br>
<input name="passwort" pattern="[!-~]{8,64}" placeholder="PW vom Netzwerk" required>
</label>
</p>
</form>
<button>
Absenden
</button>
<script>
document.querySelector('button').addEventListener('click', async () =>{
let elem = document.querySelector('form');
if(elem.checkValidity() && document.querySelector('[pattern]').checkValidity()){
let resp = await fetch('/wifisave', {method: 'post', body: new FormData(elem)});
let json = await resp.json();
document.body.innerHTML = json;
}
});
</script>
</body>
</html>)";
const char JSON[] PROGMEM = R"("<h3>Die Zugangsdaten wurden übertragen. Eine Verbindung zum Netzwerk wird hergestellt.</h3>")";
char ssid[33] {" "};
char password[65];
constexpr char key {129};
void connectWifi() {
IPAddress apIP(172, 217, 28, 1);
IPAddress netMsk(255, 255, 255, 0);
File file = LittleFS.open("/wifi.dat", "r");
if (file) {
file.read(reinterpret_cast<uint8_t*>(&ssid), sizeof(ssid));
file.read(reinterpret_cast<uint8_t*>(&password), sizeof(password));
file.close();
for (auto &c : ssid) c ^= key; // Dechiffrierung SSID
for (auto &c : password) c ^= key; // Dechiffrierung Passwort
}
WiFi.disconnect();
WiFi.persistent(false); // Auskommentieren wenn Netzwerkname und Passwort in den Flash geschrieben werden sollen.
WiFi.mode(WIFI_STA); // Station-Modus
WiFi.begin(ssid, password);
#ifdef CONFIG
WiFi.config(staticIP, gateway, subnet, dns);
#endif
uint8_t i {0};
while (WiFi.status() != WL_CONNECTED) {
pinMode(LED_BUILTIN, OUTPUT); // OnBoardLed Nodemcu, Wemos D1 Mini Pro
digitalWrite(LED_BUILTIN, 0); // Led blinkt während des Verbindungsaufbaus
delay(500);
digitalWrite(LED_BUILTIN, 1);
delay(500);
Serial.printf(" %i sek\n", ++i);
if (WiFi.status() == WL_NO_SSID_AVAIL || i > 29) { // Ist die SSID nicht erreichbar, wird ein eigenes Netzwerk erstellt.
digitalWrite(LED_BUILTIN, 0); // Dauerleuchten der Led zeigt den AP Modus an.
WiFi.disconnect();
WiFi.mode(WIFI_AP); // Soft-Access-Point-Modus
Serial.println(PSTR("\nVerbindung zum Router fehlgeschlagen !\nStarte Soft AP"));
WiFi.softAPConfig(apIP, apIP, netMsk);
if (WiFi.softAP("EspConfig")) {
Serial.println(PSTR("Verbinde dich mit dem Netzwerk \"EspConfig\".\n"));
}
break;
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf(PSTR("\nVerbunden mit: %s\nEsp8266 IP: %s\n"), WiFi.SSID().c_str(), WiFi.localIP().toString().c_str());
}
dnsServer.start(DNS_PORT, "*", apIP);
server.on("/wifisave", HTTP_POST, handleWifiSave);
server.onNotFound(handleRoot);
}
void handleWifiSave() {
if (server.hasArg("ssid") && server.hasArg("passwort")) {
strcpy(ssid, server.arg(0).c_str());
strcpy(password, server.arg(1).c_str());
for (auto &c : ssid) c ^= key; // Chiffrierung SSID
for (auto &c : password) c ^= key; // Chiffrierung Passwort
File file = LittleFS.open("/wifi.dat", "w");
file.write(reinterpret_cast<uint8_t*>(&ssid), sizeof(ssid));
file.write(reinterpret_cast<uint8_t*>(&password), sizeof(password));
file.close();
server.send(200, "application/json", JSON);
delay(500);
connectWifi();
}
}
void handleRoot() {
if (WiFi.status() != WL_CONNECTED) { // Besteht keine Verbindung zur Station wird das Formular gesendet.
server.sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
server.sendHeader("Pragma", "no-cache");
server.sendHeader("Expires", "-1");
server.send(200, "text/html", HTML);
}
else {
if (!handleFile(server.urlDecode(server.uri()))) {
if (server.urlDecode(server.uri()).endsWith("/")) sendResponce();
server.send(404, "text/plain", "FileNotFound");
}
}
}
void reStation() { // Der Funktionsaufruf "reStation();" sollte im "loop" stehen.
static unsigned long previousMillis; // Nach Stromausfall startet der Esp.. schneller als der Router.
constexpr unsigned long INTERVAL (3e5); // Im AP Modus aller 5 Minuten prüfen ob der Router verfügbar ist.
if (millis() - previousMillis >= INTERVAL) {
previousMillis += INTERVAL;
if (WiFi.status() != WL_CONNECTED) connectWifi();
}
}

View File

@ -1,9 +1,24 @@
#include <Wire.h>
#include <PCF8575.h>
#include <Adafruit_NeoPixel.h>
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h>
#include <LittleFS.h>
#include "Config.ino"
#include <DNSServer.h>
#define AMOUNTOFPORTS 8 // currently max 16 ports are supported
// Globale Deklarationen für alle Tabs
ESP8266WebServer server(80);
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
PCF8575 expander1(EXPANDER1ADDRESS);
@ -14,8 +29,12 @@ byte number=0;
extern Adafruit_NeoPixel* pixels;
void setup() {
Serial.begin(9600);
Serial.println("Starte 16-Port IO Erweiterung...");
Serial.begin(SERIAL_SPEED);
delay(100);
Serial.printf("\nSketchname: %s\nBuild: %s\t\tIDE: %d.%d.%d\n%s\n\n",
(__FILE__), (__TIMESTAMP__), ARDUINO / 10000, ARDUINO % 10000 / 100, ARDUINO % 100 / 10 ? ARDUINO % 100 : ARDUINO % 10, ESP.getFullVersion().c_str());
Serial.printf("\nKeyPatch ESP8266 WebServer\n");
//Set UP 12C Communication
Wire.begin();
for (byte adress=8; adress<120; adress++)
@ -57,10 +76,29 @@ void setup() {
};
pixels->show();
// SetUp WebServer
setupFS(); // setupFS(); oder spiffs(); je nach Dateisystem
connectWifi();
admin();
ArduinoOTA.onStart([]() {
//save(); // Wenn Werte vor dem Neustart gespeichert werden sollen
});
ArduinoOTA.begin();
server.begin();
Serial.println("Setup End...");
}
void loop() {
ArduinoOTA.handle();
server.handleClient();
if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) { // Die Funktion "runtime()" wird nur für den Admin Tab gebraucht.
runtime(); // Auskommentieren falls du den Admin Tab nicht nutzen möchtest.
}
dnsServer.processNextRequest();
reStation();
// Erst 16 Ports aus PCF8575 einlesen
for (uint8_t i = 0; i < AMOUNTOFPORTS; i++) {
portStates[i] = expander1.read(i);

173
src/LittleFS.ino Normal file
View File

@ -0,0 +1,173 @@
// ****************************************************************
// Sketch Esp8266 Filesystem Manager spezifisch sortiert Modular(Tab)
// created: Jens Fleischer, 2020-06-08
// last mod: Jens Fleischer, 2020-09-02
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 2.7.4
// Getestet auf: Nodemcu
/******************************************************************
Copyright (c) 2020 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Diese Version von LittleFS sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <ESP8266WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP8266 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP8266 Webserver stehen.
// Die Funktion "setupFS();" muss im Setup aufgerufen werden.
/**************************************************************************************/
#include <list>
#include <tuple>
const char WARNING[] PROGMEM = R"(<h2>Der Sketch wurde mit "FS:none" kompilliert!)";
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="[]" multiple><button>Upload</button></form>Lade die fs.html hoch.)";
void setupFS() { // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden
LittleFS.begin();
server.on("/format", formatFS);
server.on("/upload", HTTP_POST, sendResponce, handleUpload);
server.onNotFound([]() {
if (!handleFile(server.urlDecode(server.uri())))
server.send(404, "text/plain", "FileNotFound");
});
}
bool handleList() { // Senden aller Daten an den Client
FSInfo fs_info; LittleFS.info(fs_info); // Füllt FSInfo Struktur mit Informationen über das Dateisystem
Dir dir = LittleFS.openDir("/");
using namespace std;
typedef tuple<String, String, int> records;
list<records> dirList;
while (dir.next()) { // Ordner und Dateien zur Liste hinzufügen
if (dir.isDirectory()) {
uint8_t ran {0};
Dir fold = LittleFS.openDir(dir.fileName());
while (fold.next()) {
ran++;
dirList.emplace_back(dir.fileName(), fold.fileName(), fold.fileSize());
}
if (!ran) dirList.emplace_back(dir.fileName(), "", 0);
}
else {
dirList.emplace_back("", dir.fileName(), dir.fileSize());
}
}
dirList.sort([](const records & f, const records & l) { // Dateien sortieren
if (server.arg(0) == "1") {
return get<2>(f) > get<2>(l);
} else {
for (uint8_t i = 0; i < 31; i++) {
if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) return true;
else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) return false;
}
return false;
}
});
dirList.sort([](const records & f, const records & l) { // Ordner sortieren
if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) {
for (uint8_t i = 0; i < 31; i++) {
if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) return true;
else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) return false;
}
}
return false;
});
String temp = "[";
for (auto& t : dirList) {
if (temp != "[") temp += ',';
temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + formatBytes(get<2>(t)) + "\"}";
}
temp += ",{\"usedBytes\":\"" + formatBytes(fs_info.usedBytes) + // Berechnet den verwendeten Speicherplatz
"\",\"totalBytes\":\"" + formatBytes(fs_info.totalBytes) + // Zeigt die Größe des Speichers
"\",\"freeBytes\":\"" + (fs_info.totalBytes - fs_info.usedBytes) + "\"}]"; // Berechnet den freien Speicherplatz
server.send(200, "application/json", temp);
return true;
}
void deleteRecursive(const String &path) {
if (LittleFS.remove(path)) {
LittleFS.open(path.substring(0, path.lastIndexOf('/')) + "/", "w");
return;
}
Dir dir = LittleFS.openDir(path);
while (dir.next()) {
deleteRecursive(path + '/' + dir.fileName());
}
LittleFS.rmdir(path);
}
bool handleFile(String &&path) {
if (server.hasArg("new")) {
String folderName {server.arg("new")};
for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : folderName) if (e == c) e = 95; // Ersetzen der nicht erlaubten Zeichen
LittleFS.mkdir(folderName);
}
if (server.hasArg("sort")) return handleList();
if (server.hasArg("delete")) {
deleteRecursive(server.arg("delete"));
sendResponce();
return true;
}
if (!LittleFS.exists("fs.html")) server.send(200, "text/html", LittleFS.begin() ? HELPER : WARNING); // ermöglicht das hochladen der fs.html
if (path.endsWith("/")) path += "index.html";
if (path == "/spiffs.html") sendResponce(); // Vorrübergehend für den Admin Tab
return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); server.streamFile(f, getContentType(path)); f.close(); true;}) : false;
}
void handleUpload() { // Dateien ins Filesystem schreiben
static File fsUploadFile;
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
if (upload.filename.length() > 31) { // Dateinamen kürzen
upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length());
}
printf(PSTR("handleFileUpload Name: /%s\n"), upload.filename.c_str());
fsUploadFile = LittleFS.open(server.arg(0) + "/" + server.urlDecode(upload.filename), "w");
} else if (upload.status == UPLOAD_FILE_WRITE) {
printf(PSTR("handleFileUpload Data: %u\n"), upload.currentSize);
fsUploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
printf(PSTR("handleFileUpload Size: %u\n"), upload.totalSize);
fsUploadFile.close();
}
}
void formatFS() { // Formatiert das Filesystem
LittleFS.format();
sendResponce();
}
void sendResponce() {
server.sendHeader("Location", "fs.html");
server.send(303, "message/http");
}
const String formatBytes(size_t const& bytes) { // lesbare Anzeige der Speichergrößen
return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}
const String getContentType(const String & path) { // ermittelt den MIME-Type
using namespace mime;
char buff[sizeof(mimeTable[0].mimeType)];
for (size_t i = 0; i < maxType - 1; i++) {
strcpy_P(buff, mimeTable[i].endsWith);
if (path.endsWith(buff)) {
strcpy_P(buff, mimeTable[i].mimeType);
return static_cast<String>(buff);
}
}
strcpy_P(buff, mimeTable[maxType - 1].mimeType);
return static_cast<String>(buff);
}

54
src/Webserver.ino Normal file
View File

@ -0,0 +1,54 @@
// ****************************************************************
// Sketch Esp8266 Webserver Modular(Tab)
// created: Jens Fleischer, 2018-05-16
// last mod: Jens Fleischer, 2020-12-28
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.4.2 - 3.1.2
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
Copyright (c) 2018 Jens Fleischer. All rights reserved.
This file is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This file is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
*******************************************************************/
// Der WebServer Tab ist der Haupt Tab mit "setup" und "loop".
// #include <LittleFS.h> bzw. #include <FS.h> und #include <ESP8266WebServer.h>
// müssen im Haupttab aufgerufen werden.
// Ein Connect Tab ist erforderlich.
// Inklusive Arduino OTA-Updates (Erfordert freien Flash-Speicher)
/**************************************************************************************/
#include <ESP8266WebServer.h>
#include <ArduinoOTA.h> // https://arduino-esp8266.readthedocs.io/en/latest/ota_updates/readme.html
#include <LittleFS.h> // Library für Dateisystem LittleFS
#include <DNSServer.h>
//#include <FS.h> // Library für Dateisystem Spiffs einkommentieren wenn erforderlich
// ESP8266WebServer server(80); // Jetzt in KeyPatch.ino deklariert
// DNSServer dnsServer; // Jetzt in KeyPatch.ino deklariert
// const byte DNS_PORT = 53; // Jetzt in KeyPatch.ino deklariert
void setupWebserver() {
setupFS(); // Filesystem setup
ArduinoOTA.onStart([]() {
// Hier können Werte vor dem Neustart gespeichert werden
});
ArduinoOTA.begin();
}
void handleWebserver() {
ArduinoOTA.handle();
server.handleClient();
dnsServer.processNextRequest();
reStation();
}

172
src/html/admin.html Normal file
View File

@ -0,0 +1,172 @@
<!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', check.bind(this, document.querySelector('input')));
btn[3].addEventListener('click', re.bind(this, 'reconnect'));
btn[4].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);
span[3].innerHTML = obj['File'];
span[4].innerHTML = obj['Build'];
span[5].innerHTML = obj['SketchSize'];
span[6].innerHTML = obj['SketchSpace'];
span[7].innerHTML = obj['LocalIP'];
span[8].innerHTML = obj['Hostname'];
span[9].innerHTML = obj['SSID'];
span[10].innerHTML = obj['GatewayIP'];
span[11].innerHTML = obj['Channel'];
span[12].innerHTML = obj['MacAddress'];
span[13].innerHTML = obj['SubnetMask'];
span[14].innerHTML = obj['BSSID'];
span[15].innerHTML = obj['ClientIP'];
span[16].innerHTML = obj['DnsIP'];
span[17].innerHTML = obj['ResetReason'];
span[18].innerHTML = obj['CpuFreqMHz'] + " MHz";
span[19].innerHTML = obj['FreeHeap'];
span[20].innerHTML = obj['HeapFrag'] + "%";
span[21].innerHTML = obj['ChipSize'];
span[22].innerHTML = obj['ChipSpeed'] + " MHz";
span[23].innerHTML = obj['ChipMode'];
span[24].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
span[25].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
span[26].innerHTML = obj['SdkVersion'];
} catch(err) {
re();
}
}
async function renew() {
const resp = await fetch('admin/renew');
const array = await resp.json();
array.forEach((v, i) => {span[i].innerHTML = v});
}
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>
<aside id="left">
<span>Runtime ESP:</span>
<span>WiFi RSSI:</span>
<span>ADC/VCC:</span>
<span>Sketch Name:</span>
<span>Sketch Build:</span>
<span>SketchSize:</span>
<span>FreeSketchSpace:</span>
<span>IPv4 Address:</span>
<span>Hostname:</span>
<span>Connected to:</span>
<span>Gateway IP:</span>
<span>Channel:</span>
<span>MacAddress:</span>
<span>SubnetMask:</span>
<span>BSSID:</span>
<span>Client IP:</span>
<span>DnsIP:</span>
<span>Reset Ground:</span>
<span>CPU Freq:</span>
<span>FreeHeap:</span>
<span>Heap Fragmentation:</span>
<span>FlashSize:</span>
<span>FlashSpeed:</span>
<span>FlashMode:</span>
<span>Arduino IDE Version:</span>
<span>Esp Core Version:</span>
<span>SDK Version:</span>
</aside>
<aside id="right">
<span>0</span>
<div>
<span></span>
dBm
</div>
<span>0</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>?</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>?</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
<span>0</span>
</aside>
</main>
<div>
<button>Filesystem</button>
<button>Startseite</button>
</div>
<div id="note"></div>
<div>
<form>
<input placeholder="neuer Hostname" pattern="([A-Za-z0-9\-]{1,32})" title="Es dürfen nur Buchstaben (a-z, A-Z), Ziffern (0-9) und Bindestriche (-) enthalten sein. Maximal 32 Zeichen" required>
<button type="button">Name Senden</button>
</form>
</div>
<div>
<button>WiFi Reconnect</button>
<button>ESP Restart</button>
</div>
</body>
</html>

77
src/html/fs.html Normal file
View File

@ -0,0 +1,77 @@
<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="style.css">
<title>Filesystem Manager</title>
<script>
document.addEventListener('DOMContentLoaded', () => {
list(JSON.parse(localStorage.getItem('sortBy')));
btn.addEventListener('click', () => {
if (!confirm(`Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault();
});
});
async function list(to){
let resp = await fetch(`?sort=${to}`);
let json = await resp.json();
let myList = document.querySelector('main'), noted = '';
myList.innerHTML = '<nav><input type="radio" id="/" name="group" checked="checked"><label for="/"> &#128193;</label><span id="cr">+&#128193;</nav></span><span id="si"></span>';
for (var i = 0; i < json.length - 1; i++) {
let dir = '', f = json[i].folder, n = json[i].name;
if (f != noted) {
noted = f;
dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> &#128193; ${f} <a href="?delete=/${f}">&#x1f5d1;&#xfe0f;</a></nav>`;
}
if (n != '') dir += `<li><a href="${f}/${n}">${n}</a><small> ${json[i].size}</small><a href="${f}/${n}"download="${n}"> Download</a> or<a href="?delete=${f}/${n}"> Delete</a>`;
myList.insertAdjacentHTML('beforeend', dir);
}
myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '&#9660;' : '&#9650;'} LittleFS</b> belegt ${json[i].usedBytes.replace(".00", "")} von ${json[i].totalBytes.replace(".00", "")}`);
var free = json[i].freeBytes;
cr.addEventListener('click', () => {
document.getElementById('no').classList.toggle('no');
});
so.addEventListener('click', () => {
list(to=++to%2);
localStorage.setItem('sortBy', JSON.stringify(to));
});
document.addEventListener('change', (e) => {
if (e.target.id == 'fs') {
for (var bytes = 0, i = 0; i < event.target.files.length; i++) bytes += event.target.files[i].size;
for (var output = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) output = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
if (bytes > free) {
si.innerHTML = `<li><b> ${output}</b><strong> Ungenügend Speicher frei</strong></li>`;
up.setAttribute('disabled', 'disabled');
}
else {
si.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
up.removeAttribute('disabled');
}
}
document.querySelectorAll(`input[type=radio]`).forEach(el => { if (el.checked) document.querySelector('form').setAttribute('action', '/upload?f=' + el.id)});
});
document.querySelectorAll('[href^="?delete=/"]').forEach(node => {
node.addEventListener('click', () => {
if (!confirm('Sicher!')) event.preventDefault();
});
});
}
</script>
</head>
<body>
<h2>ESP8266 Filesystem Manager</h2>
<form method="post" enctype="multipart/form-data" action="/upload?f=/">
<input id="fs" type="file" name="up[]" multiple>
<button id="up" disabled>Upload</button>
</form>
<form id="no" class="no" method="POST">
<input name="new" placeholder="Ordner Name" pattern="[^\x22\/%&\\:;]{0,31}[^\x22\/%&\\:;\s]{1}" title="Zeichen &#8220; % & / : ; \ sind nicht erlaubt." required="">
<button>Create</button>
</form>
<main></main>
<form action="/format" method="POST">
<button id="btn">Format LittleFS</button>
</form>
</body>
</html>

120
src/style.css Normal file
View File

@ -0,0 +1,120 @@
/* For more information visit:https://fipsok.de */
body {
font-family: sans-serif;
background-color: #87cefa;
display: flex;
flex-flow: column;
align-items: center;
}
h1,h2 {
color: #e1e1e1;
text-shadow: 2px 2px 2px black;
}
li {
background-color: #feb1e2;
list-style-type: none;
margin-bottom: 10px;
padding: 2px 5px 1px 0;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
li a:first-child, li b {
background-color: #8f05a5;
font-weight: bold;
color: white;
text-decoration:none;
padding: 2px 5px;
text-shadow: 2px 2px 1px black;
cursor:pointer;
}
li strong {
color: red;
}
input {
height:35px;
font-size:14px;
padding-left: .3em;
}
label + a {
text-decoration: none;
}
h1 + main {
display: flex;
}
aside {
display: flex;
flex-direction: column;
padding: 0.2em;
}
button {
height:40px;
width:130px;
font-size:16px;
margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
div button {
background-color: #7bff97;
}
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
}
#left {
align-items:flex-end;
text-shadow: 0.5px 0.5px 1px #757474;
}
#cr {
font-weight: bold;
cursor:pointer;
font-size: 1.5em;
}
#up {
width: auto;
}
.note {
background-color: #fecdee;
padding: 0.5em;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
}
.no {
display: none;
}
form [title] {
background-color: skyblue;
font-size: 1em;
width: 120px;
}
form:nth-of-type(2) {
margin-bottom: 1em;
}
[value*=Format] {
margin-top: 1em;
box-shadow: 5px 5px 5px rgba(0,0,0,0.7);
}
[name="group"] {
display: none;
}
[name="group"] + label {
font-size: 1.5em;
margin-right: 5px;
}
[name="group"] + label::before {
content: "\002610";
}
[name="group"]:checked + label::before {
content: '\002611\0027A5';
}
@media only screen and (max-width: 500px) {
.ip {
right: 6em;
position: relative;
}
aside {
max-width: 50vw;
}
}