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.
Inhaltsverzeichnis
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:
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();
}
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);
};
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.
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.
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. |
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.
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();
}
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>
08: <script src="em-min.js"></script>
Die Datei "em-min.js" enthält den komprimierten JavaScript-Code zur Darstellung der Steuerelemente:
10: <body onload="refreshData()">
... sorgt dafür, dass das regelmäßige Update der Anzeige gestartet wird.
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.
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.
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.
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.
Die Funktionen rufen einfach refreshData mit dem passenden Funktionsargument auf.
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):