← zur Übersicht

Software (Interface)

Die gesamte Firmware ist in einzelne Funktionsgruppen aufgeteilt. Zur jeder dieser Gruppen gibt es eine oder mehrere Klassen. Durch die strikte Trennung steigt die Übersichtlichkeit und Seiteneffekte werden vermieden.

In­halts­ver­zeich­nis

HTTP-Interface

Verwendung

Implementierung

UDP-Logging

UDP-Broadcast-Interface

Verwendung

Web-Interface

Webserver

Web-Site "index.thml"

Grafik-Element Verbrauchsanzeige UrsElectricMeter

Verlaufsanzeige


HTTP-Interface

Das HTTP-Interface liefert Informationen über den aktuellen Systemzustand. Auf die den Abruf der URL "<IP>:80/data" erhält man folgende Antwort:
           <Leistung>;<Verbrauch>;<PZEM-Fehlerkennung>;<Laufzeit>;<Zeit>;<Zeit-Fehlerkennung>;<Relais>
z.B.:
           105.5;0.018;ok;27668;0d 07h 41m;ok;on

Die einzelnen Werte sind durch Semikolon (";") getrennt. I.d.R. kann die Portangabe (":80") entfallen.

Wert Erläuterung Fehlerwert
Leistung Aktuelle Leistung in Watt, 1 Dezimalstelle. 0, wenn das PZEM-004T nicht ausgelesen werden konnte. Ggf. auch während der Initialisierungsphase.
Verbrauch Gesamtverbrauch während der aktuellen Messung in kWh, 4 Dezimalstellen. 0, wenn das PZEM-004T nicht ausgelesen werden konnte. Ggf. auch während der Initialisierungsphase.
PZEM-Fehlerkennung "ok", das PZEM-004T hat zuletzt valide Daten geliefert. "err", wenn das PZEM-004T nicht ausgelesen werden konnte. Ggf. auch während der Initialisierungsphase.
Laufzeit Sekunden seit Start der aktuellen Messung. undefinierter numerischer Wert, wenn keine aktuelle UNIX-Zeit vorliegt.
Zeit Formatierte Laufzeit in Tagen, Stunden (zweistellig) und Minuten (zweistellig). "Error", wenn keine aktuelle UNIX-Zeit vorliegt.
Zeit-Fehlerkennung "ok", es gibt eine valide Systremzeit "err", wenn keine aktuelle UNIX-Zeit vorliegt.
Relais Aktueller Zustand des Relais.
"on": Der Verbraucher ist eingeschaltet.
"off": Der Verbraucher ist ausgeschaltet.
entfällt

Gleichzeitig kann über dieses Interface per Query-Parameter auch der Relais-Zustand beeinflusst werden:

Verwendung

Die Daten können z.B. per JavaScript abgefragt und analysiert werden:

function refreshData(onOff) {
   // onOff: 'switchOn' oder 'switchOff'
   var request = new XMLHttpRequest();

   // URL mit Query-Parameter
   var rStr = "data";
   if (onOff)
      rStr += "?" + onOff + "=1";

   // Request aufbauen
   request.open("GET", rStr);
   request.timeout = 1000; // timeout in milliseconds
   request.addEventListener('load', function (event) {
      if (request.status >= 200 && request.status < 300) {
         var p = request.responseText.split(";");
         // ToDo: Verwendung von p[0]...p[6]
      } else {
        // ToDo: Reaktion auf Fehler
      }
   });
   request.addEventListener('error', function (event) {
      // ToDo: Reaktion auf Verbindungsfehler
   });

   // Request senden
   request.send();
}

Implementierung

Das HTTP-Interface ist als Request-Handler des Web-Servers ausgelegt. Zunächst werden die Parameter ausgewertet. Danach wird der Response-String zusammen gesetzt und versendet.

// WebSiteData.cpp
//
// Behandelt Anfragen zur WebSite: data
// 

void EnergyWebServerClass::handleData(AsyncWebServerRequest * request) {
  // Parameter auswerten
  // =====================================================================
  if (request->hasParam(F("switchOn"))) // Relais einschalten
    Relais.setState(RelaisState::on);
  if (request->hasParam(F("switchOff"))) // Relais ausschalten
    Relais.setState(RelaisState::off);
  if (request->hasParam(F("reset")))  // Verbrauchszähler zurücksetzen (geschieht in loop())
    shouldClear = true;

  // Antwort zusammenstellen
  // =====================================================================
  // Energie-Messwerte
  String responseString = EnergySensor.getLastReading().toString();

  // Relais
  responseString += Relais.isOn() ? ";on" : ";off";

  // Antwort senden
  request->send(200, F("text/plain"), responseString);
};

UDP-Logging

Die serielle Schnittstelle ist nicht hinausgeführt. Meldungen müssen per WiFi weitergegeben werden. Eine Instanz der UrsUdpSerial-Klasse wird hierzu genutzt. Die Einstellungsdaten, IP-Adresse und Ports werden über die EMSettings-Klasse bereit gestellt, sind also außensteuerbar. Die IP-Adresse wird auf die Broadcast-Adresse "x.x.x.255" des lokalen WLANs eingestellt. Damit können die Meldungen von jeder Station im WLAN empfangen werden. Es ist nur die Einstellung des korrekten Ports notwendig.

Da erst nach einer Verbindung mit dem WLAN Daten übertragen werden können, wird während der Initialisierungsphase zunächst auf die normale serielle Schnittstelle ausgegeben. So hat man die Chance bei geöffnetem Gehäuse auch über diese Phase zu tracken. Nach einer erfolgreichen Verbindung mit dem WLAN wird umgeschaltet.

Print* EnergyMeterLog = &Serial; // Initiale Ausgabe über die serielle Schnittstelle
UrsUdpSerial UdpLog(64);         // UrsUpdSerial-Instanz für das Logging per UDP

...        // Verbindung zum WLAN herstellen
...        // UdpLog initialisieren

EnergyMeterLog = &UdpLog;        // Auf UDP-Logging umschalten

Von einem kleinen Nachteil ist, dass mit Zeigern gearbeitet werden muss. EnergyMeterLog wird in der globalen Header-Datei "Globals.h" veröffentlicht und ist somit systemweit verfügbar.


UDP-Broadcast-Interface

Auch das UDP-Interface basiert auf der UrsUdpSerial-Klasse. Die Klasse UDPIOClass mit der Instanz UDPIO ist von UrsUdpSerial abgeleitet. Die Methode handle() sorgt für die Abfrage eingehender Daten und der zugehörigen Antwort. raiseEvent() sendet Ereignisnachrichten.

Verwendung

Die Methode handle() prüft ob, ein neu empfangenes UDP-Datagramm vorliegt. Von diesem wird nur das erste Zeichen ausgewertet. Folgezeichen im gleichen Paket werden verworfen. Zu jedem behandelten Paket erfolgt eine Antwort (man bedenke, UDP garantiert die Übermittlung eines Pakets nicht). Die Antwort wird mit einem Zeilenendezeichen (CRLF, '\n', 0x0D0A) abgeschlossen.

Zeichen Reaktion Antwort
0 Relais ausschalten ok:0
1 Relais einschalten ok:1
c Setzt den Verbrauchzähler zurück ok:c
x Liefert den aktuellen Systemzustand ok:x
und in einer neunen Zeile
data:<Leistung>;<Verbrauch>;<PZEM-Fehlerkennung>;<Laufzeit>;<Zeit>;<Zeit-Fehlerkennung>;<Relais>
Für Details siehe HTTP-Interface
r Führt einen Neustart (Reset) des Prozessors aus. ok:r
i Liefert die lokale IP-Adresse ok:i
und in einer neunen Zeile
IP:<IP-Adresse>
d Das Logging der aktuellen Messdaten (10s Abstand) anstellen. ok:d
o Das Logging der aktuellen Messdaten abstellen. ok:o
q Verbindung zum WLAN trennen.
Zum Test der automatischen Wiederverbindung.
ok:q
sonst. keine got:<empfangenes Zeichen>

Verschiedene Änderungen des Systemzustand werden als Ereignis-Paket mitgeteilt. Diese habe die Form "event:<Event-Typ><CRLF>", z.B. "event:on". Die Nachrichten werden über die Methode raiseEvent() versandt. Die Event-Typen sind in der Enumeration EventType definiert.

Ereignis Nachricht
Relais wurde eingeschaltet. event:On
Relais wurde ausgeschaltet. event:Off
Verbrauchszähler wurde zurückgesetzt. event:Reset
Der Neustart des Systems wurde eingeleitet. event:Reboot
Das System wurde neu gestartet. event:Restart

Die Ereignis-Nachrichten werden auch dann versandt, wenn die Ereignisse durch die UDP-Schnittstelle ausgelöst wurden.

Die folgende Tabelle zeigt ein typisches Protokoll. Spalte "Richtung" aus Sicht des ESP8266.

Zeile Daten Richtung Erläuterung
1 event:Restart gesendet Das System wurde neu gestartet.
2 event:Off gesendet Meldung der Relaisstellung nach Systemstart.
3 event:On gesendet Relais über Web-Interface eingeschaltet.
4 0 empfangen Anweisung: Relais ausschalten.
5 ok:0 gesendet Bestätigung der Anweisung.
6 event:Off gesendet Meldung: Relais ausgeschaltet.
7 0 empfangen Anweisung: Relais ausschalten.
8 ok:0 gesendet Bestätigung der Anweisung.
    ! Es folgt keine(!) Ereignismeldung, da der Relaiszustand nicht geändert wurde.
9 1 empfangen Anweisung: Relais einschalten.
10 ok:1 gesendet Bestätigung der Anweisung.
11 event:On gesendet Meldung: Relais eingeschaltet.

Web-Interface

Das Web-Interface hat am 2018-03-08 ein kleines Update erfahren:

Das Web-Interface besteht aus einer Instanz der EnergyWebServerClass  und der SPIFFS-Datei "index.html". Das ebenfalls von der EnergyWebServerClass mit implementierte HTTP-Interface ist oben beschrieben. "index.html" benötigt darüber hinaus noch das Steuerelement UrsElectricMeter, das die Anzeige der Verbrauchsdaten übernimmt.

Webserver

Das Web-Interface basiert auf der UrsAsyncWebServer-Klasse. Diese stellt bereits eine Seite für die Außensteuerungseditor und einen SPIFFS-Explorer bereit.

Von der UrsAsyncWebServer-Klasse wird die Klasse EnergyWebServerClass abgeleitet, die das Web-Interface für dieses Projekt implementiert.

Zwei Handler-Methoden handleData() und handleIndexHTML() erledigen die Abrufe von "/data", dem HTTP-Interface (s.o.), und "/index.html", der Anzeige-Seite für die Verbrauchwerte und der Steuerung.

Die Methode begin() initialisiert den Webserver:

void EnergyWebServerClass::begin(EMSettingsClass& settings, String authenticationUsername, 
                                 String authenticationPassword,
                                 String authenticationRealm) {
  rewrite("/", "/index.html");          // root auf 'index.html' umleiten
  rewrite("/index.htm", "/index.html"); // 'index.htm' auf 'index.html' umleiten

  // Handler registrieren für ...
  on("/data", HTTP_ANY, [this](AsyncWebServerRequest *request) // ... HTTP-Interface '/data'
              { handleData(request); }); 
  on("/index.html", HTTP_ANY, [this](AsyncWebServerRequest *request) // ... 'index.html'
              { handleIndexHTML(request); });

  // Die inkludierten Dateien mit Lebensdauer versehen:
  serveStatic("/ein.png", SPIFFS, "/ein.png").setCacheControl("max-age=3600");
  serveStatic("/aus.png", SPIFFS, "/aus.png").setCacheControl("max-age=3611");
  serveStatic("/broken.png", SPIFFS, "/broken.png").setCacheControl("max-age=3622");
  serveStatic("/leftHandle.png", SPIFFS, "/leftHandle.png").setCacheControl("max-age=3633");
  serveStatic("/rightHandle.png", SPIFFS, "/rightHandle.png").setCacheControl("max-age=3644");
  serveStatic("/reset.png", SPIFFS, "/reset.png").setCacheControl("max-age=3655");
  serveStatic("/em-min.js", SPIFFS, "/em-min.js").setCacheControl("max-age=3666");

  serveStatic("/", SPIFFS, "/");   // Alle sonstigen Anfagen aus dem SPIFFS bedienen

  // Webserver starten
  UrsAsyncWebServerClass::begin(&settings, authenticationUsername, authenticationPassword, authenticationRealm);
  EnergyMeterLog->println(F("Web-Server gestartet"));
}

Die Variable energySensorShouldReset gibt an, ob die Gesamtverbrauchszählung des EnegerySensor zurück gesetzt werden soll. Das muss synchron geschehen und wird in loop() erledigt:

if (EnergyWebServer.energySensorShouldReset) {
  EnergyWebServer.energySensorShouldReset = false;
  EnergySensor.reset();
}

Der Handler für "/index.html" ist wegen der besseren Übersicht separat in der Datei "WebSiteIndex.cpp" abgelegt. Der Aufbau des Handlers entspricht dem Muster der Handler in der UrsAsyncWebServer-Klasse.

Der Template-Prozessor ersetzt die Platzhalter %title% und %RefreshInterval% durch den entsprechenden Wert aus der Außensteuerung.

String IndexResponse::indexProcessor(const String & var) {
  if (var == "RefreshInterval")
    return String(EMSettings.RefreshInterval);
  if (var == "title")
    return String(EMSettings.Name);

  return String();
}

Web-Site "index.thml"

Die stellt zunächst die sichtbaren Elemente dar. Die Aktualisierung der Seite geschieht per JavaScript über das HTTP-Interface (s.o.). Hierdurch muss die Seite nicht immer neu geladen werden, sondern es werden nur die Nutzdaten abgefragt. Dies steigert die Performance der Anzeige deutlich und redeziert das beim Neuladen nicht zu vermeidende Flackern der Seite im Browser.

Der HTML-Code der Seite:

01: <!DOCTYPE html>
02: <html lang="de">
03: <head>
04:    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
05:    <meta name="viewport" content="width=700">
06:    <title>%title%</title>
07:    <link href="favicon.ico" rel="shortcut icon" type="image/x-icon">
08:    <script src="em-min.js"></script>
09: </head>
10: <body onload="refreshData()">
11:    <h1 style="font-family: Arial, Helvetica, sans-serif; text-align: center; width:660px">%title%</h1>
12:    <div style="position: relative;">
13:       <canvas id="cv" width="660" height="393" 
14:               style="position: absolute; left: 0px; top: 0px"></canvas>
15:       <div id="OnOffDiv">
16:         <img id="onoffbutton" alt="" src="ein.png" style="position: absolute; left: 110px; top: 264px; 
                                                              height:51px" onClick="sendOnOff()">
17:       <div>
18:       <img alt="" src="reset.png" 
19:            style="position: absolute; left: 495px; top: 264px; height:51px" onClick="sendReset()">
20:    </div>
21: 
22:    <script>
23:       "use strict";
24:       var onOffState = true;
25:       var em = new UrsElectricMeter('cv', {duration: 2});
26:       var RefreshInterval = "%RefreshInterval%";
27:       if (RefreshInterval.substring(1, 1) == "R")
28:          RefreshInterval = 2;
29:       else
30:          RefreshInterval = Number(RefreshInterval);
31: 
32:       function refreshData(onOff) {
33:          var request = new XMLHttpRequest();
34: 
35:          var rStr = "data";
36:          if (onOff != undefined)
37:             rStr += "?" + onOff + "=1";
38: 
39:          request.open("GET", rStr);
40:          request.timeout = 1000; // timeout in milliseconds
41:          request.addEventListener('loadend', function (event) {
42:             var schalter = document.getElementById("onoffbutton");
43:             if (request.status >= 200 && request.status < 300) {
44:                var v = request.responseText.split(";");
45:                em.setPowerValueAnimated(v[0]);
46:                em.setConsumtionValueAnimated(v[1]);
47:                if (v[6] == "on") {
48:                   onOffState = true;
49:                   schalter.src = "ein.png";
50:                   schalter.title = "Verbraucher ausschalten";
51:                } else {
52:                   onOffState = false;
53:                   schalter.src = "aus.png";
54:                   schalter.title = "Verbraucher einschalten";
55:                }
56:                em.setLCDValue(v[4]);
57:                document.getElementById("OnOffDiv").style.display = "block";
58:             } else {
59:                console.log("No answer");
60:                document.getElementById("OnOffDiv").style.display = "none";
61:                schalter.title = "Keine Verbindung";
62:             }
63:          });
64:          request.addEventListener('error', function (event) {
65:             console.log("error");
66:             document.getElementById("OnOffDiv").style.display = "none";
67:          });
68: 
69:          request.send();
70:          if (onOff == undefined) {
71:            setTimeout(refreshData, RefreshInterval * 1000);
72:          }
73:       } // refreshData
74: 
75:       function sendReset() {
76:          if (confirm("Verbrauchszähler wird zurück gesetzt!") == true) {
77:             refreshData("reset");
78:          }
79:       }
80: 
81:       function sendOnOff() {
82:          var img = document.getElementById("onoffbutton");
83:          if (onOffState)
84:             refreshData("switchOff");
85:          else
86:             refreshData("switchOn");
87:       }
88:    </script>
89: </body>
90: </html>

Steuerelemente einbinden (Zeile 8)

08:    <script src="em-min.js"></script>

Die Datei "em-min.js" enthält den komprimierten JavaScript-Code zur Darstellung der Steuerelemente:

Initialisierung der Skripte (Zeile 10)

10: <body onload="refreshData()">

... sorgt dafür, dass das regelmäßige Update der Anzeige gestartet wird.

Grafik-Elemente bereit stellen (Zeilen 11..20)

Zunächst wird das Canvas-Element zum Zeichnen der Verbrauchsanzeige definiert. Danach werden auf das Canvas die Schaltflächen als Bilder für Ein/Aus und Zurücksetzen des Zählers positioniert. Über onClick werden diese mit den passenden JavaScript-Funktionen verbunden.

Initialisierung (Zeilen 24..256)

Die Variable onOffState führt den angezeigten Relais-Zustand mit. In der Zeile 23 wird das Anzeige-Element instanziiert und mit dem Canvas-Element verbunden.

Behandlung von RefreshInterval (Zeilen 26..30)

Weiter unten wird die Variable innerhalb einer Anweisung benutzt. Wenn die Seite, wie z.B. beim Test, nicht vom ESP8266 abgerufen wird und dieser die Ersetzung des Platzhalters vornimmt, gib es einen Syntaxfahler und die Seite wird nicht korrekt angezeigt. Dies Zeilen prüfen, ob der Platzhalter ersetzt wurde. Ist dies nicht der Fall wird RefreshInterval mit einem sinnvollen Wert belegt.

HTTP-Interface (Zeilen 32..79)

Die Datenabfrage und die Übermittlung von Steuersignalen erfolgt durch die Funktion refreshData() mittels einer XMLHttpRequest-Instanz. Wenn ein Funktionsargument übergeben wird dieses in einen Query-Parameter übersetzt (Zeilen 35..37). Im folgenden wird der Request aufgebaut und Event-Handler für die Ereignisse loadend und error registriert. Beim Ereignis loadend wird der übermittelte String zerlegt und die einzelnen HTML-Elemente angepasst.

Für den regelmäßig Update der Inhalte sorgen die Zeilen 70 und 71. Über setTimeout wird dafür gesorgt, dass refreshData nach Ablauf der vorgesehenen Zeit erneut aufgerufen wird.

 Die Ereignishandler für das Anklicken der Schaltflächen in den Folgezeilen rufen ebenfalls refreshData zur Übermittlung der Steuerbefehle auf. Dadurch wird erreicht, dass das Ergebnis der Steuerbefehle direkt sichtbar wird. Man muss jedoch darauf achten, dass es bei einer Refresh-Schleife bleibt. Die Click-Handler geben allesamt ein Funktionsargument mit mit und können daran erkannt werden. In diesen Fällen darf setTimeout nicht aufgerufen werden.

Click-Handler (Zeilen 81..87)

Die Funktionen rufen einfach refreshData mit dem passenden Funktionsargument auf.

Grafik-Element Verbrauchsanzeige UrsElectricMeter

Im wesentlichen zeichnet das Steuerelement auf dem übergebenen Canvas-Element den Rahmen der Anzeige und den Rahmen für das Odometer. Auf diesen Hintergrund wird dann ein UrsOdometerEx-Element zur Verbrauchsanzeige, ein UrsSlidingScaleEx-Element zur Anzeige der aktuellen Leistungsaufnahme und ein UrsLcdDisplay-Element zur Anzeige der Messdauer platziert.

So sieht es dann in etwa aus (animiertes Beispiel):

Web-Interface

Verlaufsanzeige

Verlaufsanzeige