Debugging per Logging und für ESPs mit WLAN

https://oer-informatik.de/esp_logging

tl/dr; (ca. 10 min Lesezeit): Bei einfachen Projekten ist das Logging über die serielle Verbindung ein Segen beim Debugging: über die USB-Verbindung können am seriellen Monitor der Arduino-IDE Werte ausgegeben werden. Was aber tun, wenn die serielle Verbindung nicht besteht? Wie kann ich Logging am ESP komfortabel und einfach umsetzen? Ein Vorschlag.

Wir bauen in fünf Stufen ein per WLAN erreichbares Logging zusammen:

  • Stufe eins: den seriellen Monitor nutzen

  • Stufe zwei: den seriellen Monitor nur im Debug-Modus nutzen

  • Stufe drei: LogLevel einfügen

  • Stufe vier: Ausgabe mit Zeit/Datum (über WLAN und Zeitserver)

  • Stufe fünf: Log-Ausgabe auf Website des ESP

Für eilige gibt es bei einigen Stufen auch Links zu fertigem Beispielcode

Stufe eins: den seriellen Monitor nutzen.

Der serielle Monitor bietet die Möglichkeit, Variableninhalte während der Programmausführung auf dem Microcontroller an den Computer auszugeben. Hierzu muss mit Serial.begin() eine serielle Verbindung zum Computer aufgebaut werden (über die RX/TX-Pins, sprich: per USB). Mit Serial.print() können dann Werte oder Zeichenketten an den seriellen Monitor übermittelt werden. Im folgenden Code ist dies einfach der Inhalt einer Laufvariablen:

Der serielle Monitor lässt sich in der Arduino IDE öffnen über Tools/Serial Monitor oder über die Tastenkombination Strg+Shift+M

Wichtig ist, dass die Baud-Rate, die in dem Serial.begin()-Befehl übergeben wurde auch im seriellen Monitor eingestellt ist (hier: 115200 baud):

Beispielausgabe des seriellen Monitors von obigem Beispiel: “Logging No. 3…”
Beispielausgabe des seriellen Monitors von obigem Beispiel: “Logging No. 3…”

Stufe zwei: den seriellen Monitor nur im Debug-Modus nutzen.

Wenn das Programm erstmal läuft soll dieser ganze Debugging-Code gar nicht mehr ausgeführt werden und den Seriellen Anschluss benutzen. Ich möchte ihn auf einfache Weise aktivieren und deaktivieren können.

Ein Weg dies relativ komfortabel zu tun ist eine Präprozessor-Direktive. Bevor der Compiler den Quelltext verarbeitet werden Präprozessor-Direktiven ausgeführt. Dahinter verbirgt sich eigentlich nur eine Suchen/Ersetzen Funktionalität:

  • mit #define NAME WERT deklariere ich ein Marko, eine Art Platzhalter. Dieser Platzhalter besagt: Wenn im folgenden Quellcode irgendwo NAME steht, dann ersetze das durch WERT. Es werden nur ganze Wörter ersetzt und nur ausserhalb der Präprozessor Direktiven.

  • mit #ifdef NAME Codezeilen1 else Codezeilen2 endif überprüfe ich, ob ein Makro namens NAME existiert. Wurde dieses Makro deklariert, dann bleiben die Codezeilen1 im compilierten Code enthalten sind. Ist es nicht deklariert worden (oder wurde die Deklaration auskommentiert), dann bleibt nur Codezeilen2 vorhanden.

Wir nutzen beide Funktionalitäten kombiniert:

Für den Fall, dass DEBUG deklariert wurde, wird aus jedem DEBUG_PRINT(x) ein Serial.print(x) (also eine Debug-Ausgabe auf dem seriellen Monitor).

Für den Fall, dass DEBUG nicht deklariert wurde, wird jedes DEBUG_PRINT(x) ersetzt durch `` (nichts).

Wenn jetzt im Quelltext für Debugging anstelle des Funktionsaufrufs (Serial.print(x)) immer DEBUG_PRINT(x) angegeben wird, dann kann mit Hilfe #define DEBUG debugging aktiviert, mit einem auskommentierten //#define DEBUG deaktiviert werden. Es muss nur noch der Code für Serial.begin(115200) ebenso in die #ifdef DEBUG Bereiche eingefasst werden. Im Ganzen sieht das beispielhaft so aus:

Der compilierte Code enthält diesen ganzen Hokuspokus nicht mehr. Die Präprozessor-Direktiven entfernen einfach unnötige Passagen aus dem Code. (Interesse geweckt? Hier und hier finden sich weitere Infos dazu.)

Stufe drei: LogLevel einfügen

Wer’s eilig hat: der fertige Code ist hier.

Bei größeren Projekten will man häufig genauer aufschlüsseln, welche Informationen ausgegeben werden sollen. Sowohl beim Lesen als auch bereits beim Logging kann man also filtern, ab welcher Wichtigkeit man Informationen erhalten will

Nr. LogLevel Beschreibung
7 ALL Alle Meldungen werden ausgegeben
6 TRACE Sehr ausführliche Informationen zum Auffinden von Fehlern werden ausgegeben. Diese sind i.d.R. nur für den Entwickler interpretierbar.
5 DEBUG Infos zum Auffinden von Fehlern werden ausgegeben. Die Infos sollten auch Anwendern (nicht nur Entwicklern) beim Auffinden von Problemen helfen.
4 INFO Allgemeine Informationen zum Programmablauf werden ausgegeben. Start/Stop und wesentliche Zustände des Systems
3 WARN Unerwartete Situationen werden ausgegeben. Diese Situationen können zwar zu Problemen führen, sind in diesem Fall aber unkritisch verlaufen.
2 ERROR Nur Fehler im Programmablauf, die jedoch nicht zum Gesamtabbruch führen, werden ausgegeben. Es sind i.d.R. nur einzelne Module betroffen und in der Funktionalität eingeschränkt
1 FATAL Nur kritische Fehler, die zum Programmabbruch (bzw. Abbruch des Gesamtservices) führen werden ausgegeben
0 OFF Keine Ausgabe von irgendwelchen Log-Nachrichten.

Die Nummerierung ist angelehnt an die LogLevel des Linux Kernel (im .Net-Kontext wird häufig eine umgekehrte Nummerierung gewählt). Höhere Stufen beinhalten immer auch die Infos der unteren Stufen (DEBUG enthält also z.B. auch INFO-Nachrichten).

Für den Quellcode heißt das: Es muss ein Array mit den LogLevel-Namen erstellt werden (und darüber die Nummern-Zuordnung umgesetzt sein). Zudem muss ein MIN_LOG_LEVEL für die momentane Ausführung festgelegt werden. Als Default-Einstellung wähle ich immer INFO, das nur wesentliche Nachrichten des regulären Betriebs ausgeben sollte. In diesem Beispiel steht es aus Anschauungszwecken auf DEBUG.

Das Logging selbst übernimmt eine neue Funktion, die den LogLevel jeder Nachricht voranstellt (und wieder über die Präprozessor-Direktiven deaktiviert wird):

Der Log-Aufruf selbst erfolgt jetzt über die neue Funktion, wobei als letztes Argument das LogLevel der jeweiligen Nachricht übergeben wird:

Stufe vier: Ausgabe mit Zeit/Datum (über WLAN und Zeitserver)

Wer’s eilig hat: der fertige Code ist hier.

Für die folgenden Schritte ist eine stabile WiFi-Verbindung (Tutorial) Voraussetzung. Die einzelnen Codeblöcke, die ergänzt werden müssen stelle ich kurz Schritt für Schritt vor:

Stufe 4a: WiFi…

Die relevanten Imports (Abschnitt 1; der Code ist etwas komplizierter, weil für ESP32 und ESP8266 gleichermaßen gültig):

Es müssen die Zugangsdaten geladen werden. Ich lese diese aus einer externen Datei ein, die nicht im Versionscontrollsystem erfasst wird:

Eine Datei secrets.h muss erstellt werden, die (möglichst) nicht in die Versionskontrolle aufgenommen wird. Sie könnte z.B. so aussehen´(natürlich individualisiert auf das eigene WLAN):

Die relevanten Einträge in die setup() (Abschnitt 3):

In der loop() wird nur noch geprüft, ob eine WLAN-Verbindung besteht und diese ggf. erneuert:

Die neue Funktion stellt die WLAN-Verbindung sicher (und loggt direkt mit der neuen Funktion debugOutput()):

Stufe 4b: Zeitserver

Für den Zeitserver müssen folgende Imports ergänzt und konfiguriert werden:

Am Ende der setup() (nachdem die WiFi-Verbindung mit ensureWIFIConnection(); erstellt wurde, wird der Zeitserver konfiguriert:

Schließlich muss nur noch die Funktion debugOutput() um das formatierte Datum ergänzt werden (für den Fall, dass schon Kontakt zum Zeitserver besteht)

Stufe fünf: Log-Ausgabe auf Website des ESP

Wer’s eilig hat: der fertige Code ist hier. Wir wollen jetzt auf einer Website die Log-Dateien zur Verfügung stellen - schließlich soll der ESP nicht dauerhaft per USB verbunden blieben.

Ein erster Schritt ist es, die nötigen Bibliotheken zu importieren und Variablen zu schaffen, in denen wir die Log-Infos zwischenspeichern können. Die Logs sollen für die Startphase (setup()) und das laufende Programm loop() getrennt gespeichert werden. Die 8085 ist der Port, unter der die Logs später erreichbar sein werden (Teil der URL).

Am Ende der setup() muss nun der Server gestartet werden. Es wird festgelegt, auf welchem Pfad die Log-Dateien erreichbar sind (/log - ein weiterer Teil der URL) und welche Funktion die Ausgabe generiert, die dann an den Browser gesendet wird (respondRequestWithLogEntries(): diese Funktion folgt unten).

Bei jedem Aufruf der loop() wird zu Beginn geprüft, ob die Logfile-Internetseite aufgerufen wird. Sinnvollerweise erfolgt diese Abfrage zu Beginn direkt nach ensureWIFIConnection();.

Die beiden Variabeln für die Logtexte müssen noch befüllt werden. Da es zwei verschiedene sind (setup/loop) wird mit einer Flag unterschieden, wohin der jeweilige Logtext landen soll (setupLog):

Zwei überladene Funktionen mit abweichenden Parametern sind eine Abkürzung, falls wir mal nicht Loglevel oder die neue Flag mitgeben wollen:

Schließlich benötigen wir noch die Funktion, die die Logdateien zurückgibt sowie eine Hilfsfunktion, die eine einfache HTML-Seite drumrum baut:

Wie sieht das ganze aus? Erstmal brauchen wir die IP-Adresse des ESP. Die finden wir z.B. im seriellen Monitor:

Log-Auszug aus dem seriellen Monitor, darin “IP address: 192.168.43.43”
Log-Auszug aus dem seriellen Monitor, darin “IP address: 192.168.43.43”

In meinem Fall steht da die 192.168.43.43. Zusammen mit den beiden URL-Bestandteilen, die wir oben konfiguriert hatten (Port: 8085 und Pfad: /log) ergibt sich daraus die URL, die wir im Browser eingeben können, um die Log-Datei anzuschauen.

http://192.168.43.43:8080/log

Log-Auszug in der Browseransicht, z.B. “2023-02-24_15:19:11 [INFO] Logging No. 1”
Log-Auszug in der Browseransicht, z.B. “2023-02-24_15:19:11 [INFO] Logging No. 1”

Wie weiter?

Natürlich muss man aufpassen: diese Seite ist für alle im lokalen WLAN ohne Passwort einsehbar. Es sollten daher keine Daten geloggt werden, die niemanden etwas angehen. Mittelfristig muss also noch ein Zugriffsschutz kommen.

Dazu aber an anderer Stelle mehr.

Quellen und offene Ressourcen (OER)

Die Ursprungstexte (als Markdown), Grafiken und zugrunde liegende Diagrammquelltexte finden sich (soweit möglich in weiterbearbeitbarer Form) in folgendem git-Repository:

https://gitlab.com/oer-informatik/mcu/arduino-esp.

Sofern nicht explizit anderweitig angegeben sind sie zur Nutzung als Open Education Resource (OER) unter Namensnennung (H. Stein, oer-informatik.de) freigegeben gemäß der Creative Commons Namensnennung 4.0 International Lizenz (CC BY 4.0).

Creative Commons Lizenzvertrag

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: