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

ESP32 DEV KIT WROOM32 – das Herz des Messgeräts, WLAN und Rechenleistung in einem
1,3 Zoll OLED 128x64 – groß genug, um im Gehen abzulesen
0,96 Zoll OLED SSD1306 – die kompaktere Alternative
Rotary Encoder KY-040 – zwischen den Messmodi umschalten
TP4056 Lademodul – macht das Gerät mobil
MT3608 Step-Up – bringt die Akkuspannung auf stabile 5 V
Micro-SD-Kartenmodul – Messreihen für die spätere Auswertung mitschreiben
ESP32 Acryl-Gehäuse – damit man das Ding gefahrlos herumtragen kann
Breadboard + Dupont-Kabel Set – für den Aufbau vor dem Löten
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

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.

Zurück zum Blog