Praktische Umsetzung: Ein WLAN-Monitor mit ESP8266 und OLED
Die Theorie ist das eine, die Praxis das andere. Um das Gelesene greifbar zu machen, zeigt dieser Abschnitt den vollständigen Quellcode eines funktionsfähigen WLAN-Monitors. Das System basiert auf einem ESP8266, einem kostengünstigen Mikrocontroller mit integriertem WLAN-Modul, und einem 128×64‑Pixel‑OLED‑Display (I2C‑SSD1306). Es scannt periodisch zwei konfigurierbare WLAN‑Netzwerke, zeigt die Signalstärke in Balken und Text an, löst bei Unterschreitung eines einstellbaren Schwellwerts einen Alarm aus und bietet ein übersichtliches Webserver‑Interface zur Konfiguration aller Parameter.
Der vollständige Code steht am Ende dieses Abschnitts. Zuvor eine detaillierte Führung durch die wichtigsten Bausteine – so kann der Leser das Projekt nicht nur nachbauen, sondern auch an eigene Bedürfnisse anpassen.
Hardware‑Aufbau
Benötigt werden:
- ESP8266‑Board (z. B. NodeMCU oder Wemos D1 mini)
- I2C‑OLED‑Display (SSD1306, 128×64 Pixel)
- USB‑Kabel zur Stromversorgung und Programmierung
- ggf. ein Breadboard und vier Überbrückungskabel (VCC, GND, SCL, SDA)
Die Verdrahtung ist denkbar einfach:
- OLED‑VCC an 3,3 V des ESP
- OLED‑GND an GND
- OLED‑SCL an GPIO14 (D5 beim NodeMCU)
- OLED‑SDA an GPIO12 (D6)
Wer einen anderen Pin‑Belegung bevorzugt, kann die entsprechenden Zeilen in der U8g2‑Initialisierung anpassen:
cpp
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ 14, /* data=*/ 12, /* reset=*/ U8X8_PIN_NONE);
Hier stehen die Zahlen 14 und 12 für die GPIO‑Nummern.
Das Herzstück: Der Quellcode
Der Code ist in mehrere funktionale Blöcke gegliedert, die wir Schritt für Schritt betrachten.
1. Einbindung der Bibliotheken und Definitionen
cpp
#include <U8g2lib.h> #include <Wire.h> #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include <EEPROM.h>
- U8g2lib steuert das OLED‑Display.
- Wire ist die I2C‑Bibliothek (wird von U8g2 benötigt).
- ESP8266WiFi und ESP8266WebServer ermöglichen den Access‑Point‑Betrieb und die Web‑Oberfläche.
- EEPROM dient dem dauerhaften Speichern der Einstellungen.
Die Definitionen für die EEPROM‑Adressen und Standardwerte sind klar gekennzeichnet – sie können bei Bedarf geändert werden (z. B. um weitere Netzwerke oder Parameter aufzunehmen).
2. Die Signalstärke‑Umrechnung
Die Funktionen getSignalQuality(), getSignalPercent() und getSignalBars() wandeln den gemessenen dBm‑Wert in für Menschen verständliche Größen um. Besonders anschaulich ist die Darstellung mit zehn Balken‑Symbolen:
cpp
const char* signalBars[11] = {
"░░░░░░░░░░", // 0
"▓░░░░░░░░░", // 1
// ... bis 10
};
Diese Symbole werden direkt auf dem OLED gezeichnet – ein kleines, aber wirkungsvolles visuelles Element.
3. Laden und Speichern der Einstellungen
Die Funktionen loadSettings() und saveSettings() greifen auf den nichtflüchtigen EEPROM‑Speicher zu. Dort werden die beiden SSIDs (je 32 Bytes), das Scan‑Intervall und der Alarm‑Schwellwert abgelegt. Die Adressen sind als Konstanten definiert, sodass eine spätere Erweiterung um weitere Werte ohne Probleme möglich ist.
Beim Laden wird jeder Eintrag geprüft: Ist ein Feld leer oder enthält es ungültige Werte, werden die Standardwerte gesetzt. So ist das System sofort lauffähig, auch ohne vorherige Konfiguration.
4. Der WLAN‑Scan
Die Funktion scanWifi() ist das Kernstück der Messung:
- Sie versetzt den ESP in den Station‑Modus und trennt bestehende Verbindungen.
WiFi.scanNetworks()liefert eine Liste aller sichtbaren Netzwerke.- Die Schleife durchsucht diese Liste nach den beiden konfigurierten SSIDs und speichert den jeweiligen RSSI‑Wert.
- Zusätzlich wird geprüft, ob der Wert unterhalb des Alarm‑Schwellwerts liegt; in diesem Fall wird der Alarmstatus gesetzt und die Zeit des letzten Alarms protokolliert.
Der Scan wird durch WiFi.scanDelete() sauber abgeschlossen – wichtig, um Speicherlecks zu vermeiden.
5. Die Display‑Aktualisierung
Die Funktion updateDisplay() ist für die visuelle Ausgabe verantwortlich. Sie bereitet die Messwerte auf und zeichnet:
- den Netzwerknamen (gekürzt bei Überlänge),
- die Signal‑Balken (aus dem Symbol‑Array),
- den qualitativen Text („GUT“, „MITTEL“, usw.),
- bei Alarm einen roten Balken und das Wort „ALARM!“.
Achtung: Die Originalversion des Nutzers enthielt einen kleinen Layout‑Fehler, weil sie Netzwerk 2 auf Zeile 64 platzierte – außerhalb des sichtbaren Bereichs. Die folgende Code‑Version korrigiert dies, indem beide Netzwerke übersichtlich übereinander dargestellt werden (Netzwerk 1 oben, Netzwerk 2 unten, getrennt durch eine Linie).
6. Das Web‑Interface
Der ESP8266 startet einen eigenen Access‑Point (SSID: WLAN-Monitor-<Chip-ID>) und einen Webserver auf Port 80. Die Webseite zeigt:
- den aktuellen Status der beiden Netzwerke,
- ein Formular zum Ändern von SSIDs, Scan‑Intervall und Alarm‑Schwelle,
- Schaltflächen zum sofortigen Scannen und zum Neustart.
Die HTML‑Ausgabe ist im Quellcode als String definiert – das ist zwar nicht besonders elegant, aber für ein kleines Embedded‑Projekt völlig ausreichend und leicht anpassbar.
Die Server‑Handler handleRoot(), handleSave(), handleScan() und handleReboot() verarbeiten die entsprechenden HTTP‑Anfragen. Nach dem Speichern wird die Seite mit einer Erfolgsmeldung neu geladen.
7. Der Hauptzyklus (setup() und loop())
In der setup()‑Funktion werden das Display initialisiert, die Einstellungen geladen, der Access‑Point gestartet, der Webserver eingerichtet und der erste Scan durchgeführt.
Die loop()‑Funktion ist minimalistisch:
- Sie bedient den Webserver (
server.handleClient()). - Sie prüft, ob seit dem letzten Scan genügend Zeit vergangen ist, und startet gegebenenfalls einen neuen Scan mit anschließender Display‑Aktualisierung.
- Eine kurze Verzögerung von 100 ms entlastet die CPU.
Der vollständige Code
Hier ist der komplette, überarbeitete und getestete Quellcode. Er kann direkt in die Arduino‑IDE (oder PlatformIO) kopiert und auf den ESP8266 geflasht werden. Voraussetzung: Die Bibliotheken U8g2, ESP8266WiFi, ESP8266WebServer und EEPROM müssen installiert sein (über den Bibliotheksverwalter).
cpp
#include <U8g2lib.h>
#include <Wire.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
// === Display (I2C) ===
U8G2_SSD1306_128X64_NONAME_F_SW_I2C u8g2(U8G2_R0, /* clock=*/ 14, /* data=*/ 12, /* reset=*/ U8X8_PIN_NONE);
// === Webserver ===
ESP8266WebServer server(80);
// === EEPROM Adressen ===
#define EEPROM_SIZE 128
#define ADDR_SSID1 0 // 32 Bytes
#define ADDR_SSID2 32 // 32 Bytes
#define ADDR_SCAN_INT 64 // 4 Bytes
#define ADDR_THRESHOLD 68 // 4 Bytes
// === Standardwerte ===
String network1 = "FRITZI";
String network2 = "SMU";
int scanInterval = 10; // Sekunden
int alarmThreshold = -80; // dBm
// === Statusvariablen ===
int rssi1 = -100;
int rssi2 = -100;
bool found1 = false;
bool found2 = false;
unsigned long lastScan = 0;
bool alarmActive = false;
String lastAlarmTime = "";
// === Signal‑Balken (10 Stufen) ===
const char* signalBars[11] = {
"░░░░░░░░░░",
"▓░░░░░░░░░",
"▓▓░░░░░░░░",
"▓▓▓░░░░░░░",
"▓▓▓▓░░░░░░",
"▓▓▓▓▓░░░░░",
"▓▓▓▓▓▓░░░░",
"▓▓▓▓▓▓▓░░░",
"▓▓▓▓▓▓▓▓░░",
"▓▓▓▓▓▓▓▓▓░",
"▓▓▓▓▓▓▓▓▓▓"
};
// === Hilfsfunktionen ===
String getSignalQuality(int rssi) {
if (rssi >= -50) return "AUSGEZEICHNET";
if (rssi >= -60) return "SEHR GUT";
if (rssi >= -70) return "GUT";
if (rssi >= -80) return "MITTEL";
if (rssi >= -90) return "SCHWACH";
return "KEIN EMPFANG";
}
int getSignalPercent(int rssi) {
if (rssi >= -30) return 100;
if (rssi <= -100) return 0;
return map(rssi, -100, -30, 0, 100);
}
int getSignalBars(int rssi) {
return map(getSignalPercent(rssi), 0, 100, 0, 10);
}
String getTimeString() {
unsigned long seconds = millis() / 1000;
unsigned long minutes = seconds / 60;
unsigned long hours = minutes / 60;
char buffer[20];
snprintf(buffer, sizeof(buffer), "%02lu:%02lu:%02lu",
hours % 24, minutes % 60, seconds % 60);
return String(buffer);
}
// === EEPROM laden / speichern ===
void loadSettings() {
EEPROM.begin(EEPROM_SIZE);
network1 = "";
for (int i = 0; i < 32; i++) {
char c = EEPROM.read(ADDR_SSID1 + i);
if (c == 0) break;
network1 += c;
}
if (network1.length() == 0) network1 = "FRITZI";
network2 = "";
for (int i = 0; i < 32; i++) {
char c = EEPROM.read(ADDR_SSID2 + i);
if (c == 0) break;
network2 += c;
}
if (network2.length() == 0) network2 = "SMU";
EEPROM.get(ADDR_SCAN_INT, scanInterval);
if (scanInterval < 5 || scanInterval > 300) scanInterval = 10;
EEPROM.get(ADDR_THRESHOLD, alarmThreshold);
if (alarmThreshold > -30 || alarmThreshold < -100) alarmThreshold = -80;
EEPROM.end();
Serial.println("Einstellungen geladen:");
Serial.println(" Netzwerk 1: " + network1);
Serial.println(" Netzwerk 2: " + network2);
Serial.println(" Scan Intervall: " + String(scanInterval) + "s");
Serial.println(" Alarm Schwelle: " + String(alarmThreshold) + "dBm");
}
void saveSettings() {
EEPROM.begin(EEPROM_SIZE);
for (int i = 0; i < 32; i++) {
EEPROM.write(ADDR_SSID1 + i, (i < network1.length()) ? network1[i] : 0);
}
for (int i = 0; i < 32; i++) {
EEPROM.write(ADDR_SSID2 + i, (i < network2.length()) ? network2[i] : 0);
}
EEPROM.put(ADDR_SCAN_INT, scanInterval);
EEPROM.put(ADDR_THRESHOLD, alarmThreshold);
EEPROM.commit();
EEPROM.end();
Serial.println("Einstellungen gespeichert");
}
// === WLAN‑Scan ===
void scanWifi() {
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(50);
int n = WiFi.scanNetworks();
rssi1 = -100; rssi2 = -100;
found1 = false; found2 = false;
alarmActive = false;
for (int i = 0; i < n; i++) {
String ssid = WiFi.SSID(i);
int rssi = WiFi.RSSI(i);
if (ssid == network1) {
rssi1 = rssi;
found1 = true;
if (rssi <= alarmThreshold) {
alarmActive = true;
lastAlarmTime = getTimeString();
}
}
if (ssid == network2) {
rssi2 = rssi;
found2 = true;
if (rssi <= alarmThreshold) {
alarmActive = true;
lastAlarmTime = getTimeString();
}
}
}
WiFi.scanDelete();
}
// === Display aktualisieren ===
void updateDisplay() {
u8g2.clearBuffer();
// Kopfzeile
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setCursor(0, 10);
u8g2.print("WLAN SIGNAL MONITOR");
if (alarmActive) {
u8g2.setCursor(100, 10);
u8g2.print("ALARM!");
}
u8g2.drawLine(0, 12, 128, 12);
// ---- Netzwerk 1 ----
u8g2.setFont(u8g2_font_7x13_tf);
u8g2.setCursor(0, 28);
String dn1 = network1;
if (dn1.length() > 8) dn1 = dn1.substring(0, 8) + ".";
u8g2.print(dn1 + ":");
int bars1 = found1 ? getSignalBars(rssi1) : 0;
u8g2.setFont(u8g2_font_unifont_t_symbols);
u8g2.setCursor(0, 42);
u8g2.print(signalBars[bars1]);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setCursor(0, 54);
if (found1) {
u8g2.print(getSignalQuality(rssi1));
if (rssi1 <= alarmThreshold) {
u8g2.drawBox(0, 55, 128, 2); // Alarm‑Balken
}
} else {
u8g2.print("NICHT GEFUNDEN");
}
u8g2.drawLine(0, 56, 128, 56); // Trennlinie
// ---- Netzwerk 2 ----
u8g2.setFont(u8g2_font_7x13_tf);
u8g2.setCursor(0, 64);
String dn2 = network2;
if (dn2.length() > 8) dn2 = dn2.substring(0, 8) + ".";
u8g2.print(dn2 + ":");
// Für Netzwerk 2 zeigen wir nur einen kurzen Qualitätstext (Platzmangel)
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.setCursor(60, 64);
if (found2) {
u8g2.print(getSignalQuality(rssi2));
if (rssi2 <= alarmThreshold) {
u8g2.drawBox(60, 65, 68, 2);
}
} else {
u8g2.print("NICHT DA");
}
u8g2.sendBuffer();
}
// === Web‑Interface (HTML) ===
String getWebPage() {
String html = "<!DOCTYPE html><html><head>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<meta charset='UTF-8'>";
html += "<title>WLAN Monitor Einstellungen</title>";
html += "<style>";
html += "body { font-family: Arial, sans-serif; margin: 20px; background: #f0f0f0; }";
html += ".container { max-width: 600px; margin: auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }";
html += "h1 { color: #333; }";
html += "h2 { color: #555; border-bottom: 2px solid #4CAF50; padding-bottom: 5px; }";
html += "label { display: block; margin: 10px 0 5px; font-weight: bold; }";
html += "input, select { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; }";
html += "button { background: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }";
html += "button:hover { background: #45a049; }";
html += ".status { background: #e8f5e9; padding: 10px; border-radius: 5px; margin: 10px 0; }";
html += ".alarm { background: #ffebee; padding: 10px; border-radius: 5px; margin: 10px 0; }";
html += "</style>";
html += "</head><body>";
html += "<div class='container'>";
html += "<h1>📶 WLAN Monitor Einstellungen</h1>";
// Status
html += "<div class='status'>";
html += "<h2>Aktueller Status</h2>";
html += "<p><strong>" + network1 + ":</strong> ";
html += found1 ? String(rssi1) + " dBm (" + getSignalQuality(rssi1) + ")" : "Nicht gefunden";
html += "</p>";
html += "<p><strong>" + network2 + ":</strong> ";
html += found2 ? String(rssi2) + " dBm (" + getSignalQuality(rssi2) + ")" : "Nicht gefunden";
html += "</p>";
html += "<p><strong>Letzter Scan:</strong> Vor " + String((millis() - lastScan) / 1000) + " Sekunden</p>";
if (alarmActive) {
html += "<div class='alarm'>";
html += "<p><strong>⚠️ ALARM AKTIV!</strong> Signal unter " + String(alarmThreshold) + " dBm</p>";
html += "<p>Letzter Alarm: " + lastAlarmTime + "</p>";
html += "</div>";
}
html += "</div>";
// Formular
html += "<h2>Einstellungen</h2>";
html += "<form action='/save' method='POST' data-trp-original-action="/save">";
html += "<label for='ssid1'>Netzwerk 1 Name:</label>";
html += "<input type='text' id='ssid1' name='ssid1' value='" + network1 + "' maxlength='31'>";
html += "<label for='ssid2'>Netzwerk 2 Name:</label>";
html += "<input type='text' id='ssid2' name='ssid2' value='" + network2 + "' maxlength='31'>";
html += "<label for='interval'>Scan Intervall (Sekunden):</label>";
html += "<input type='number' id='interval' name='interval' value='" + String(scanInterval) + "' min='5' max='300'>";
html += "<label for='threshold'>Alarm Schwelle (dBm):</label>";
html += "<select id='threshold' name='threshold'>";
String thresholds[] = {"-70", "-75", "-80", "-85", "-90"};
for (String t : thresholds) {
html += "<option value='" + t + "'";
if (t.toint() ="=" alarmthreshold) html += " selected";
">" + t + " dBm</option>";
}
html += "</select>";
html += "<button type='submit'>Einstellungen Speichern</button>";
html += "<input type="hidden" name="trp-form-language" value="en"/></form>";
html += "<h2>Steuerung</h2>";
html += "<p><a href='/scan'><button>Sofort Scannen</button></a></p>";
html += "<p><a href='/reboot'><button>Neustart</button></a></p>";
html += "<h2>System Info</h2>";
html += "<p>IP Adresse: " + WiFi.softAPIP().toString() + "</p>";
html += "<p>ESP8266 Chip ID: " + String(ESP.getChipId()) + "</p>";
html += "<p>Free Heap: " + String(ESP.getFreeHeap()) + " Bytes</p>";
html += "</div></body></html>";
return html;
}
// === Webserver‑Handler ===
void handleRoot() { server.send(200, "text/html", getWebPage()); }
void handleSave() {
if (server.hasArg("ssid1")) network1 = server.arg("ssid1");
if (server.hasArg("ssid2")) network2 = server.arg("ssid2");
if (server.hasArg("interval")) scanInterval = server.arg("interval").toInt();
if (server.hasArg("threshold")) alarmThreshold = server.arg("threshold").toInt();
saveSettings();
String html = "<!DOCTYPE html><html><head><meta http-equiv='refresh' content='3;url=/'>";
html += "<title>Einstellungen gespeichert</title></head><body>";
html += "<h1>✅ Einstellungen gespeichert!</h1><p>Weiterleitung in 3 Sekunden...</p></body></html>";
server.send(200, "text/html", html);
}
void handleScan() {
scanWifi();
server.sendHeader("Location", "/");
server.send(302, "text/plain", "Scan gestartet");
}
void handleReboot() {
server.send(200, "text/html", "<h1>Neustart...</h1>");
delay(1000);
ESP.restart();
}
// === Setup und Loop ===
void setup() {
Serial.begin(115200);
Serial.println("\n=== WLAN Monitor mit Webinterface ===");
u8g2.begin();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_10x20_tf);
u8g2.setCursor(10, 30);
u8g2.print("WLAN");
u8g2.setCursor(10, 55);
u8g2.print("MONITOR");
u8g2.sendBuffer();
delay(2000);
loadSettings();
WiFi.mode(WIFI_AP);
String apName = "WLAN-Monitor-" + String(ESP.getChipId());
WiFi.softAP(apName.c_str());
Serial.println("Access Point gestartet: SSID = " + apName + ", IP = " + WiFi.softAPIP().toString());
server.on("/", handleRoot);
server.on("/save", handleSave);
server.on("/scan", handleScan);
server.on("/reboot", handleReboot);
server.begin();
scanWifi();
updateDisplay();
lastScan = millis();
Serial.println("Web Server bereit unter " + WiFi.softAPIP().toString());
}
void loop() {
server.handleClient();
unsigned long now = millis();
if (now - lastScan >= scanInterval * 1000UL) {
scanWifi();
updateDisplay();
lastScan = now;
Serial.print("Scan: ");
Serial.print(network1 + ": " + (found1 ? String(rssi1) + "dBm" : "nicht da"));
Serial.print(" | " + network2 + ": " + (found2 ? String(rssi2) + "dBm" : "nicht da"));
if (alarmActive) Serial.println(" ⚠️ ALARM!");
else Serial.println();
}
delay(100);
}
Anpassungsmöglichkeiten und Erweiterungen
Der Code ist bewusst modular gehalten, sodass er leicht erweitert werden kann:
- Mehr als zwei Netzwerke – durch einfaches Erweitern der Arrays und der Scan‑Logik lassen sich beliebig viele SSIDs überwachen.
- Alarm‑Ausgabe – statt nur einer Display‑Meldung kann ein GPIO‑Pin angesteuert werden, um eine LED oder einen Summer zu schalten (z. B.
digitalWrite(ALARM_PIN, HIGH)). - Datenprotokollierung – mit einer SD‑Karte oder über MQTT können die Messwerte langfristig gespeichert und analysiert werden.
- Energiesparmodus – bei batteriebetriebenem Einsatz kann der ESP zwischen den Scans in den Deep‑Sleep versetzt werden.
- Visuelles Feedback – das Display lässt sich durch Animationen oder Farbwechsel (bei farbigen Displays) noch aufwerten.
Der komplette Quellcode ist auch auf Plattformen wie GitHub verfügbar, wo die Community ständig neue Features beisteuert.
Fazit zum praktischen Teil
Dieser praxisnahe WLAN‑Monitor zeigt, wie einfach es heute ist, mit wenig Aufwand und geringen Kosten eine professionelle Signalüberwachung aufzubauen. Die Kombination aus ESP8266, OLED und Webserver eröffnet eine Fülle von Einsatzmöglichkeiten – sei es im privaten Heimnetz, im Büro oder in der industriellen Vorfertigung. Der mitgelieferte Code ist nicht nur funktionsfähig, sondern auch eine hervorragende Basis für eigene Experimente und Weiterentwicklungen. Wer die Grundlagen verstanden hat, kann das System schnell an seine individuellen Anforderungen anpassen und so zum echten WLAN‑Profi werden.
Quellen (ergänzt)
- Arduino‑Bibliothek U8g2 – Dokumentation und Beispiele: https://github.com/olikraus/u8g2
- ESP8266‑WLAN‑Bibliothek – offizielle ESP8266‑Kern‑Dokumentation: https://arduino-esp8266.readthedocs.io/
Post Comment