Update: 2020-10-15

Esp8266 Admin IPv6 als Arduino Tab.

In der IDE unter Werkzeuge auf lwip Variant: "v2 IPv6 Lower Memory" stellen.

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

Admin.ino

// ****************************************************************
// Sketch Esp8266 Admin IPv6 Modular(Tab)
// created: Jens Fleischer, 2020-01-26
// last mod: Jens Fleischer, 2020-10-13
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp8266
// Software: Esp8266 Arduino Core 2.6.0 - 2.7.4
// Geprüft: von 1MB bis 16MB Flash
// Getestet auf: Nodemcu, Wemos D1 Mini Pro, Sonoff Switch, Sonoff Dual
/******************************************************************
  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 Admin 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.
// Die LittleFS.ino muss im ESP8266 Webserver enthalten sein
// Funktion "admin();" muss im setup() nach setupFS() 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();"
// In der IDE unter Werkzeuge auf lwip Variant: ;"v2 IPv6 Lower Memory;" einstellen.
/**************************************************************************************/

#include <AddrList.h>

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");
     //toSave();      //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() / 1000 + "," + ESP.getVcc() % 1000 / 10 + " V" + "\"]");
}

void handleonce() {
  String ipv6Local, ipv6Global;
#if LWIP_IPV6
  for (auto &a : addrList) {
    if (a.isV6()) a.isLocal() ? ipv6Local = a.toString() : ipv6Global = a.toString();
  }
#endif
  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() + "\", \"IPv6l\":\"" + ipv6Local +
                "\", \"IPv6g\":\"" +  ipv6Global + "\", \"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;
}

Die Funktion "sketchName();" muss sich im "Haupt-Tab" befinden.
Dies ist durch eine Änderung im Esp Core ab Version 2.6.0 erforderlich.

Beispiel:

*************** Funktion sketchName() ******************

String sketchName() {                             // Dateiname für den Admin Tab
  char file[sizeof(__FILE__)] = __FILE__;
  char * pos = strrchr(file, ); *pos = '\0';
  return file;
}  

void setup() {
 ......
  .........

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();
  }
 ......
}

Das Webinterface zum Esp8266 IPv6 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">
    <link rel="stylesheet" href="style.css">
    <title>ESP8266 Admin</title>
	<style>
	  @media only screen and (max-width: 500px) {
	    .ip {
	      right: 6em;
		  position: relative;
	    }
	    aside {
	      max-width: 50vw;
	    }
	  }
	</style>
    <script>
      window.addEventListener('load', () => {
		renew(),once();
		let output = document.querySelector('#note');
		let button = document.querySelectorAll('button');
		let span = document.querySelectorAll('#right span');
        button[0].addEventListener('click', () => {
            window.location = '/fs.html';
        });
        button[1].addEventListener('click', () => {
          window.location = '/';
        });
        button[2].addEventListener('click', check.bind(this, document.querySelector('input')));
		button[3].addEventListener('click', re.bind(this, 'reconnect'));
		button[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['IPv6l'] ? obj['IPv6l'] : 'inaktiv';
            span[9].innerHTML = obj['IPv6g'] ? obj['IPv6g'] : 'inaktiv';
			span[10].innerHTML = obj['Hostname'];
            span[11].innerHTML = obj['SSID'];
            span[12].innerHTML = obj['GatewayIP'];
            span[13].innerHTML = obj['Channel'];
            span[14].innerHTML = obj['MacAddress'];
            span[15].innerHTML = obj['SubnetMask'];
            span[16].innerHTML = obj['BSSID'];
            span[17].innerHTML = obj['ClientIP'];
            span[18].innerHTML = obj['DnsIP'];
            span[19].innerHTML = obj['ResetReason'];
            span[20].innerHTML = obj['CpuFreqMHz'] + " MHz";
			span[21].innerHTML = obj['FreeHeap'];
			span[22].innerHTML = obj['HeapFrag'] + "%";
            span[23].innerHTML = obj['ChipSize'];
            span[24].innerHTML = obj['ChipSpeed'] + " MHz";
            span[25].innerHTML = obj['ChipMode'];
			span[26].innerHTML = obj['IdeVersion'].replace(/(\d)(\d)(\d)(\d)/,obj['IdeVersion'][3]!=0 ? '$1.$3.$4' : '$1.$3.');
            span[27].innerHTML = obj['CoreVersion'].replace(/_/g,'.');
            span[28].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>Link-Local:</span>
		<span class="ip">IPv6:</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 class="ip">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>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>0</span>
        <span class="ip">0</span>
        <span>?</span>
        <span>?</span>
        <span>0</span>
        <span>0</span>
        <span>0</span>
        <span>0</span>
        <span>0</span>
        <span class="ip">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>
		<span>0</span>
      </aside>
    </main>
    <div>
      <button>LittleFS</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>

Für die Darstellung ist die style.css analog Filesystem Manager, erforderlich.

style.css

/* 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;
}
.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: 16px;
	width: 125px;
}
form:nth-of-type(2) {
	margin-bottom: 1em;
}
[type=submit] {
	height:40px; 
	font-size: 16px;
}
[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';
}