Update: 2023-11-12

Die Spiffs.ino ist zum ausführen der Admin.ino erforderlich.

Esp32 Admin als Arduino Tab.

Admin.ino

// ****************************************************************
// Sketch Esp32 Admin Modular(Tab)
// created: Jens Fleischer, 2018-07-05
// last mod: Jens Fleischer, 2023-11-12
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 1.0.0 - 1.0.4
// Geprüft: bei 4MB Flash
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  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.
*******************************************************************/
// Diese Version von Admin sollte als Tab eingebunden werden.
// #include "SPIFFS.h" #include <WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// Die Spiffs.ino muss im ESP32 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach spiffs() aber vor 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();"
/**************************************************************************************/

#include <rom/rtc.h>

const char* const PROGMEM flashChipMode[] = {"QIO", "QOUT", "DIO", "DOUT", "Unbekannt"};
const char* const PROGMEM resetReason[] = {"ERR", "Power on", "Unknown", "Software", "Watch dog", "Deep Sleep", "SLC module", "Timer Group 0", "Timer Group 1",
"RTC Watch dog", "Instrusion", "Time Group CPU", "Software CPU", "RTC Watch dog CPU", "Extern CPU", "Voltage not stable", "RTC Watch dog RTC"};

void admin() {                          // Funktionsaufruf "admin();" muss im Setup eingebunden werden
  File file = SPIFFS.open("/config.json");
  if (file) {
    String Hostname = file.readStringUntil('\n');
    if (Hostname != "") {
      WiFi.setHostname(Hostname.substring(2, Hostname.length() - 2).c_str());
      ArduinoOTA.setHostname(WiFi.getHostname());
    }
  }
  file.close();
  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();          //Einkommentieren wenn Werte vor dem Neustart gesichert werden sollen
    ESP.restart();
  });
}

void handlerenew() {
  server.send(200, "application/json", R"([")" + runtime() + R"(",")" + temperatureRead() + R"(",")" + WiFi.RSSI() + R"("])");     // Json als Array
}

void handleonce() {
  if (server.arg(0) != "") {
    WiFi.setHostname(server.arg(0).c_str());
    File file = SPIFFS.open("/config.json", FILE_WRITE);
    file.printf("[\"%s\"]", WiFi.getHostname());
    file.close();
  }
  String fname = String(__FILE__).substring( 3, String(__FILE__).lastIndexOf ('\\'));
  String temp = R"({"File":")" + fname.substring(fname.lastIndexOf ('\\') + 1, fname.length()) + R"(", "Build":")" +  (String)__DATE__ + " " + (String)__TIME__ +
                R"(", "LocalIP":")" +  WiFi.localIP().toString() + R"(", "Hostname":")" + WiFi.getHostname() + R"(", "SSID":")" + WiFi.SSID() +
                R"(", "GatewayIP":")" +  WiFi.gatewayIP().toString() +  R"(", "Channel":")" +  WiFi.channel() + R"(", "MacAddress":")" +  WiFi.macAddress() +
                R"(", "SubnetMask":")" +  WiFi.subnetMask().toString() + R"(", "BSSID":")" +  WiFi.BSSIDstr() + R"(", "ClientIP":")" + server.client().remoteIP().toString() +
                R"(", "DnsIP":")" + WiFi.dnsIP().toString() + R"(", "Reset1":")" + resetReason[rtc_get_reset_reason(0)] +
                R"(", "Reset2":")" + resetReason[rtc_get_reset_reason(1)] + R"(", "CpuFreqMHz":")" + ESP.getCpuFreqMHz() +
                R"(", "FreeHeap":")" + formatBytes(ESP.getFreeHeap()) + R"(", "ChipSize":")" +  formatBytes(ESP.getFlashChipSize()) +
                R"(", "ChipSpeed":")" + ESP.getFlashChipSpeed() / 1000000 + R"(", "ChipMode":")" + flashChipMode[ESP.getFlashChipMode()] +
                R"(", "IdeVersion":")" + ARDUINO + R"(", "SdkVersion":")" + ESP.getSdkVersion() + R"("})";
  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 Überlauf
    DEBUG_P("Millis Überlauf");
  }
  previousMillis = currentMillis;
  uint32_t sec {(0xFFFFFFFF / 1000) * rolloverCounter + (currentMillis / 1000)};
  char buf[20];
  snprintf(buf, sizeof(buf), "%d Tag%s %02d:%02d:%02d", sec / 86400, sec < 86400 || sec >= 172800 ? "e" : "", sec / 3600 % 24, sec / 60 % 60, sec % 60);
  return buf;
}

Die Funktion "runtime();" muss mindestens zweimal innerhalb 49 Tage aufgerufen werden.
Entweder durch den Client(Webseite) oder besser im loop().

Beispiel:

*************** Aufruf runtime() ******************

void loop() {
 ......
 .........
  if (millis() < 0x2FFF || millis() > 0xFFFFF0FF) runtime();
 ......
}

Der Funktionsaufruf "save();" kann zum speichern wichtiger Messwerte, vor einem Neustart, einkommentiert werden.
Diese Messwerte sollten beim Neustart im "setup();" wieder eingelesen werden.

Beispiel:

void save() {
  File f = SPIFFS.open("/sichern.txt", "w");                        // Datei zum schreiben öffnen
  if (f && freeSpace(300)) {
    f.printf("%.2f\n\%.2f\n%s\n%s\n", Temperaturmax, Temperaturmin, Luftfeuchtemax, Luftfeuchtemin);
  }
  f.close();
}

Das Webinterface zum Esp32 Admin Tab.

admin.html

<!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.0">
    <link rel="stylesheet" href="style32.css">
    <title>ESP32 Admin</title>
    <SCRIPT>
      window.addEventListener('load', () => {
        document.querySelector('#spiff').addEventListener('click', () => {
          window.location = '/spiffs.html';
        });
        document.querySelector('#home').addEventListener('click', () => {
          window.location = '/';
        });
        document.querySelector('#restart').addEventListener('click', () => {
          if (confirm('Bist du sicher!')) re('restart');
        });
        document.querySelector('#reconnect').addEventListener('click', re.bind(this, 'reconnect'));
        document.querySelector('#hostbutton').addEventListener('click', check.bind(this, document.querySelector('input')));
        once();
        var output = document.querySelector('#note');
        function once(arg1, arg2) {
          fetch('/admin/once', {
            method: 'POST',
            body: arg1
          }).then( resp => {
            return resp.json();
          }).then( obj => {
            output.innerHTML = '';
            output.classList.remove('note');
            document.querySelector('form').reset();
            if (arg1 == undefined) wait = window.setInterval(renew, 1000);
            if (arg2 == 'reconnect') re(arg2);
            document.querySelector('#file').innerHTML = obj['File'];
            document.querySelector('#build').innerHTML = obj['Build'];
            document.querySelector('#local').innerHTML = obj['LocalIP'];
            document.querySelector('#host').innerHTML = obj['Hostname'];
            document.querySelector('#ssid').innerHTML = obj['SSID'];
            document.querySelector('#gateway').innerHTML = obj['GatewayIP'];
            document.querySelector('#kanal').innerHTML = obj['Channel'];
            document.querySelector('#mac').innerHTML = obj['MacAddress'];
            document.querySelector('#subnet').innerHTML = obj['SubnetMask'];
            document.querySelector('#bss').innerHTML = obj['BSSID'];
            document.querySelector('#client').innerHTML = obj['ClientIP'];
            document.querySelector('#dns').innerHTML = obj['DnsIP'];
            document.querySelector('#reset1').innerHTML = obj['Reset1'];
            document.querySelector('#reset2').innerHTML = obj['Reset2'];
            document.querySelector('#cpufreq').innerHTML = obj['CpuFreqMHz'];
            document.querySelector('#freeheap').innerHTML = obj['FreeHeap'];
            document.querySelector('#csize').innerHTML = obj['ChipSize'];
            document.querySelector('#cspeed').innerHTML = obj['ChipSpeed'];
            document.querySelector('#cmode').innerHTML = obj['ChipMode'];
			document.querySelector('#ide').innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
            document.querySelector('#sdk').innerHTML = obj['SdkVersion'];
          }).catch(function (err) {
            re();
          });
        }
        function renew() {
          fetch('admin/renew').then( resp => {
            return resp.json();
          }).then( array => {
            document.querySelector('#runtime').innerHTML = array[0];
            document.querySelector('#temp').innerHTML = array[1];
            document.querySelector('#rssi').innerHTML = array[2];
          });
        }
        function check(inObj) {
          !inObj.checkValidity() ? (output.innerHTML = inObj.validationMessage, output.classList.add('note')) : (once(inObj.value, 'reconnect'));
        }
        function re(arg) {
          window.clearInterval(wait);
          fetch(arg);
          output.classList.add('note');
          if (arg == 'restart') {
            output.innerHTML = 'Der Server wird neu gestartet. Die Daten werden in 10 Sekunden neu geladen.';
            setTimeout(once, 10000);
          } else if (arg == 'reconnect') {
            output.innerHTML = 'Die WiFi Verbindung wird neu gestartet. Daten werden in 5 Sekunden neu geladen.';
            setTimeout(once, 5000);
          } else {
            output.innerHTML = 'Es ist ein Verbindungfehler aufgetreten. Es wird versucht neu zu verbinden.';
            setTimeout(once, 2000);
          }
        }
      });
    </SCRIPT>
  </head>
  <body>
    <h1>ESP32 Admin Page</h1>
    <main>
      <aside id="left">
        <span>Runtime ESP:</span>
        <span>WiFi RSSI:</span>
        <span>CPU Temperatur:</span>
        <span>Sketch Name:</span>
        <span>Sketch Build:</span>
        <span>IP 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 CPU 1:</span>
        <span>Reset CPU 2:</span>
        <span>CPU Freq:</span>
        <span>FreeHeap:</span>
        <span>FlashSize:</span>
        <span>FlashSpeed:</span>
        <span>FlashMode:</span>
		<span>Arduino IDE Version:</span>
        <span>SDK-Version:</span>
      </aside>
      <aside>
        <data id="runtime">00:00:00</data>
        <div>
          <data id="rssi"></data>
          dBm
        </div>
        <div>
          <data id="temp"></data>
          °C
        </div>
        <data id="file">?</data>
        <data id="build">0</data>
        <data id="local">0</data>
        <data id="host">?</data>
        <data id="ssid">?</data>
        <data id="gateway">0</data>
        <data id="kanal">0</data>
        <data id="mac">0</data>
        <data id="subnet">0</data>
        <data id="bss">0</data>
        <data id="client">0</data>
        <data id="dns">0</data>
        <data id="reset1">0</data>
        <data id="reset2">0</data>
        <div>
          <data id="cpufreq"></data>
          MHz
        </div>
        <data id="freeheap">0</data>
        <data id="csize">0</data>
        <div>
          <data id="cspeed"></data>
          MHz
        </div>
        <data id="cmode">0</data>
		<data id="ide">0</data>
        <data id="sdk">0</data>
      </aside>
    </main>
    <div>
      <button class="button" id="spiff">Spiffs</button>
      <button class="button" id="home">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 class="button" type="button" id="hostbutton">Name Senden</button>
      </form>
    </div>
    <div>
      <button class="button" id="reconnect">WiFi Reconnect</button>
      <button class="button" id="restart">ESP Restart</button>
    </div>
  </body>
</html>

Für die Darstellung ist die style32.css analog Spiffs Verwaltung, erforderlich.

style32.css

/*For more information visit: https://fipsok.de*/
body {
font-family: sans-serif;
background-color: #a9a9a9;
display: flex;
flex-flow: column;
align-items: center;
}
h1,h2 {
color: #e1e1e1;
text-shadow: 2px 2px 2px black;
}
li {
background-color: #7cfc00;
list-style-type: none;
margin-bottom: 10px;
padding-right: 5px;
border-top: 3px solid #7cfc00;
border-bottom: 3px solid #7cfc00;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.7);
}
li a:first-child, li b {
background-color: #ff4500;
font-weight: bold;
color: white;
text-decoration: none;
padding: 0 5px;
border-top: 3.2px solid #ff4500;
border-bottom: 3.6px solid #ff4500;
text-shadow: 2px 2px 1px black;
  cursor:pointer;
}
li strong {
color: red;
}
input {
height: 35px;
font-size: 13px;
}
h1+main {
display: flex;
}
aside {
display: flex;
flex-direction: column;
padding: 0.2em;
}
#left {
align-items: flex-end;
text-shadow: 1px 1px 2px #757474;
}
.note {
background-color: salmon;
padding: 0.5em;
margin-top: 1em;
text-align: center;
max-width: 320px;
border-radius: 0.5em;
}
.button {
width: 130px;
height: 40px;
font-size: 16px;
margin-top: 1em;
cursor: pointer;
background-color: #adff2f;
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.7);
}
[type=submit] {
height: 40px;
min-width: 70px;
}
[value^=Format] {
background-color: #ddd;
}
[title] {
background-color: silver;
font-size: 16px;
width: 125px;
}