WLAN-Scanner & RSSI-Messgerät mit ESP32 bauen: Funklöcher finden, Kanäle prüfen, Ausleuchtung messen
Im Wohnzimmer läuft alles flüssig, im Schlafzimmer bricht der Videoanruf ab, und der Sensor im Keller verliert alle paar Stunden die Verbindung. Die übliche Reaktion: Repeater kaufen und hoffen. Die bessere: nachmessen. Mit einem ESP32, einem OLED und einem Akku baust du ein handliches Messgerät, das dir die Signalstärke live als Balken zeigt, die Kanalbelegung deiner Nachbarschaft als Diagramm darstellt und deine Wohnung Raum für Raum protokolliert. Und ganz nebenbei verstehst du danach, warum −75 dBm nicht „drei Viertel" bedeutet.
dBm verstehen – warum −70 doppelt so gut ist wie −73
RSSI steht für Received Signal Strength Indicator und wird in dBm angegeben – Dezibel bezogen auf ein Milliwatt. Die Skala ist logarithmisch, und genau daran scheitert die Intuition: Der Unterschied zwischen −40 und −50 dBm ist nicht „ein bisschen", sondern Faktor zehn.
- 3 dB weniger = halbe Leistung. Von −70 auf −73 dBm hat sich die empfangene Energie halbiert.
- 10 dB weniger = ein Zehntel der Leistung. −60 auf −70 dBm ist ein dramatischer Einbruch.
- Die Werte sind immer negativ, weil das Empfangssignal weit unter einem Milliwatt liegt. Näher an null ist besser.
| RSSI | Bewertung | Was noch geht | Typische Situation |
|---|---|---|---|
| −30 bis −45 dBm | ausgezeichnet | alles | gleicher Raum wie der Router |
| −46 bis −60 dBm | sehr gut | 4K-Streaming, große Uploads | Nachbarraum, offene Tür |
| −61 bis −70 dBm | gut | HD-Streaming, Videoanrufe | eine Wand dazwischen |
| −71 bis −80 dBm | grenzwertig | Surfen, IoT-Sensoren | zwei Wände oder eine Etage |
| −81 bis −90 dBm | schlecht | kaum stabile Verbindung | Keller, Stahlbetondecke |
| unter −90 dBm | unbrauchbar | Verbindungsabbrüche | Garage, Gartenhäuschen |
💡 Für IoT-Sensoren ist die Schmerzgrenze ein anderer Wert als für dein Notebook. Ein D1 Mini, der alle fünf Minuten 40 Byte sendet, funktioniert auch bei −85 dBm noch – er wiederholt die Übertragung eben. Aber: Jede Wiederholung kostet Sendezeit und damit Strom. Bei Batteriebetrieb halbiert ein schlechter Standort deine Laufzeit locker. Der Unterschied zwischen −65 und −85 dBm kann Monate ausmachen.
Das zweite Problem: überfüllte Kanäle
Ein starkes Signal nützt nichts, wenn zwölf Nachbarn auf demselben Kanal funken. Im 2,4-GHz-Band liegen die 13 Kanäle nur 5 MHz auseinander, ein Kanal ist aber rund 20 MHz breit – sie überlappen sich also massiv.
| Kanal | Mittenfrequenz | Überlappt mit | Empfehlung |
|---|---|---|---|
| 1 | 2412 MHz | 2, 3, 4, 5 | eine der drei guten Wahlen |
| 2–5 | 2417–2432 MHz | 1 und 6 | stört beide Nachbarkanäle |
| 6 | 2437 MHz | 2–5, 7–10 | eine der drei guten Wahlen |
| 7–10 | 2442–2457 MHz | 6 und 11 | stört beide Nachbarkanäle |
| 11 | 2462 MHz | 7–10, 12, 13 | eine der drei guten Wahlen |
| 12–13 | 2467–2472 MHz | 11 | in Europa erlaubt, manche Geräte können sie nicht |
ℹ️ Die Kanäle 1, 6 und 11 sind die einzigen drei, die sich überhaupt nicht überlappen. Deshalb ist ein Router auf Kanal 6 neben zwei Nachbarn auf Kanal 6 immer noch besser als einer auf Kanal 4, der Kanal 1 und Kanal 6 stört. Bei gleichem Kanal teilen sich die Geräte die Sendezeit höflich; bei überlappenden Kanälen hören sie einander gar nicht und funken einfach in die Übertragung des anderen hinein.
⚠️ Der ESP32 kann nur 2,4 GHz – das ist hier ein Vorteil. Genau in diesem Band leben deine IoT-Projekte, und genau dort ist es voll. Das 5-GHz-Band deines Notebooks siehst du mit diesem Messgerät nicht. Wenn du es messen willst, brauchst du ein Board mit WiFi 6 wie den ESP32-C6 – aber für die Frage „warum verliert mein Sensor die Verbindung" ist 2,4 GHz genau das richtige Band.
Das brauchst du
| Bauteil | Pin | ESP32 | Hinweis |
|---|---|---|---|
| OLED | SDA | GPIO21 | I2C-Standard |
| OLED | SCL | GPIO22 | — |
| OLED | VCC / GND | 3V3 / GND | Adresse meist 0x3C |
| Encoder | CLK / DT / SW | GPIO25 / 26 / 27 | interne Pullups nutzen |
| SD-Modul | CS / MOSI / MISO / SCK | GPIO5 / 23 / 19 / 18 | optional, für Protokolle |
| MT3608 OUT | + / − | 5V / GND | vorher exakt auf 5,0 V drehen |
⚠️ Den MT3608 vor dem Anschließen einstellen. Das Modul kommt oft mit über 20 V am Ausgang. Erst Akku dran, dann mit dem Multimeter messen und das Poti auf 5,0 V drehen – und danach den ESP32 anschließen. Details dazu im Artikel LM2596, XL4015 & MT3608 richtig einstellen.
1Der Scan im Seriellen Monitor
Bevor das Display dazukommt, lohnt sich der Blick auf die Rohdaten. WiFi.scanNetworks() liefert alles, was in Reichweite funkt – inklusive versteckter Netze, wenn man danach fragt.
#include <WiFi.h>
const char *verschluesselung(wifi_auth_mode_t m) {
switch (m) {
case WIFI_AUTH_OPEN: return "offen";
case WIFI_AUTH_WEP: return "WEP";
case WIFI_AUTH_WPA_PSK: return "WPA";
case WIFI_AUTH_WPA2_PSK: return "WPA2";
case WIFI_AUTH_WPA_WPA2_PSK: return "WPA/2";
case WIFI_AUTH_WPA3_PSK: return "WPA3";
case WIFI_AUTH_WPA2_WPA3_PSK: return "WPA2/3";
default: return "?";
}
}
// Grobe Entfernungsschaetzung ueber das Pfadverlustmodell
float entfernungMeter(int32_t rssi) {
const float rssiBei1m = -40.0f; // typisch fuer einen Heimrouter
const float n = 2.7f; // 2.0 = Freifeld, 3.0 = Innenraum
return powf(10.0f, (rssiBei1m - rssi) / (10.0f * n));
}
void setup() {
Serial.begin(115200);
delay(300);
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
}
void loop() {
Serial.println("\nScanne...");
// true = versteckte Netze mitnehmen
int n = WiFi.scanNetworks(false, true);
if (n <= 0) {
Serial.println("Keine Netze gefunden.");
} else {
Serial.printf("%d Netze gefunden\n\n", n);
Serial.println("Nr RSSI Kan Verschl. ca. m SSID");
Serial.println("--------------------------------------------------");
for (int i = 0; i < n; i++) {
String ssid = WiFi.SSID(i);
if (ssid.length() == 0) ssid = "<versteckt>";
Serial.printf("%2d %4d %3d %-8s %5.1f %s\n",
i + 1,
WiFi.RSSI(i),
WiFi.channel(i),
verschluesselung(WiFi.encryptionType(i)),
entfernungMeter(WiFi.RSSI(i)),
ssid.c_str());
}
// --- Kanalbelegung zaehlen ---
uint8_t proKanal[14] = {0};
for (int i = 0; i < n; i++) {
int k = WiFi.channel(i);
if (k >= 1 && k <= 13) proKanal[k]++;
}
Serial.println("\nKanalbelegung:");
for (int k = 1; k <= 13; k++) {
Serial.printf("%2d |", k);
for (int j = 0; j < proKanal[k]; j++) Serial.print('#');
if (k == 1 || k == 6 || k == 11) Serial.print(" <- ueberlappungsfrei");
Serial.println();
}
}
WiFi.scanDelete(); // Speicher wieder freigeben
delay(5000);
}
ℹ️ WiFi.scanDelete() nicht vergessen. Die Scan-Ergebnisse bleiben sonst im Heap liegen. Bei einem Gerät, das im Sekundentakt scannt, ist der Speicher nach einer halben Stunde voll und der ESP32 startet neu. Ein Scan über alle 13 Kanäle dauert übrigens rund 2 bis 3 Sekunden – pro Kanal wird eine feste Zeit gelauscht.
ℹ️ Die Entfernungsschätzung ist bewusst grob. Das Pfadverlustmodell setzt freie Sicht voraus. Eine Betonwand kostet 10 bis 20 dB, was den geschätzten Abstand vervielfacht. Als relativer Vergleich zwischen zwei Standorten ist der Wert trotzdem nützlich – als absolute Meterangabe nicht.
2Netzliste auf dem OLED
Jetzt wird das Gerät mobil: Die fünf stärksten Netze mit Balken, Kanal und dBm-Wert. Die Sortierung übernimmt der ESP32 selbst, denn die Scan-Ergebnisse kommen unsortiert.
#include <WiFi.h>
#include <Wire.h>
#include <U8g2lib.h>
// Fuer das 1,3 Zoll Display: U8G2_SH1106_128X64_NONAME_F_HW_I2C
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
#define MAX_ZEIGEN 5
struct Netz {
char ssid[20];
int32_t rssi;
uint8_t kanal;
};
Netz liste[MAX_ZEIGEN];
uint8_t anzahl = 0;
uint16_t gesamt = 0;
// RSSI in eine Balkenlaenge von 0 bis breite umrechnen
uint8_t balkenLaenge(int32_t rssi, uint8_t breite) {
int32_t r = rssi;
if (r > -35) r = -35;
if (r < -95) r = -95;
return (uint8_t)((r + 95) * breite / 60);
}
void scannen() {
int n = WiFi.scanNetworks(false, true);
gesamt = (n > 0) ? n : 0;
anzahl = 0;
if (n <= 0) return;
// Einfache Auswahl der staerksten MAX_ZEIGEN Netze
bool benutzt[64] = {false};
uint8_t grenze = (n < 64) ? n : 64;
for (uint8_t platz = 0; platz < MAX_ZEIGEN && platz < grenze; platz++) {
int besterIdx = -1;
int32_t bestesRssi = -200;
for (uint8_t i = 0; i < grenze; i++) {
if (benutzt[i]) continue;
if (WiFi.RSSI(i) > bestesRssi) { bestesRssi = WiFi.RSSI(i); besterIdx = i; }
}
if (besterIdx < 0) break;
benutzt[besterIdx] = true;
String s = WiFi.SSID(besterIdx);
if (s.length() == 0) s = "(versteckt)";
s.toCharArray(liste[platz].ssid, sizeof(liste[platz].ssid));
liste[platz].rssi = WiFi.RSSI(besterIdx);
liste[platz].kanal = WiFi.channel(besterIdx);
anzahl++;
}
WiFi.scanDelete();
}
void zeichnen() {
char buf[28];
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_5x8_tf);
snprintf(buf, sizeof(buf), "WLAN-SCAN %u Netze", gesamt);
u8g2.drawStr(0, 6, buf);
u8g2.drawHLine(0, 8, 128);
for (uint8_t i = 0; i < anzahl; i++) {
uint8_t y = 18 + i * 10;
// SSID gekuerzt
char kurz[13];
strncpy(kurz, liste[i].ssid, 12);
kurz[12] = '\0';
u8g2.drawStr(0, y, kurz);
// Kanal
snprintf(buf, sizeof(buf), "%2u", liste[i].kanal);
u8g2.drawStr(64, y, buf);
// Balken
uint8_t len = balkenLaenge(liste[i].rssi, 30);
u8g2.drawFrame(76, y - 6, 32, 7);
if (len > 0) u8g2.drawBox(77, y - 5, len, 5);
// dBm
snprintf(buf, sizeof(buf), "%ld", (long)liste[i].rssi);
u8g2.drawStr(110, y, buf);
}
u8g2.sendBuffer();
}
void setup() {
Wire.begin();
Wire.setClock(400000);
u8g2.begin();
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
}
void loop() {
scannen();
zeichnen();
delay(2000);
}
3Der Ausleuchtungs-Modus – ein Netz, live und mit Verlauf
Das ist der eigentliche Kern des Geräts. Du verbindest dich mit deinem eigenen Netz und läufst durch die Wohnung. Der große Zahlenwert zeigt den aktuellen RSSI, darunter läuft ein Verlaufsdiagramm – so siehst du im Gehen, wo genau das Signal einbricht.
#include <WiFi.h>
#include <Wire.h>
#include <U8g2lib.h>
#define WLAN_SSID "MeinWLAN"
#define WLAN_PASSWORT "geheim123"
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
#define VERLAUF_N 100
int8_t verlauf[VERLAUF_N];
uint8_t verlaufIdx = 0;
bool verlaufVoll = false;
int32_t rssiAktuell = 0;
int32_t rssiMin = 0, rssiMax = -120;
int32_t summe = 0;
uint32_t n = 0;
uint32_t letzteMessung = 0;
void verlaufHinzu(int32_t rssi) {
if (rssi > -1) rssi = -1;
if (rssi < -120) rssi = -120;
verlauf[verlaufIdx] = (int8_t)rssi;
verlaufIdx++;
if (verlaufIdx >= VERLAUF_N) { verlaufIdx = 0; verlaufVoll = true; }
}
void zeichneVerlauf(uint8_t x, uint8_t y, uint8_t breite, uint8_t hoehe) {
uint8_t anz = verlaufVoll ? VERLAUF_N : verlaufIdx;
if (anz < 2) return;
const int8_t OBEN = -35, UNTEN = -95;
uint8_t vorherY = 0;
for (uint8_t i = 0; i < anz && i < breite; i++) {
uint8_t idx = verlaufVoll ? (verlaufIdx + i) % VERLAUF_N : i;
int16_t w = verlauf[idx];
if (w > OBEN) w = OBEN;
if (w < UNTEN) w = UNTEN;
uint8_t py = y + hoehe - 1 -
(uint16_t)(w - UNTEN) * (hoehe - 1) / (OBEN - UNTEN);
if (i > 0) u8g2.drawLine(x + i - 1, vorherY, x + i, py);
vorherY = py;
}
// Referenzlinie bei -70 dBm (Grenze "noch gut")
uint8_t refY = y + hoehe - 1 -
(uint16_t)(-70 - UNTEN) * (hoehe - 1) / (OBEN - UNTEN);
for (uint8_t i = 0; i < breite; i += 4) u8g2.drawPixel(x + i, refY);
}
const char *bewertung(int32_t r) {
if (r >= -50) return "SEHR GUT";
if (r >= -60) return "GUT";
if (r >= -70) return "OK";
if (r >= -80) return "SCHWACH";
return "KRITISCH";
}
void setup() {
Serial.begin(115200);
Wire.begin();
Wire.setClock(400000);
u8g2.begin();
u8g2.enableUTF8Print();
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 20, "Verbinde...");
u8g2.sendBuffer();
WiFi.mode(WIFI_STA);
WiFi.begin(WLAN_SSID, WLAN_PASSWORT);
while (WiFi.status() != WL_CONNECTED) delay(200);
rssiMin = WiFi.RSSI();
rssiMax = WiFi.RSSI();
}
void loop() {
if (millis() - letzteMessung < 250) return;
letzteMessung = millis();
if (WiFi.status() != WL_CONNECTED) {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 20, "VERBINDUNG WEG");
u8g2.drawStr(0, 36, "Hier ist Schluss.");
u8g2.sendBuffer();
WiFi.reconnect();
delay(1000);
return;
}
rssiAktuell = WiFi.RSSI();
verlaufHinzu(rssiAktuell);
if (rssiAktuell < rssiMin) rssiMin = rssiAktuell;
if (rssiAktuell > rssiMax) rssiMax = rssiAktuell;
summe += rssiAktuell;
n++;
char buf[24];
u8g2.clearBuffer();
// Kopfzeile
u8g2.setFont(u8g2_font_5x8_tf);
snprintf(buf, sizeof(buf), "K%u %s", WiFi.channel(), bewertung(rssiAktuell));
u8g2.drawStr(0, 6, buf);
u8g2.drawHLine(0, 8, 128);
// Grosser Wert
u8g2.setFont(u8g2_font_logisoso24_tn);
snprintf(buf, sizeof(buf), "%ld", (long)rssiAktuell);
u8g2.drawStr(0, 34, buf);
u8g2.setFont(u8g2_font_6x10_tf);
u8g2.drawStr(60, 34, "dBm");
// Min / Mittel / Max
u8g2.setFont(u8g2_font_4x6_tf);
snprintf(buf, sizeof(buf), "max %ld", (long)rssiMax);
u8g2.drawStr(92, 18, buf);
snprintf(buf, sizeof(buf), "avg %ld", (long)(n ? summe / (int32_t)n : 0));
u8g2.drawStr(92, 26, buf);
snprintf(buf, sizeof(buf), "min %ld", (long)rssiMin);
u8g2.drawStr(92, 34, buf);
// Verlauf
u8g2.drawHLine(0, 38, 128);
zeichneVerlauf(2, 41, 100, 22);
u8g2.setFont(u8g2_font_4x6_tf);
u8g2.drawStr(105, 47, "-35");
u8g2.drawStr(105, 62, "-95");
u8g2.sendBuffer();
}
💡 Die gepunktete Linie bei −70 dBm ist das eigentliche Werkzeug. Solange die Kurve darüber bleibt, ist alles gut. Sobald sie darunter rutscht, weißt du: hier beginnt das Problem – und zwar auf den halben Meter genau, weil du beim Laufen zusiehst. Genau das kann dir keine Handy-App so unmittelbar zeigen, weil du dort auf den Bildschirm starrst statt auf ein Gerät in der Hand mit einer einzigen großen Zahl.
ℹ️ Warum int8_t für den Verlauf reicht: RSSI-Werte liegen zwischen 0 und −120 – das passt genau in ein vorzeichenbehaftetes Byte. 100 Messwerte kosten damit 100 Byte statt 400. Auf dem ESP32 ist das egal, aber die Gewohnheit zahlt sich aus, sobald derselbe Code auf einem Nano landen soll. Mehr dazu im Artikel Arduino Speicher voll? RAM sparen.
4Kanalbelegung als Balkendiagramm
Der dritte Modus beantwortet die Frage „auf welchen Kanal soll ich meinen Router stellen". Statt nur zu zählen, wie viele Netze pro Kanal funken, gewichtet dieser Code sie nach Stärke – ein schwaches Netz zwei Häuser weiter stört eben weniger als der Router nebenan.
#include <WiFi.h>
#include <Wire.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
uint16_t last[14]; // gewichtete Belastung pro Kanal
uint8_t zaehler[14]; // Anzahl Netze pro Kanal
void kanaeleMessen() {
for (uint8_t k = 0; k < 14; k++) { last[k] = 0; zaehler[k] = 0; }
int n = WiFi.scanNetworks(false, true);
if (n <= 0) { WiFi.scanDelete(); return; }
for (int i = 0; i < n; i++) {
int k = WiFi.channel(i);
if (k < 1 || k > 13) continue;
int32_t r = WiFi.RSSI(i);
if (r < -95) r = -95;
if (r > -30) r = -30;
// Gewicht 0..65: je staerker das Netz, desto hoeher
uint16_t gewicht = (uint16_t)(r + 95);
zaehler[k]++;
// Nachbarkanaele mitbelasten - 2,4 GHz ueberlappt nun mal
for (int d = -2; d <= 2; d++) {
int z = k + d;
if (z < 1 || z > 13) continue;
uint16_t anteil = gewicht >> (abs(d)); // d=0 voll, d=1 halb, d=2 viertel
last[z] += anteil;
}
}
WiFi.scanDelete();
}
uint8_t besterKanal() {
uint8_t kandidaten[3] = {1, 6, 11};
uint8_t bester = 1;
uint16_t minLast = 0xFFFF;
for (uint8_t i = 0; i < 3; i++) {
uint8_t k = kandidaten[i];
if (last[k] < minLast) { minLast = last[k]; bester = k; }
}
return bester;
}
void zeichnen() {
char buf[26];
uint16_t maxLast = 1;
for (uint8_t k = 1; k <= 13; k++) if (last[k] > maxLast) maxLast = last[k];
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_5x8_tf);
snprintf(buf, sizeof(buf), "KANAELE frei: %u", besterKanal());
u8g2.drawStr(0, 6, buf);
u8g2.drawHLine(0, 8, 128);
// 13 Balken zu je 9 Pixel Breite
for (uint8_t k = 1; k <= 13; k++) {
uint8_t x = (k - 1) * 9 + 4;
uint8_t h = (uint16_t)last[k] * 38 / maxLast;
if (h > 38) h = 38;
uint8_t y = 52 - h;
u8g2.drawBox(x, y, 7, h);
// Kanaele 1, 6, 11 markieren
if (k == 1 || k == 6 || k == 11) u8g2.drawHLine(x, 54, 7);
}
u8g2.drawHLine(0, 53, 128);
// Beschriftung
u8g2.setFont(u8g2_font_4x6_tf);
for (uint8_t k = 1; k <= 13; k += 2) {
snprintf(buf, sizeof(buf), "%u", k);
u8g2.drawStr((k - 1) * 9 + 5, 62, buf);
}
u8g2.sendBuffer();
}
void setup() {
Wire.begin();
Wire.setClock(400000);
u8g2.begin();
WiFi.mode(WIFI_STA);
WiFi.disconnect();
delay(100);
}
void loop() {
kanaeleMessen();
zeichnen();
delay(4000);
}
ℹ️ Die Verschiebung gewicht >> abs(d) ist die Kanalüberlappung in einer Zeile. Ein Netz auf Kanal 6 belastet Kanal 6 voll, die Kanäle 5 und 7 zur Hälfte, die Kanäle 4 und 8 zu einem Viertel. Das ist eine grobe Näherung des tatsächlichen Spektrums, reicht aber völlig, um den unter den drei überlappungsfreien Kanälen am wenigsten belasteten zu finden.
5Raum für Raum protokollieren
Die letzte Ausbaustufe: Du drückst in jedem Raum den Encoder-Taster, das Gerät mittelt 20 Messungen und schreibt sie mit Raumnummer auf die SD-Karte. Am Ende hast du eine CSV, aus der eine echte Ausleuchtungskarte wird.
#include <WiFi.h>
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <U8g2lib.h>
#define WLAN_SSID "MeinWLAN"
#define WLAN_PASSWORT "geheim123"
#define TASTER_PIN 27
#define SD_CS 5
#define MESSUNGEN 20
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
uint16_t punktNr = 0;
bool sdBereit = false;
void zeige(const char *z1, const char *z2, const char *z3) {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x12_tf);
u8g2.drawStr(0, 14, z1);
u8g2.drawStr(0, 32, z2);
u8g2.drawStr(0, 50, z3);
u8g2.sendBuffer();
}
void messpunktAufnehmen() {
int32_t summe = 0, min = 0, max = -120;
uint8_t gueltig = 0;
char buf[26];
for (uint8_t i = 0; i < MESSUNGEN; i++) {
if (WiFi.status() != WL_CONNECTED) { delay(200); continue; }
int32_t r = WiFi.RSSI();
if (r == 0 || r < -120) { delay(150); continue; }
if (gueltig == 0) { min = r; max = r; }
if (r < min) min = r;
if (r > max) max = r;
summe += r;
gueltig++;
snprintf(buf, sizeof(buf), "Messe %u/%u", i + 1, MESSUNGEN);
zeige("MESSPUNKT", buf, "bitte still halten");
delay(150);
}
if (gueltig == 0) { zeige("FEHLER", "keine Messung", ""); delay(2000); return; }
int32_t mittel = summe / gueltig;
punktNr++;
char z2[26], z3[26];
snprintf(z2, sizeof(z2), "Punkt %u: %ld dBm", punktNr, (long)mittel);
snprintf(z3, sizeof(z3), "min %ld max %ld", (long)min, (long)max);
zeige("GESPEICHERT", z2, z3);
if (sdBereit) {
File f = SD.open("/wlan_messung.csv", FILE_APPEND);
if (f) {
f.printf("%u;%ld;%ld;%ld;%u;%lu\n",
punktNr, (long)mittel, (long)min, (long)max,
WiFi.channel(), millis() / 1000UL);
f.close();
}
}
delay(2500);
}
void setup() {
Serial.begin(115200);
pinMode(TASTER_PIN, INPUT_PULLUP);
Wire.begin();
Wire.setClock(400000);
u8g2.begin();
zeige("START", "SD-Karte...", "");
sdBereit = SD.begin(SD_CS);
if (sdBereit && !SD.exists("/wlan_messung.csv")) {
File f = SD.open("/wlan_messung.csv", FILE_WRITE);
if (f) { f.println("Punkt;Mittel;Min;Max;Kanal;Sekunden"); f.close(); }
}
zeige("START", "WLAN...", "");
WiFi.mode(WIFI_STA);
WiFi.begin(WLAN_SSID, WLAN_PASSWORT);
while (WiFi.status() != WL_CONNECTED) delay(200);
}
void loop() {
char z2[26];
int32_t r = (WiFi.status() == WL_CONNECTED) ? WiFi.RSSI() : 0;
snprintf(z2, sizeof(z2), "aktuell %ld dBm", (long)r);
zeige("BEREIT", z2, sdBereit ? "Taster = messen" : "ohne SD-Karte");
if (digitalRead(TASTER_PIN) == LOW) {
delay(30); // entprellen
if (digitalRead(TASTER_PIN) == LOW) {
while (digitalRead(TASTER_PIN) == LOW) delay(10);
messpunktAufnehmen();
}
}
delay(200);
}
💡 Warum 20 Messungen statt einer: RSSI schwankt auch bei völlig stillstehendem Gerät um 3 bis 6 dB – Mehrwegempfang, bewegte Personen, Mikrowellen und Bluetooth-Geräte reden alle mit. Ein Einzelwert ist damit fast wertlos. Der Mittelwert aus 20 Messungen über drei Sekunden ist reproduzierbar, und die Spanne zwischen Min und Max verrät dir zusätzlich, wie unruhig der Standort ist. Eine große Spanne bei gutem Mittelwert ist oft schlechter als ein etwas schwächeres, aber stabiles Signal.
Troubleshooting
⚠️ WiFi.RSSI() gibt 0 oder 31 zurück: Du bist nicht verbunden. Ohne aktive Verbindung liefert die Funktion keinen sinnvollen Wert – prüfe vorher mit WiFi.status() == WL_CONNECTED. Beim Scannen dagegen brauchst du die indizierte Variante WiFi.RSSI(i).
⚠️ Der Scan findet nichts: Der ESP32 muss vorher in den Station-Modus und darf nicht verbunden sein: WiFi.mode(WIFI_STA); WiFi.disconnect(); delay(100);. Im Access-Point-Modus scannt er nicht.
⚠️ Nach einigen Minuten startet das Gerät neu: Fehlendes WiFi.scanDelete(). Jedes Scan-Ergebnis belegt Heap, der sonst nie freigegeben wird.
⚠️ Die Werte sind viel schlechter als auf dem Handy: Das ist normal. Die kleine Leiterbahnantenne eines Dev-Boards ist einem Smartphone deutlich unterlegen – Unterschiede von 5 bis 10 dB sind üblich. Für relative Messungen ist das egal, und genau darum geht es hier: Du willst wissen, wo es schlechter wird, nicht den absoluten Pegel.
⚠️ Die Anzeige springt stark, obwohl du still stehst: Auch normal, siehe oben. Mittel über 10 bis 20 Werte, dann wird es ruhig. Ein einfacher gleitender Mittelwert reicht völlig, siehe Sensorwerte glätten mit Arduino.
⚠️ Das Display flackert bei jedem Scan: Während des Scans steht das Programm 2 bis 3 Sekunden. Nutz stattdessen WiFi.scanNetworks(true) für einen asynchronen Scan und frag mit WiFi.scanComplete() ab, ob er fertig ist – dann bleibt die Anzeige flüssig.
⚠️ Am Akku hält das Gerät nur kurz: Dauerscannen und Display kosten zusammen leicht 150 mA. Ein 18650 mit 2500 mAh reicht damit rund 15 Stunden – fürs Ausmessen mehr als genug, für Dauerbetrieb nicht. Details zum Aufbau im Artikel TP4056 & MT3608: akkubetriebene ESP32-Projekte.
Praxistipps für die Messung
- Immer auf gleicher Höhe messen. Hüfthöhe ist ein guter Standard. Der Unterschied zwischen Boden und Kopfhöhe beträgt schnell 6 dB – wer mal so und mal so misst, vergleicht Äpfel mit Birnen.
- Den eigenen Körper beachten. Du bestehst größtenteils aus Wasser und dämpfst 2,4 GHz spürbar. Halte das Gerät vom Körper weg und dreh dich beim Messen einmal langsam – der beste Wert zählt.
- Skizze statt Gedächtnis. Zeichne einen groben Grundriss und trag die Punktnummern aus der CSV ein. Nach zwölf Messpunkten weiß sonst niemand mehr, welcher Punkt 7 war.
- Metall ist schlimmer als Beton. Ein Kühlschrank, ein Heizkörper oder eine Fußbodenheizung wirken als Spiegel. Oft löst ein halber Meter Versatz beim Sensor ein Problem, das kein Repeater löst.
- Erst Antenne ausrichten, dann Repeater kaufen. Router-Antennen strahlen seitlich am stärksten, nach oben und unten kaum. Zwei Antennen – eine senkrecht, eine waagerecht – decken Etagen deutlich besser ab.
- Externe Antenne beim ESP32 prüfen. Boards wie das ESP32-CAM-Set haben eine umschaltbare Antenne. Ein falsch gesetzter Lötjumper kostet dort 15 dB – das ist der Unterschied zwischen „läuft" und „läuft nicht".
- Zweimal messen, morgens und abends. Nachbar-WLANs, Bluetooth-Kopfhörer und die Mikrowelle sind abends aktiver. Ein Kanal, der mittags frei aussieht, kann um 20 Uhr voll sein.
- Vor dem Sensoreinbau messen, nicht danach. Fünf Minuten mit dem Messgerät an der geplanten Stelle ersparen dir später das Ausbauen eines eingeklebten D1 Mini.
🚀 Bauteile für dein WLAN-Messgerät
- ESP32 DEV KIT WROOM32 – Scanner, Rechner und Anzeige-Treiber in einem
- 1,3 Zoll OLED 128x64 – im Gehen gut ablesbar
- 0,96 Zoll OLED SSD1306 – kompakter, gleiche Auflösung
- Rotary Encoder KY-040 – Modus wechseln und Messpunkt auslösen
- TP4056 Lademodul – Akkubetrieb mit Schutzschaltung
- MT3608 Step-Up – vom Akku auf stabile 5 V
- Micro-SD-Kartenmodul – Messreihen als CSV mitschreiben
- ESP32 Acryl-Gehäuse – aus dem Prototyp wird ein Werkzeug
- ESP32-C6 Dev Kit – wenn du später auch WiFi 6 messen willst
- Breadboard + Dupont-Kabel Set – der Aufbau für den Testlauf
Fazit
Ein WLAN-Problem löst man nicht mit Hardware, sondern mit Daten. Mit rund fünfzehn Euro Bauteilen und einer Stunde Arbeit hast du ein Messgerät, das dir drei Dinge beantwortet, die man sonst nur rät: Wo genau bricht das Signal ein? Welcher Kanal ist wirklich frei? Und ist dieser Standort für meinen Sensor gut genug?
Die wichtigsten Erkenntnisse für die Praxis: Die Skala ist logarithmisch – 10 dB weniger bedeutet ein Zehntel der Leistung. Nur die Kanäle 1, 6 und 11 überlappen sich nicht. Und ein einzelner RSSI-Wert sagt fast nichts; erst der Mittelwert aus zwanzig Messungen ist eine Messung. Wenn du das Gerät einmal gebaut hast, wirst du es öfter in die Hand nehmen, als du denkst – spätestens beim nächsten Sensor an einer schwierigen Stelle.