Überblick über das Programm
Bei dem vorgestellten Programm handelt es sich nicht um eine fertige, eigenständige Anwendung, sondern um ein stabiles und erweiterbares Basisprogramm. Es stellt die grundlegenden Funktionen bereit, die für die drahtlose Netzwerkkommunikation, die Fernübertragung von Daten sowie den automatischen Wechsel zwischen verschiedenen Zugangspunkten erforderlich sind. Auf dieser Programmstruktur können später durch das Hinzufügen weiterer Funktionen unterschiedliche Zielanwendungen aufgebaut werden.
Ziel der Entwicklung ist die Umsetzung eines ESP32-basierten WLAN-Clients, der im Hintergrund und ohne den Ablauf des Hauptprogramms zu blockieren die verfügbaren drahtlosen Netzwerke erfassen kann. Anschließend wählt er unter den Zugangspunkten mit derselben SSID denjenigen AP aus, der die günstigere Verbindung bietet. Dadurch kann das Gerät auch während der Bewegung eine stabilere und zuverlässigere Netzwerkverbindung aufrechterhalten.

Das Programm verwaltet die WLAN-Verbindung des ESP32, überwacht kontinuierlich deren Status und realisiert die MQTT-basierte Kommunikation. Darüber hinaus enthält es die grundlegende Logik für den automatischen Wechsel zwischen Zugangspunkten, also für das Roaming. Bei der Entscheidungsfindung werden die gemessene Signalstärke, ihr gemittelter Wert, die eingestellte Hysterese sowie die Stabilität des neuen Zugangspunktes berücksichtigt.
Ein besonderer Schwerpunkt der Entwicklung lag auf der Gestaltung einer zuverlässigen, übersichtlichen und leicht erweiterbaren Programmstruktur. Dementsprechend werden die Verwaltung der WLAN-Verbindung, die Netzwerksuche, das Roaming, die MQTT-Kommunikation und die Zeitsynchronisation in voneinander getrennten Funktionen umgesetzt. Dieser modulare Aufbau erleichtert die Fehlersuche und die Weiterentwicklung. Gleichzeitig ermöglicht er es, dass spätere Anwendungen die bereits implementierten Grundfunktionen mit nur minimalen Änderungen wiederverwenden können.
Die Zeitsteuerung des Programms basiert hauptsächlich auf der von der Funktion millis() zurückgegebenen Laufzeit. Dadurch lassen sich blockierende Wartezeiten vermeiden, sodass das System die Netzwerkverbindungen kontinuierlich überwachen und schnell auf Ereignisse reagieren kann.
Detaillierte Vorstellung des Programms
Verwendete Bibliotheken
Für den Betrieb des Programms werden drei Bibliotheken benötigt.
Die Bibliothek WiFi.h stellt die Funktionen des ESP32 für die drahtlose Netzwerkkommunikation bereit, beispielsweise den Verbindungsaufbau, die Suche nach Netzwerken und das Auslesen der RSSI-Werte.
Die Bibliothek PubSubClient.h ist für die Umsetzung der MQTT-Kommunikation zuständig. Mit ihrer Hilfe kann das Gerät über einen MQTT-Broker Nachrichten senden und empfangen.
Die Bibliothek time.h ermöglicht die Zeitsynchronisation über das Internet. Das Programm verwendet sie zur Bestimmung der Startzeit.
#include <WiFi.h>
#include <PubSubClient.h>
#include "time.h"
Konfiguration und globale Variablen
Am Anfang des Programms befinden sich die Konstanten und globalen Variablen, die die grundlegenden Einstellungen für die Netzwerkkommunikation und den Programmbetrieb enthalten. Hier werden der Name des WLAN-Netzwerks (SSID) und das Passwort, die IP-Adresse und die Portnummer des MQTT-Brokers sowie die MQTT-Themen festgelegt, über die der ESP32 kommuniziert.
Für die Verwendung des Programms müssen diese Werte an die eigene Netzwerkumgebung angepasst werden. Dementsprechend sind die Zugangsdaten des verwendeten WLAN-Netzwerks sowie die IP-Adresse und die für die Kommunikation verwendete Portnummer des MQTT-Brokers einzutragen.
// WLAN-Zugangsdaten
const char* ssid = "mySSID";
const char* password = "myPassword";
// Einstellungen des MQTT-Servers
const char* mqttServer = "192.168.1.2";
const uint16_t mqttPort = 1883;
Anschließend werden ein WLAN-Client-Objekt und ein MQTT-Client-Objekt erstellt. Diese sind für die Verwaltung der Netzwerkverbindung beziehungsweise der MQTT-Kommunikation verantwortlich.
Zu den globalen Variablen gehören außerdem die für die Mittelwertbildung der RSSI-Werte benötigten Arrays, die Statusvariablen zur Steuerung des Roaming-Entscheidungsprozesses sowie Variablen zur Zeitsteuerung und zur Speicherung des aktuellen Verbindungsstatus.
Die Funktionsweise des Programms kann durch die Änderung einiger Konstanten einfach angepasst werden.
Die Konstante wifiScanFreq legt fest, in welchen Zeitabständen eine neue WLAN-Netzwerksuche gestartet wird. Der Wert wird in Millisekunden angegeben.
// Zeitraum zwischen den Netzwerksuchen in Millisekunden
unsigned long wifiScanFreq = 15000; // 15 Sekunden
Die Anzahl der für die RSSI-Mittelwertbildung verwendeten Messwerte wird durch die Konstante rssiSampleCount festgelegt. Sie bestimmt, aus wie vielen aufeinanderfolgenden RSSI-Messungen das Programm einen Mittelwert berechnet. Ein höherer Wert führt zu einem stabileren Mittelwert, reagiert jedoch langsamer auf Änderungen der Signalstärke.
// Anzahl der Messwerte, aus denen der Mittelwert berechnet wird
const int rssiSampleCount = 3;
Die Konstante requiredStableScans legt fest, bei wie vielen aufeinanderfolgenden Netzwerksuchen der neue Zugangspunkt die beste Wahl sein muss, bevor das Programm den Roaming-Vorgang ausführt.
// Der neue AP muss bei mindestens so vielen aufeinanderfolgenden
// Suchvorgängen die beste Wahl sein.
const int requiredStableScans = 2;
Die Konstante rssiHysteresis gibt den minimalen Unterschied der Signalstärke an, der erforderlich ist, damit das Programm zu einem neuen Zugangspunkt wechselt. Je kleiner dieser Wert ist, desto empfindlicher reagiert das System auf Änderungen der Signalstärke. Bei einem größeren Wert hält es stabiler an der bestehenden Verbindung fest.
// Der neue AP kann nur ausgewählt werden, wenn seine Signalstärke
// mindestens um diesen Wert in dBm besser ist als die aktuelle Verbindung
const int rssiHysteresis = 12; // 12 dBm
Ereignisgesteuerte WLAN-Verwaltung
Das Programm überwacht den Zustand der WLAN-Verbindung ereignisgesteuert. Die einzelnen Netzwerkereignisse werden von der Callback-Funktion WiFiEvent() verarbeitet. Diese wird von der WLAN-Bibliothek des ESP32 automatisch aufgerufen, sobald sich der Zustand der Netzwerkverbindung ändert.
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
switch(event)
{
// Erfolgreiche Verbindung mit dem Zugangspunkt
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
....
break;
// Das Gerät hat eine IP-Adresse erhalten,
// die Verbindung ist einsatzbereit
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
....
break;
// Verbindung wurde getrennt,
// Zurücksetzen der Statusvariablen
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
...
break;
// Andere WLAN-Ereignisse müssen nicht verarbeitet werden
default:
break;
}
}
Wenn sich der ESP32 erfolgreich mit einem Zugangspunkt verbindet, gibt das Programm eine entsprechende Meldung auf dem seriellen Monitor aus. In diesem Zustand wurde die drahtlose Verbindung bereits hergestellt, das Gerät wartet jedoch noch auf die Zuweisung einer IP-Adresse.
// Erfolgreiche Verbindung mit dem Zugangspunkt
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
if(debug)
{
Serial.println();
Serial.print("Mit dem WLAN-Netzwerk ");
Serial.print(WiFi.SSID().c_str());
Serial.println(" verbunden.");
Serial.println("Warten auf die IP-Adresse ...");
}
break;
Nachdem das Gerät vom Netzwerk eine IP-Adresse erhalten hat, setzt das Programm eine Verbindungsstatusvariable, speichert die aktuelle BSSID und gibt die IP-Adresse, den aktuellen RSSI-Wert sowie den Gerätenamen aus.
Die gespeicherte BSSID wird bei späteren Netzwerksuchen benötigt. Mit ihrer Hilfe kann das Programm den aktuell verwendeten Zugangspunkt von anderen Zugangspunkten mit derselben SSID unterscheiden.
// Das Gerät hat eine IP-Adresse erhalten,
// die Verbindung ist einsatzbereit
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiConnected = true; // Verbindungsstatus setzen
currentBssid = WiFi.BSSIDstr();
if(debug)
{
Serial.println();
Serial.println("IP-Adresse empfangen!");
Serial.print("IP-Adresse: ");
Serial.println(WiFi.localIP());
Serial.print("BSSID: ");
Serial.println(currentBssid);
Serial.print("Hostname: ");
Serial.println(WiFi.getHostname());
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
Serial.println();
}
break;
Wenn die WLAN-Verbindung unterbrochen wird, löscht das Programm die Statusvariable, die den bestehenden Verbindungszustand kennzeichnet. Zusätzlich wird auch der Zustand der Netzwerksuche zurückgesetzt, damit nach der nächsten erfolgreichen Verbindung eine neue Suche gestartet werden kann.
// Verbindung wurde getrennt,
// Zurücksetzen der Statusvariablen
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
wifiConnected = false;
isScanning = false;
break;
Verbindung mit dem Netzwerk
Die Funktion setupWifi() initialisiert nicht nur die WLAN-Verbindung, sondern sorgt auch dafür, dass das Gerät aus einem definierten und bereinigten Zustand startet. Sie trennt eine eventuell bestehende frühere Verbindung, versetzt den ESP32 in den Client- beziehungsweise Station-Modus (STA), setzt den Zustand der Netzwerksuche zurück und versucht anschließend, eine Verbindung mit dem angegebenen drahtlosen Netzwerk herzustellen.
void setupWifi()
{
// Vorherige WLAN-Verbindung trennen
WiFi.disconnect(true);
// Den ESP32 in den Client- beziehungsweise Station-Modus versetzen
WiFi.mode(WIFI_STA);
// Zustand der Netzwerksuche zurücksetzen
isScanning = false;
// Verbindungsversuch mit dem angegebenen WLAN-Netzwerk
if(!connectToWifi(ssid, password, 0, NULL))
{
...
}
}
Der eigentliche Verbindungsaufbau wird von der Funktion connectToWifi() durchgeführt. Durch ihren Aufbau kann derselbe Programmabschnitt sowohl für eine normale Verbindung als auch während des Roamings verwendet werden.
Bei der erstmaligen Verbindung genügt es, den Namen des WLAN-Netzwerks (SSID) und das zugehörige Passwort anzugeben. In diesem Fall wählt der ESP32 automatisch den geeigneten Zugangspunkt aus.
Nach einem WLAN-Scan kennt das Programm beim Roaming, sofern die erforderlichen Bedingungen erfüllt sind, bereits die BSSID und den verwendeten Funkkanal des ausgewählten Zugangspunktes. Daher kann es sich direkt mit diesem AP verbinden. Dadurch wird der Verbindungsaufbau verkürzt, da keine erneute Netzwerksuche erforderlich ist.
Die Auslagerung des Verbindungsaufbaus in eine separate Funktion bietet den Vorteil, dass dieselbe Implementierung sowohl für die erstmalige Netzwerkverbindung als auch für den beim Roaming durchgeführten Wechsel des Zugangspunktes verwendet werden kann. Dadurch wird eine Wiederholung von Programmcode vermieden.
bool connectToWifi(const char* targetSsid, const char* targetPass,
int32_t channel, const uint8_t* bssid)
{
....
// Wenn die BSSID des Ziel-APs bekannt ist,
// wird die Verbindung direkt zu diesem AP hergestellt
if(bssid != NULL)
{
WiFi.begin(targetSsid, targetPass, channel, bssid, true);
}
else
{
// Andernfalls wählt der ESP32 selbst einen Zugangspunkt aus
WiFi.begin(targetSsid, targetPass);
}
....
}
Nach einer erfolgreichen Verbindung initialisiert die Funktion die für das Roaming erforderlichen Variablen. Sie startet die RSSI-Mittelwertbildung neu, löscht einen eventuell zuvor gespeicherten Roaming-Kandidaten und setzt den Roaming-Status zurück. Dadurch beginnt jede neue Verbindung aus einem definierten Ausgangszustand, unabhängig davon, ob es sich um die erstmalige Verbindung oder um einen Wechsel zwischen Zugangspunkten handelt.
....
// Bei einem fehlgeschlagenen Verbindungsaufbau
// wird false zurückgegeben
if(!wifiConnected)
{
return false;
}
// Nach erfolgreicher Verbindung wird die
// RSSI-Mittelwertbildung neu gestartet
averagedRssi = -100;
updateAveragedRssi();
// Daten der vorherigen Roaming-Entscheidung löschen
candidateBssid = "";
candidateCount = 0;
// Kennzeichnen, dass der Roaming-Vorgang abgeschlossen ist
isRoaming = false;
return true;
}
Nicht blockierende (asynchrone) WLAN-Suche
Herkömmliche WLAN-Scanmethoden können die Programmausführung für mehrere Sekunden anhalten, was bei einem IoT-Gerät zu Datenverlusten führen kann. Das Prinzip der asynchronen WLAN-Suche besteht darin, dass der ESP32 im Hintergrund nach den verfügbaren Netzwerken in seiner Umgebung sucht, ohne während der Messung untätig auf deren Abschluss zu warten. Während des im Hintergrund ausgeführten Scanvorgangs kann das Hauptprogramm kontinuierlich und ohne Unterbrechung weiterlaufen.
Die Funktion startAsynchronousScan() startet diesen Suchvorgang jedoch nur dann, wenn das Gerät bereits mit dem Netzwerk verbunden ist und aktuell kein anderer Scan ausgeführt wird.
void startAsynchronousScan()
{
// Wenn bereits ein Scan läuft oder keine aktive WLAN-Verbindung besteht,
// muss kein neuer Suchvorgang gestartet werden
if(isScanning || !wifiConnected) return;
// Aktualisierung der Signalstärke der aktuellen Verbindung
// zur Vorbereitung der Roaming-Entscheidung
updateAveragedRssi();
// Statusvariable für den Scan setzen
isScanning = true;
if(debug) Serial.println("Netzwerksuche gestartet ...");
// Asynchrone Netzwerksuche starten
// Die Programmausführung wird während des Scans nicht angehalten
WiFi.scanNetworks(true, true, false);
}
Intelligenter Roaming-Algorithmus mit Hysterese
Die Messung der WLAN-Signalstärke (RSSI) ist in der Praxis sehr instabil. Wände, Hindernisse und sogar die Bewegung des menschlichen Körpers können kurzfristige Schwankungen verursachen.
Die Funktion updateAveragedRssi() führt eine gleitende Mittelwertbildung (Moving Average) der RSSI-Werte durch. Das System trifft seine Entscheidung nicht auf Grundlage einer einzelnen RSSI-Messung, sondern verwendet den Durchschnitt der letzten drei Messergebnisse. Dadurch wird der Einfluss kurzfristiger Schwankungen der Signalstärke reduziert und eine stabilere Roaming-Entscheidung ermöglicht.
void updateAveragedRssi()
{
// Aktuelle WLAN-Signalstärke abfragen
int8_t currentRssi = WiFi.RSSI();
// Bei der ersten Messung werden alle Speicherplätze
// mit dem aktuellen Wert gefüllt, damit sofort
// ein verwendbarer Mittelwert zur Verfügung steht
if(averagedRssi == -100)
{
for(int i = 0; i < rssiSampleCount; i++)
{
rssiSamples[i] = currentRssi;
}
averagedRssi = currentRssi;
return;
}
// Das neue Messergebnis im Ringpuffer speichern
rssiSamples[rssiSampleIndex] = currentRssi;
// Position für den nächsten Messwert bestimmen
rssiSampleIndex = (rssiSampleIndex + 1) % rssiSampleCount;
// Gespeicherte RSSI-Werte summieren
int32_t sum = 0;
for(int i = 0; i < rssiSampleCount; i++)
{
sum += rssiSamples[i];
}
// Durchschnittlichen RSSI-Wert berechnen
averagedRssi = sum / rssiSampleCount;
}
Die Hysterese stellt einen Sicherheitsbereich dar, der verhindert, dass der ESP32 ständig zwischen zwei WLAN-Zugangspunkten hin- und herwechselt, wenn deren Signalstärken nahezu identisch sind. Dieses Verhalten wird als Ping-Pong-Effekt bezeichnet. Der derzeit im Programm verwendete Hysteresewert beträgt 12 dBm.
Was würde ohne Hysterese passieren? Wenn die Signalstärke des Zugangspunktes A beispielsweise -70 dBm beträgt und die Signalstärke des Zugangspunktes B kurzfristig auf -69 dBm ansteigt, würde der ESP32 die Verbindung zu A sofort trennen und zu B wechseln. Fällt die Signalstärke von B eine Sekunde später wieder auf -71 dBm, würde der ESP32 erneut zurückwechseln. Dies würde zu ständigen Unterbrechungen der Netzwerkverbindung führen.
Die Hysterese von 12 dBm verhindert dieses Verhalten, indem das Roaming nur dann gestartet wird, wenn die Signalstärke des neu gefundenen Zugangspunktes mindestens 12 dBm stärker ist als die gemittelte Signalstärke der aktuellen Verbindung.
Die Hysterese allein reicht jedoch noch nicht aus, um sicherzustellen, dass der neue Zugangspunkt dauerhaft eine bessere Verbindung bietet. Deshalb verwendet das Programm zusätzlich eine zeitliche Bestätigung, die als Debouncing bezeichnet werden kann.
Mit den aktuellen Einstellungen muss derselbe Zugangspunkt bei zwei aufeinanderfolgenden Netzwerksuchen die beste Wahl sein. Bei einem Scanintervall von 15 Sekunden entspricht dies insgesamt ungefähr 30 Sekunden.
void checkScanResults()
{
....
// Die Möglichkeit eines Roamings wird nur geprüft,
// wenn die Signalstärke des neuen APs den
// Hystereseschwellwert überschreitet
if(bestNetworkIndex != -1 &&
bestScanRssi > averagedRssi + rssiHysteresis)
{
// Prüfen, ob derselbe AP bei mehreren
// aufeinanderfolgenden Suchvorgängen
// die beste Wahl ist
if(bestScanBssid == candidateBssid)
{
candidateCount++;
}
else
{
candidateBssid = bestScanBssid;
candidateCount = 1;
}
....
Wenn alle Bedingungen innerhalb der Funktion erfüllt sind, wird der Roaming-Vorgang gestartet. Der ESP32 trennt die bestehende WLAN-Verbindung, beendet die MQTT-Kommunikation und verbindet sich anschließend direkt mit dem neuen Zugangspunkt.
Das Trennen der MQTT-Verbindung ist erforderlich, weil durch die Unterbrechung der WLAN-Verbindung auch die zugrunde liegende TCP-Verbindung ungültig wird. Nach dem Verbindungsaufbau mit dem neuen Zugangspunkt wird die MQTT-Verbindung automatisch erneut hergestellt.
....
// Wenn alle Roaming-Bedingungen erfüllt sind,
// wird zum neuen Zugangspunkt gewechselt
if(candidateCount >= requiredStableScans)
{
if(debug)
{
Serial.printf(
"Roaming von %s zu %s\n",
currentBssid.c_str(),
candidateBssid.c_str()
);
Serial.println();
}
// Kennzeichnen, dass ein Roaming-Vorgang läuft
isRoaming = true;
int32_t channel = WiFi.channel(bestNetworkIndex);
const uint8_t* bssid = WiFi.BSSID(bestNetworkIndex);
// MQTT-Verbindung vor dem Roaming trennen
if(mqttClient.connected())
{
mqttClient.disconnect();
}
// WLAN-Verbindung trennen
WiFi.disconnect();
// Scanergebnisse löschen und Speicher freigeben
WiFi.scanDelete();
isScanning = false;
// Verbindung mit dem ausgewählten AP herstellen
connectToWifi(ssid, password, channel, bssid);
return;
}
....
Nach jedem Scanzyklus gibt das Programm den belegten Speicher durch den Aufruf der Funktion WiFi.scanDelete() wieder frei und setzt die Statusvariable so, dass die Netzwerksuche als beendet gekennzeichnet wird.
Damit ist der aktuelle Scanzyklus abgeschlossen. Bis zur nächsten zeitgesteuerten Netzwerksuche arbeitet das Programm über die bestehende Verbindung weiter.
// Scanergebnisse löschen und Speicher freigeben
WiFi.scanDelete();
// Netzwerksuche als beendet kennzeichnen
isScanning = false;
....
}
MQTT-Verbindung und Fernsteuerung
Bei der MQTT-Kommunikation ist es wichtig, dass die Suche nach WLAN-Netzwerken und der Wechsel zwischen den Zugangspunkten (Roaming) keinen dauerhaften Verbindungsverlust verursachen. Wird die MQTT-Verbindung während des Roamings unterbrochen, stellt das Programm sie automatisch wieder her. Dadurch wird die Kommunikation ohne menschliches Eingreifen fortgesetzt.
Für den erneuten Aufbau der MQTT-Verbindung ist die Funktion reconnectMqtt() zuständig. Im Gegensatz zu herkömmlichen Beispielprogrammen verwendet diese Funktion keine blockierende while-Schleife. Ist der MQTT-Broker vorübergehend nicht erreichbar, bleibt das Programm daher nicht wartend stehen, sondern setzt die Ausführung der loop()-Schleife fort. Dadurch können die Verwaltung der WLAN-Verbindung, die Netzwerksuche und der Roaming-Prozess weiterhin störungsfrei ausgeführt werden.
void reconnectMqtt()
{
// Nur bei aktiver WLAN-Verbindung und nicht während einer
// Netzwerksuche versuchen, eine MQTT-Verbindung herzustellen
if(wifiConnected && !mqttClient.connected() && !isScanning)
{
if(debug)
{
Serial.println("Verbindung zum MQTT-Server wird hergestellt ...");
}
// Verbindung mit dem MQTT-Broker herstellen
if(mqttClient.connect(mqttClientId))
{
if(debug)
{
Serial.println("Verbindung zum MQTT-Server hergestellt!");
Serial.println();
}
....
Das Programm abonniert das Debug-Thema. Dadurch kann der Debug- beziehungsweise Protokollierungsmodus der seriellen Schnittstelle mit einer einzigen MQTT-Nachricht aus der Ferne ein- oder ausgeschaltet werden. Im aktivierten Zustand gibt das Programm detaillierte Informationen auf dem seriellen Monitor aus, was die Überprüfung der Funktionsweise und die Fehlersuche erheblich erleichtert.
Bei einer späteren Erweiterung des Programms können an dieser Stelle weitere Themen abonniert werden.
....
// Erforderliche MQTT-Themen abonnieren
mqttClient.subscribe(debugTopic);
// Weitere Themen abonnieren ...
}
else
{
if(debug)
{
Serial.print("Verbindung zum MQTT-Server fehlgeschlagen. | rc=");
Serial.println(mqttClient.state());
Serial.println();
}
}
}
}
Die Verarbeitung eingehender MQTT-Nachrichten erfolgt in der Funktion mqttCallback(). Diese Funktion wird automatisch aufgerufen, sobald der MQTT-Broker eine neue Nachricht zu einem abonnierten Thema weiterleitet.
In der aktuellen Implementierung ermöglicht die Callback-Funktion die Fernsteuerung des Debug-Modus. Durch das Senden der Werte true oder false an das Debug-Thema kann die Protokollierung über die serielle Schnittstelle während des laufenden Betriebs ein- oder ausgeschaltet werden. Dadurch ist weder eine erneute Kompilierung noch ein Neustart des Programms erforderlich.
void mqttCallback(char* topic, byte* payload, unsigned int length)
{
// Eingegangenes Thema und Nachricht in Zeichenketten umwandeln
String strTopic = String(topic);
String strPayload = String((char*)payload, length);
// Fernsteuerung des Debug-Modus über eine MQTT-Nachricht
if(strTopic == debugTopic)
{
if(strPayload == "true")
{
debug = true;
Serial.println("Debug-Modus aktiviert!");
Serial.println();
}
else if(strPayload == "false")
{
Serial.println("Debug-Modus deaktiviert!");
Serial.println();
debug = false;
}
}
// Verarbeitung weiterer MQTT-Nachrichten ...
}
Der Aufbau der Callback-Funktion ermöglicht es, später auf einfache Weise weitere MQTT-Befehle hinzuzufügen. Dadurch kann das System problemlos um neue Funktionen erweitert werden.
Während des Betriebs veröffentlicht der ESP32 kontinuierlich die wichtigsten Statusinformationen, beispielsweise die aktuelle IP-Adresse und den gemittelten RSSI-Wert. Diese Daten werden in dafür vorgesehenen MQTT-Themen veröffentlicht, sodass eine externe Anwendung oder ein Überwachungssystem den Zustand des Geräts fortlaufend verfolgen kann.
Die Veröffentlichung erfolgt am Ende der Funktion checkScanResults(). Dies hat den Vorteil, dass die WLAN-Verbindung und die MQTT-Kommunikation nach einem Roaming-Vorgang bereits wiederhergestellt sind. Die veröffentlichten Daten spiegeln daher stets den aktuellen Netzwerkzustand wider.
void checkScanResults()
{
....
// Aktuellen Status über MQTT übertragen
if(mqttClient.connected())
{
mqttClient.publish(rssiTopic, String(averagedRssi).c_str());
mqttClient.publish(ipTopic, WiFi.localIP().toString().c_str());
if(debug)
{
Serial.print("Gemittelter RSSI-Wert: ");
Serial.println(averagedRssi);
Serial.print("IP-Adresse: ");
Serial.println(WiFi.localIP());
Serial.println();
}
}
}
Startzeitstempel mit Zeitzonenverwaltung
Nach der ersten erfolgreichen WLAN- und MQTT-Verbindung ruft das Gerät das genaue Datum und die aktuelle Uhrzeit vom NTP-Server (Network Time Protocol) pool.ntp.org ab. Anschließend wird die mitteleuropäische Zeitzone (CET/CEST) eingestellt, die den Wechsel zwischen Sommer- und Winterzeit automatisch berücksichtigt.
Auf Grundlage der abgerufenen Zeit erzeugt das Programm einen formatierten Zeitstempel und sendet diesen als MQTT-Nachricht an den Broker. Dadurch kann der Server den genauen Startzeitpunkt des Geräts protokollieren. Dies erleichtert die Überwachung des Systembetriebs sowie die nachträgliche Analyse möglicher Fehler.
void sendLocalTime()
{
// Aktuelle Zeit vom NTP-Server abrufen
if(!getLocalTime(&timeinfo))
{
....
// Zeitstempel formatieren
strftime(
Msg_Timestamp,
sizeof(Msg_Timestamp),
"%Y.%m.%d-%H:%M:%S",
&timeinfo
);
....
// Zeitstempel als MQTT-Nachricht senden
mqttClient.publish(startTimeTopic, formattedTime);
}
Vor der Verwendung der NTP-basierten Zeitsynchronisation müssen die verwendete Zeitzone und der NTP-Server konfiguriert werden. Dies erfolgt in der Funktion setup() durch den Aufruf der Funktion configTzTime().
void setup()
{
....
// Zeitsynchronisation mithilfe eines NTP-Servers konfigurieren
configTzTime(timeZone, ntpServer);
}
Der Zeitstempel wird nur einmal nach der ersten erfolgreichen Verbindung gesendet. Das Programm stellt dies mithilfe einer Statusvariablen sicher. Dadurch wird die Nachricht bei späteren Verbindungswiederherstellungen oder Roaming-Vorgängen nicht erneut übertragen.
void loop()
{
....
mqttClient.loop();
// Startzeitstempel nur einmal senden
if(first)
{
sendLocalTime();
first = false;
}
....
}
Initialisierung des Systems
Die Funktion setup() wird beim Programmstart einmal ausgeführt und übernimmt die für den Systembetrieb erforderlichen Initialisierungsaufgaben. Dabei richtet sie die serielle Kommunikation ein, registriert die Ereignisbehandlungsfunktionen für WLAN und MQTT, initialisiert die Netzwerkverbindung und konfiguriert die NTP-basierte Zeitsynchronisation.
void setup()
{
// Serielle Kommunikation initialisieren
Serial.begin(115200);
// Kurze Wartezeit zur Stabilisierung des Systems
delay(2000);
// WLAN-Ereignisbehandlung registrieren
WiFi.onEvent(WiFiEvent);
// WLAN-Verbindung initialisieren
setupWifi();
// Adresse und Port des MQTT-Servers festlegen
mqttClient.setServer(mqttServer, mqttPort);
// MQTT-Callback-Funktion registrieren
mqttClient.setCallback(mqttCallback);
// Zeitsynchronisation mithilfe eines NTP-Servers konfigurieren
configTzTime(timeZone, ntpServer);
}
Damit sind alle erforderlichen Dienste für die Ausführung des Hauptprogramms vorbereitet.
Funktionsweise des Hauptprogramms
Die Funktion loop() bildet die zentrale Steuerungsschleife des Programms und wird während des gesamten Systembetriebs kontinuierlich wiederholt. Ihre Aufgabe besteht darin, die WLAN- und MQTT-Verbindungen zu überwachen, die Netzwerksuche zeitlich zu steuern, den Roaming-Prozess zu verwalten und weitere zeitgesteuerte Aufgaben auszuführen.

Das Hauptprogramm verwendet keine durch die Funktion delay() verursachten blockierenden Wartezeiten. Dadurch können die verschiedenen Aufgaben unabhängig voneinander und kontinuierlich ausgeführt werden. Die Zeitsteuerung basiert auf der von der Funktion millis() zurückgegebenen Laufzeit. Dies ermöglicht einen nicht blockierenden Betrieb und eine schnelle Reaktion des Systems auf Ereignisse.
Zunächst prüft das Programm, ob der ESP32 über eine aktive WLAN-Verbindung verfügt. Falls die Verbindung unterbrochen wurde und die Unterbrechung nicht durch einen Roaming-Vorgang verursacht wurde, startet das Programm automatisch einen neuen Verbindungsversuch.
Ist die WLAN-Verbindung aktiv, startet das Programm in festgelegten Zeitabständen eine asynchrone Netzwerksuche. Nach Abschluss des Scanvorgangs werden die Ergebnisse ausgewertet. Falls erforderlich, führt das Programm anschließend einen Wechsel zwischen den Zugangspunkten, also einen Roaming-Vorgang, durch.
Parallel dazu überwacht das Programm kontinuierlich den Zustand der MQTT-Verbindung. Bei Bedarf stellt es die Verbindung zum Broker automatisch wieder her, verarbeitet eingehende MQTT-Nachrichten und sendet nach der ersten erfolgreichen Verbindung einmalig den Startzeitstempel.
void loop()
{
// Aktuelle Laufzeit abfragen
currentMillis = millis();
// Wenn keine WLAN-Verbindung besteht und die Unterbrechung
// nicht durch einen Roaming-Vorgang verursacht wurde,
// wird der Verbindungsaufbau erneut gestartet
if(!wifiConnected && !isRoaming)
{
setupWifi();
return;
}
else if(wifiConnected)
{
// In festgelegten Zeitabständen eine neue Netzwerksuche starten
if(currentMillis - prevScanMillis > wifiScanFreq)
{
prevScanMillis = currentMillis;
startAsynchronousScan();
}
// Ergebnisse der Netzwerksuche auswerten
if(isScanning)
{
checkScanResults();
}
// MQTT-Verbindung bei Bedarf wiederherstellen
if(!mqttClient.connected())
{
reconnectMqtt();
}
else
{
// MQTT-Kommunikation aufrechterhalten
mqttClient.loop();
// Startzeitstempel nur einmal senden
if(first)
{
sendLocalTime();
first = false;
}
// Weitere MQTT-Nachrichten können hier gesendet werden ...
}
}
}
Anhang
Der vollständige, kommentierte Quellcode:
#include <WiFi.h>
#include <PubSubClient.h>
#include "time.h"
// WLAN-Zugangsdaten
const char* ssid = "mySSID";
const char* password = "myPassword";
// Einstellungen des MQTT-Servers
const char* mqttServer = "192.168.1.2";
const uint16_t mqttPort = 1883;
const char* mqttClientId = "ESP32_Mesh_Node";
// MQTT-Themen
const char* rssiTopic = "esp32/RSSI";
const char* debugTopic = "esp32/DebugEnabled";
const char* ipTopic = "esp32/IP";
const char* newstartTopic = "esp32/Newstart";
// TCP- und MQTT-Client-Objekte erstellen
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// Debug-Modus aktivieren
bool debug = true;
// Variablen für die Zeitsteuerung
unsigned long currentMillis = 0;
unsigned long wifiScanFreq = 15000; // Intervall der Netzwerksuche (15 Sekunden)
unsigned long prevScanMillis = 0;
// Daten des aktuellen APs
String currentBssid;
int8_t rssi;
bool isScanning = false; // Zeigt an, ob gerade eine Netzwerksuche läuft
// Variablen zur Berechnung des gleitenden RSSI-Mittelwerts
const int rssiSampleCount = 3;
int8_t rssiSamples[rssiSampleCount] = {0};
int rssiSampleIndex = 0;
int8_t averagedRssi = -100;
// Variablen für die Roaming-Entscheidung
String candidateBssid = "";
int candidateCount = 0;
// Der neue AP muss bei mindestens zwei aufeinanderfolgenden Suchvorgängen
// die beste Wahl sein
const int requiredStableScans = 2;
// Der neue AP kann nur ausgewählt werden, wenn seine Signalstärke
// mindestens 12 dBm besser ist als die aktuelle Verbindung
const int rssiHysteresis = 12;
// Einstellungen des NTP-Servers und der Zeitzone
const char* ntpServer = "pool.ntp.org";
const char* timeZone = "CET-1CEST, M3.5.0, M10.5.0/3";
// Der Startzeitstempel soll nur einmal gesendet werden
bool first = true;
// Statusvariablen
bool wifiConnected = false;
bool isRoaming = false;
/*****************************************************************************/
// WLAN-Ereignisbehandlungsfunktion.
// Das System ruft diese Funktion automatisch auf, wenn sich der Zustand
// der WLAN-Verbindung ändert, beispielsweise bei einer Verbindung,
// beim Erhalt einer IP-Adresse oder bei einem Verbindungsabbruch.
// Abhängig vom Ereignistyp aktualisiert sie den Programmstatus und zeigt
// bei Bedarf Diagnoseinformationen an.
/*****************************************************************************/
void WiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info)
{
switch(event)
{
// Erfolgreiche Verbindung mit dem Zugangspunkt
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
if(debug)
{
Serial.println();
Serial.print("Mit dem WLAN-Netzwerk ");
Serial.println(WiFi.SSID().c_str());
Serial.println(" verbunden. Warten auf die IP-Adresse ...");
}
break;
// Das Gerät hat eine IP-Adresse erhalten,
// die Verbindung ist einsatzbereit
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
wifiConnected = true;
currentBssid = WiFi.BSSIDstr();
if(debug)
{
Serial.println();
Serial.println("IP-Adresse empfangen!");
Serial.print("IP-Adresse: ");
Serial.println(WiFi.localIP());
Serial.print("BSSID: ");
Serial.println(currentBssid);
Serial.print("Hostname: ");
Serial.println(WiFi.getHostname());
Serial.print("RSSI: ");
Serial.println(WiFi.RSSI());
Serial.println();
}
break;
// Verbindung wurde getrennt, Statusvariablen zurücksetzen
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
wifiConnected = false;
isScanning = false;
break;
// Andere WLAN-Ereignisse müssen nicht verarbeitet werden
default:
break;
}
}
/*****************************************************************************/
// Verbindung mit dem angegebenen WLAN-Netzwerk herstellen.
// Bei einer normalen Verbindung genügt die Angabe von SSID und Passwort.
// Während des Roamings können zusätzlich eine konkrete BSSID und ein Kanal
// angegeben werden, wodurch ein schnellerer AP-Wechsel ermöglicht wird.
/*****************************************************************************/
bool connectToWifi(const char* targetSsid, const char* targetPass,
int32_t channel, const uint8_t* bssid)
{
// Verbindungsstatus zurücksetzen
wifiConnected = false;
if(debug) Serial.print("Verbindung mit dem WLAN wird hergestellt ...");
// Wenn die BSSID des Ziel-APs bekannt ist,
// wird die Verbindung direkt zu diesem AP hergestellt
if(bssid != NULL)
{
WiFi.begin(targetSsid, targetPass, channel, bssid, true);
}
else
{
// Andernfalls wählt der ESP32 selbst einen Zugangspunkt aus
WiFi.begin(targetSsid, targetPass);
}
// Auf eine erfolgreiche Verbindung warten, maximal 15 Sekunden
int attempts = 0;
while(!wifiConnected && attempts < 30)
{
delay(500);
attempts++;
}
// Bei einem fehlgeschlagenen Verbindungsaufbau false zurückgeben
if(!wifiConnected)
{
return false;
}
// Nach einer erfolgreichen Verbindung die RSSI-Mittelwertbildung neu starten
averagedRssi = -100;
updateAveragedRssi();
// Daten der vorherigen Roaming-Entscheidung löschen
candidateBssid = "";
candidateCount = 0;
// Kennzeichnen, dass der Roaming-Vorgang abgeschlossen ist
isRoaming = false;
return true;
}
/*****************************************************************************/
// WLAN-Verbindung initialisieren.
// Eine möglicherweise bestehende Verbindung wird getrennt, der ESP32 wird
// in den Client- beziehungsweise Station-Modus versetzt und anschließend
// wird versucht, eine Verbindung mit dem angegebenen WLAN herzustellen.
/*****************************************************************************/
void setupWifi()
{
// Vorherige WLAN-Verbindung trennen
WiFi.disconnect(true);
// Den ESP32 in den Client- beziehungsweise Station-Modus versetzen
WiFi.mode(WIFI_STA);
// Zustand der Netzwerksuche zurücksetzen
isScanning = false;
// Verbindungsversuch mit dem angegebenen WLAN-Netzwerk
if(!connectToWifi(ssid, password, 0, NULL))
{
if(debug)
{
Serial.println();
Serial.println(
"Erster Verbindungsversuch fehlgeschlagen. "
"Warten auf weitere Ereignisversuche."
);
}
}
}
/*****************************************************************************/
// Asynchrone WLAN-Netzwerksuche starten.
// Die Funktion prüft, ob eine neue Suche gestartet werden kann, und beginnt
// anschließend im Hintergrund mit der Erfassung der verfügbaren Netzwerke.
// Die Suche blockiert die Programmausführung nicht, sodass andere Aufgaben,
// beispielsweise die MQTT-Kommunikation, weiterhin ausgeführt werden können.
/*****************************************************************************/
void startAsynchronousScan()
{
// Wenn bereits eine Suche läuft oder keine aktive WLAN-Verbindung besteht,
// muss keine neue Suche gestartet werden
if(isScanning || !wifiConnected) return;
// Signalstärke der aktuellen Verbindung zur Vorbereitung
// der Roaming-Entscheidung aktualisieren
updateAveragedRssi();
// Statusvariable für die Netzwerksuche setzen
isScanning = true;
if(debug) Serial.println("Netzwerksuche gestartet ...");
// Asynchrone Netzwerksuche starten
// Die Programmausführung wird während der Suche nicht angehalten
WiFi.scanNetworks(true, true, false);
}
/*****************************************************************************/
// Aktuellen RSSI-Wert aktualisieren und mitteln.
// Aufgrund der natürlichen Schwankungen der WLAN-Signalstärke verwendet
// das Programm für Roaming-Entscheidungen einen aus mehreren Messwerten
// berechneten Mittelwert. Dadurch wird der Einfluss kurzfristiger
// Signalstärkeschwankungen reduziert.
/*****************************************************************************/
void updateAveragedRssi()
{
// Aktuelle WLAN-Signalstärke abfragen
int8_t currentRssi = WiFi.RSSI();
// Bei der ersten Messung werden alle Speicherplätze mit dem aktuellen
// Wert gefüllt, damit sofort ein verwendbarer Mittelwert verfügbar ist
if(averagedRssi == -100)
{
for(int i = 0; i < rssiSampleCount; i++)
{
rssiSamples[i] = currentRssi;
}
averagedRssi = currentRssi;
return;
}
// Neues Messergebnis im Ringpuffer speichern
rssiSamples[rssiSampleIndex] = currentRssi;
// Position für den nächsten Messwert bestimmen
rssiSampleIndex = (rssiSampleIndex + 1) % rssiSampleCount;
// Gespeicherte RSSI-Werte summieren
int32_t sum = 0;
for(int i = 0; i < rssiSampleCount; i++)
{
sum += rssiSamples[i];
}
// Durchschnittlichen RSSI-Wert berechnen
averagedRssi = sum / rssiSampleCount;
}
/*****************************************************************************/
// Ergebnisse der Netzwerksuche auswerten.
// Die Funktion wählt den günstigsten Zugangspunkt aus und entscheidet anhand
// der Hysterese- und Stabilitätsbedingungen, ob ein Roaming-Vorgang
// durchgeführt werden muss.
/*****************************************************************************/
void checkScanResults()
{
// Prüfen, ob die Netzwerksuche abgeschlossen ist
int16_t networks = WiFi.scanComplete();
if(networks == WIFI_SCAN_RUNNING) return;
// Bei einer fehlgeschlagenen Suche den Suchstatus zurücksetzen
if(networks == WIFI_SCAN_FAILED)
{
if(debug)
{
Serial.println("Netzwerksuche fehlgeschlagen!");
Serial.println();
}
isScanning = false;
return;
}
if(networks > 0)
{
// Daten des besten während der Suche gefundenen APs speichern
int bestNetworkIndex = -1;
int8_t bestScanRssi = -100;
String bestScanBssid = "";
if(debug)
{
Serial.print("Netzwerksuche abgeschlossen. ");
Serial.print(networks);
Serial.println(" Netzwerke gefunden:");
}
// Unter den Netzwerken mit derselben SSID wird der stärkste,
// aktuell nicht verwendete Zugangspunkt ausgewählt
for(int i = 0; i < networks; i++)
{
if(WiFi.SSID(i) == ssid)
{
int8_t scanRssi = WiFi.RSSI(i);
if(scanRssi > bestScanRssi && WiFi.BSSIDstr(i) != currentBssid)
{
bestScanRssi = scanRssi;
bestScanBssid = WiFi.BSSIDstr(i);
bestNetworkIndex = i;
}
}
if(debug)
{
Serial.print(i + 1);
Serial.print(". SSID: ");
Serial.print(WiFi.SSID(i));
Serial.print(" | RSSI: ");
Serial.print(WiFi.RSSI(i));
Serial.print(" | BSSID: ");
Serial.println(WiFi.BSSIDstr(i));
}
}
if(debug) Serial.println();
// Roaming nur prüfen, wenn die Signalstärke des neuen APs
// den Hystereseschwellwert überschreitet
if(bestNetworkIndex != -1 &&
bestScanRssi > averagedRssi + rssiHysteresis)
{
// Prüfen, ob derselbe AP bei mehreren aufeinanderfolgenden
// Suchvorgängen die beste Wahl ist
if(bestScanBssid == candidateBssid)
{
candidateCount++;
}
else
{
candidateBssid = bestScanBssid;
candidateCount = 1;
}
// Wenn die Roaming-Bedingungen erfüllt sind,
// zum neuen Zugangspunkt wechseln
if(candidateCount >= requiredStableScans)
{
if(debug)
{
Serial.printf(
"Roaming von %s zu %s\n",
currentBssid.c_str(),
candidateBssid.c_str()
);
Serial.println();
}
// Kennzeichnen, dass ein Roaming-Vorgang läuft
isRoaming = true;
int32_t channel = WiFi.channel(bestNetworkIndex);
const uint8_t* bssid = WiFi.BSSID(bestNetworkIndex);
// MQTT-Verbindung vor dem Roaming trennen
if(mqttClient.connected())
{
mqttClient.disconnect();
}
// WLAN-Verbindung trennen
WiFi.disconnect();
// Scanergebnisse löschen und Speicher freigeben
WiFi.scanDelete();
isScanning = false;
// Verbindung mit dem ausgewählten AP herstellen
connectToWifi(ssid, password, channel, bssid);
return;
}
}
else
{
// Wenn kein geeigneter Roaming-Zielpunkt vorhanden ist,
// den vorherigen Kandidaten löschen
candidateBssid = "";
candidateCount = 0;
}
}
// Scanergebnisse löschen und Speicher freigeben
WiFi.scanDelete();
// Netzwerksuche als beendet kennzeichnen
isScanning = false;
// Aktuellen Status über MQTT übertragen
if(mqttClient.connected())
{
mqttClient.publish(rssiTopic, String(averagedRssi).c_str());
mqttClient.publish(ipTopic, WiFi.localIP().toString().c_str());
if(debug)
{
Serial.print("Gemittelter RSSI-Wert: ");
Serial.println(averagedRssi);
Serial.print("IP-Adresse: ");
Serial.println(WiFi.localIP());
Serial.println();
}
}
}
/*****************************************************************************/
// Aktuelles Datum und aktuelle Uhrzeit von einem NTP-Server abrufen.
// Nach erfolgreicher Zeitsynchronisation wird der Zeitstempel formatiert
// und als MQTT-Nachricht zur Protokollierung der Gerätestartzeit gesendet.
/*****************************************************************************/
void sendLocalTime()
{
char Msg_Timestamp[20];
struct tm timeinfo;
// Aktuelle Zeit vom NTP-Server abrufen
if(!getLocalTime(&timeinfo))
{
if(debug)
{
Serial.println(
"Die Zeit konnte nicht vom NTP-Server abgerufen werden!"
);
Serial.println();
}
return;
}
else
{
if(debug) Serial.println("Zeit erfolgreich abgerufen!");
}
// Zeit formatieren
strftime(
Msg_Timestamp,
sizeof(Msg_Timestamp),
"%Y.%m.%d-%H:%M:%S",
&timeinfo
);
if(debug)
{
Serial.println(Msg_Timestamp);
Serial.println();
}
// Zeitstempel als MQTT-Nachricht senden
mqttClient.publish(newstartTopic, Msg_Timestamp);
}
/*****************************************************************************/
// MQTT-Callback-Funktion.
// Der MQTT-Client ruft diese Funktion automatisch auf, wenn eine neue
// Nachricht empfangen wird. Abhängig vom Thema und Nachrichteninhalt führt
// sie die erforderlichen Aktionen aus. Derzeit ermöglicht sie die
// Fernsteuerung des Debug-Modus. Später können hier weitere MQTT-Nachrichten
// für zusätzliche Funktionen verarbeitet werden.
/*****************************************************************************/
void mqttCallback(char* topic, byte* payload, unsigned int length)
{
// Eingegangenes Thema und Nachricht in Zeichenketten umwandeln
String strTopic = String(topic);
String strPayload = String((char*)payload, length);
// Fernsteuerung des Debug-Modus über eine MQTT-Nachricht
if(strTopic == debugTopic)
{
if(strPayload == "true")
{
debug = true;
Serial.println("Debug-Modus aktiviert!");
Serial.println();
}
else if(strPayload == "false")
{
Serial.println("Debug-Modus deaktiviert!");
Serial.println();
debug = false;
}
}
// Weitere MQTT-Nachrichten verarbeiten ...
}
/*****************************************************************************/
// MQTT-Verbindung wiederherstellen.
// Wenn die WLAN-Verbindung aktiv, die MQTT-Verbindung jedoch unterbrochen ist,
// versucht die Funktion, erneut eine Verbindung mit dem Broker herzustellen.
// Nach erfolgreicher Verbindung werden die erforderlichen MQTT-Themen
// abonniert.
/*****************************************************************************/
void reconnectMqtt()
{
// Nur bei aktiver WLAN-Verbindung und nicht während einer Netzwerksuche
// versuchen, eine MQTT-Verbindung herzustellen
if(wifiConnected && !mqttClient.connected() && !isScanning)
{
if(debug)
{
Serial.println(
"Verbindung mit dem MQTT-Server wird hergestellt ..."
);
}
// Verbindung mit dem MQTT-Broker herstellen
if(mqttClient.connect(mqttClientId))
{
if(debug)
{
Serial.println("Verbindung mit dem MQTT-Server hergestellt!");
Serial.println();
}
// Erforderliche MQTT-Themen abonnieren
mqttClient.subscribe(debugTopic);
}
else
{
if(debug)
{
Serial.print(
"Verbindung mit dem MQTT-Server fehlgeschlagen. | rc="
);
Serial.println(mqttClient.state());
Serial.println();
}
}
}
}
/*****************************************************************************/
// Initialisierung des Programms
/*****************************************************************************/
void setup()
{
// Serielle Kommunikation initialisieren
Serial.begin(115200);
// Kurze Wartezeit zur Stabilisierung des Systems
delay(2000);
// WLAN-Ereignisbehandlung registrieren
WiFi.onEvent(WiFiEvent);
// WLAN-Verbindung initialisieren
setupWifi();
// Adresse und Port des MQTT-Servers festlegen
mqttClient.setServer(mqttServer, mqttPort);
// MQTT-Callback-Funktion registrieren
mqttClient.setCallback(mqttCallback);
// Zeitsynchronisation mithilfe eines NTP-Servers konfigurieren
configTzTime(timeZone, ntpServer);
}
/*****************************************************************************/
// Hauptschleife des Programms.
// Sie überwacht kontinuierlich den Zustand der WLAN- und MQTT-Verbindungen,
// startet zeitgesteuert die Netzwerksuche, wertet deren Ergebnisse aus und
// sorgt für den kontinuierlichen Betrieb des Roamings und der
// MQTT-Kommunikation.
/*****************************************************************************/
void loop()
{
// Aktuelle Laufzeit abfragen
currentMillis = millis();
// Wenn keine WLAN-Verbindung besteht und die Unterbrechung nicht durch
// einen Roaming-Vorgang verursacht wurde, den Verbindungsaufbau neu starten
if(!wifiConnected && !isRoaming)
{
setupWifi();
return;
}
else if(wifiConnected)
{
// In festgelegten Zeitabständen eine neue Netzwerksuche starten
if(currentMillis - prevScanMillis > wifiScanFreq)
{
prevScanMillis = currentMillis;
startAsynchronousScan();
}
// Ergebnisse der Netzwerksuche auswerten
if(isScanning)
{
checkScanResults();
}
// MQTT-Verbindung bei Bedarf wiederherstellen
if(!mqttClient.connected())
{
reconnectMqtt();
}
else
{
// MQTT-Kommunikation aufrechterhalten
mqttClient.loop();
// Startzeitstempel nur einmal senden
if(first)
{
sendLocalTime();
first = false;
}
// Weitere MQTT-Nachrichten können hier gesendet werden ...
}
}
}