WiFi Analyzer with ESP8266 and ILI9341 LCD

 Posted by:   Posted on:   Updated on:  2020-05-01T16:21:03Z

Make your own fast and reliable WiFi analyzer with ESP8266 and ILI9341 LCD. Scan and find all nearby acces points in the 2.4 GHz band.

This WiFi analyzer can help you identify all wireless access points (AP) in your area, providing you with detailed information about each of them. You can identify potentially unused channels and find the best place to install your router. You can use any smartphone for this task since there are a lot of apps that will scan for WiFi networks. However, I did this with NodeMcu, an ESP8266 development board.

ESP8266 has some advantages over my Android phone: it scans faster and it finds more access points. The phone comes with the advantage of 5 GHz band support, yet for the simple task of scanning WiFi, the analyzer app needs permission to access location of the device. Building an analyzer with ESP8266 requires a way of showing the information. I used a 2.8” color LCD display, with 240x320 pixels, based on ILI9341.

WiFi Analyzer with ESP8266 and ILI9341 LCD

Besides ESP8266 and LCD, you will also need two pushbuttons and some connection wires if you build this thing on the breadboard. The schematic of the device is the following.

ESP8266 WiFi analyzer schematic

ESP8266 WiFi analyzer schematic

I wrote the firmware in Arduino IDE. The LCD is driven over SPI with TFT_eSPI library which is optimized for ESP8266. When powering up, a full scan is performed and you will see a list of access points on the screen. Only nine at a time. Unless the AP is hidden, SSID is displayed along with BSSID (MAC address). On the next line there is a signal indicator and RSSI value in dBm followed by channel and AP security. On the right of the first list item there is a selection marker which can be moved to the next AP by pressing the SELECT button (assigned to pin D1 of NodeMcu). Keep pressing this button to see all found networks. When you reach the last list element, the next APs will be shown. Each push of the SELECT button delays the next AP scan by 5 seconds. Each new scan resets the list and puts the selection marker on the first item. By the way, access points are sorted by descending RSSI, therefore the nearest will appear on top. Scanning is performed using WiFi.scanNetworksAsync() function.

Single AP scan mode

Single AP scan mode

If you push the DISPLAY button (assigned to pin D2 of NodeMcu), you will see the single network scan screen. This mode is useful for monitoring only the selected network (BSSID and channel are used to identify the network you selected in list). Besides the big signal indicator, RSSI is also displayed along with an approximation of distance. The presumed distance is calculated in line-of-sight conditions.

For this mode of operation, I needed the scan to be as fast as possible. Therefore, scanning all channels was not an option. Because in the list mode, hidden APs are displayed (without SSID), I couldn't scan by SSID. I had to write my own function to scan a selected channel for a specific BSSID. This is the code that triggers the scan:

    if (singleNetworkDisplay) {
      struct scan_config config;
      memset(&config, 0, sizeof(config));

      config.bssid = &selectedBSSID[0];
      if (scanKnownChannelOnly) config.channel = selectedChannel;
      config.show_hidden = 1;
      config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
      config.scan_time.active.min = 250;
      config.scan_time.active.max = 1000;

      if (wifi_station_scan(&config, single_scan_done)) {
        customScanDone = false;
        yield();
      }
    }

The structure scan_config holds scan parameters. The scan time is very fast and sometimes misses the AP although it is in range. Therefore, I had to specify a minimum scan time of 250 ms to get reliable results. The callback function single_scan_done prints AP details on LCD when scan is done.

static void ICACHE_FLASH_ATTR single_scan_done(void *arg, STATUS status) {
  customScanDone = true;
  tft.fillRect(224, 304, 16, 16, TFT_LIGHTGREY);
  scanTime = current; // readjust scan time

  if  (status == OK) {
    struct bss_info *bss_link = (struct bss_info*)arg;

    if (bss_link != NULL) {
      String msg;

      char ssid_copy[33]; // Ensure space for maximum len SSID (32) plus trailing 0
      memcpy(ssid_copy, bss_link->ssid, sizeof(bss_link->ssid));
      ssid_copy[32] = 0; // Potentially add 0-termination if none present earlier

      bigSignalIndicator(bss_link->rssi);

      tft.setTextDatum(BC_DATUM);

      tft.setTextPadding(240);
      if (bss_link->is_hidden == 0) {
        tft.setTextColor(TFT_CYAN, TFT_BLACK);
        tft.drawString(ssid_copy, 120, 172, 4);
      }
      else {
        tft.setTextColor(TFT_DARKGREY, TFT_BLACK);
        tft.drawString("Hidden", 120, 172, 4);
      }

      tft.setTextDatum(L_BASELINE);
      int frq = bss_link->channel * 5 + 2407;
      if (frq == 2477) frq = 2484;
      msg = "Channel " + String(bss_link->channel) + " (" + String(frq) + "MHz)";
      tft.setTextColor(TFT_WHITE, TFT_BLACK);
      tft.drawString(msg, 4, 200, TEXT_FONT);
      tft.setTextColor(TFT_GREEN, TFT_BLACK);
      msg = "802.11";
      if (bss_link->phy_11b) msg += 'b';
      if (bss_link->phy_11g) msg += 'g';
      if (bss_link->phy_11n) msg += 'n';
      tft.drawString(msg, 136, 200, TEXT_FONT);

      if (bss_link->wps) {
        tft.fillRoundRect(216, 198, 21, 11, 3, TFT_LIGHTGREY);
        tft.setTextPadding(0);
        tft.setTextColor(TFT_BLACK, TFT_LIGHTGREY);
        tft.drawString("WPS", 218, 200, TEXT_FONT);
      }
      else
        tft.fillRoundRect(220, 184, 20, 16, 3, TFT_BLACK);

      tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
      tft.drawString("Security:", 4, 216, TEXT_FONT);

      switch (bss_link->authmode) {
        case AUTH_OPEN: msg = "None"; break;
        case AUTH_WEP: msg = "WEP"; break;
        case AUTH_WPA_PSK: msg = "WPA-PSK"; break;
        case AUTH_WPA2_PSK: msg = "WPA2-PSK"; break;
        case AUTH_WPA_WPA2_PSK: msg = "WPA/WPA2-PSK"; break;
        case AUTH_MAX: msg = "MAX"; break;
      }

      if (bss_link->authmode != 0) {
        switch (bss_link->pairwise_cipher) {
          case CIPHER_WEP40: msg += " (WEP40)"; break;
          case CIPHER_WEP104: msg += " (WEP104)"; break;
          case CIPHER_TKIP: msg += " (TKIP)"; break;
          case CIPHER_CCMP: msg += " (CCMP)"; break;
          case CIPHER_TKIP_CCMP: msg += " (TKIP/CCMP)"; break;
          case CIPHER_UNKNOWN: msg += " (Unknown)"; break;
        }
      }

      tft.setTextPadding(180);
      tft.setTextColor(TFT_WHITE, TFT_BLACK);
      tft.drawString(msg, 60, 216, TEXT_FONT);

      msg = "Freq. offset=" + String(bss_link->freq_offset) + " (Calibration=" + String(bss_link->freqcal_val) + ")";
      tft.setTextPadding(240);
      tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
      tft.drawString(msg, 4, 264, TEXT_FONT);
    }
    else {
      tft.setTextDatum(BC_DATUM);
      tft.setTextPadding(240);
      tft.setTextColor(TFT_MAROON, TFT_BLACK);
      tft.drawString("Out of range", 120, 172, 4);
      tft.setTextDatum(L_BASELINE);

      bigSignalIndicator(-100);
    }
  }
}

Refresh interval is 2 seconds in this mode, compared to 5 seconds in list mode (plus scan duration). On this screen there is also information you couldn't get with Arduino API for ESP8266: the 802.11 standard (b/g/n) and WPS status.

Distance calculation is performed using a formula provided by Mario Kutlev as answer to a question on Stack Overflow. Its accuracy is disputed, yet the results seem acceptable.

void approxDistanceToAP(int rssi) {
  // See: https://stackoverflow.com/a/18359639
  int f = selectedChannel * 5 + 2407;
  if (selectedChannel == 14) f = 2484;
  rssi = abs(rssi);
  double ex = (27.55 - (20 * log10(f)) + rssi) / 20;
  float dist = pow(10, ex);
  String sdist = String(dist, 1);
  if (rssi == 100) sdist = "--";

  tft.setTextDatum(R_BASELINE);
  tft.setTextPadding(64);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.drawString(sdist, 226, 136, 4);
  tft.setTextDatum(R_BASELINE);
  tft.setTextPadding(0);
  tft.setTextColor(TFT_LIGHTGREY, TFT_BLACK);
  tft.drawString("m", 236, 129, TEXT_FONT);
}

There is another extra feature in the single AP scan mode. Depending on BSSID, the AP manufacturer is determined. This works because the sketch includes a file stored in SPIFFS which holds this data. It's just a text file with entries on single line: the first three numbers of MAC in HEX format followed by a single character, then the manufacturer of the device (in my example there's a TAB character after each MAC).

C8:3A:35 Tenda Technology Co., Ltd.
04:8D:38 Netcore Technology Inc.
E4:BE:ED Netcore Technology Inc.
10:FE:ED TP-Link Technologies Co.,Ltd.
90:F6:52 TP-Link Technologies Co.,Ltd.
6C:19:8F D-Link International
00:15:6D Ubiquiti Networks Inc.
64:6D:6C Huawei Technologies Co.,Ltd
80:8C:97 Kaonmedia CO., LTD.

If you're wondering where I got this from, there is a (similar) text file which holds over 30000 records available with Wireshark [GitLab, TXT]. You can flash all those records to the SPIFFS (there is space on a 4 MB flash), yet it takes up to one minute to read all of them and find the manufacturer of a MAC. So, make your own file with only a few manufacturers.

Switch back to list mode by pushing SELECT button. Last scan results are preserved and you can either select another AP or wait 5 seconds for a new full scan.

Resources

  • Full source code (~500 lines) on GitHub.
  • TFT_eSPI library must be installed in Arduino IDE (also available through Library Manager). Does not require any configuration unless you change wiring.
  • You have to upload the manufacturer list file on SPIFFS. Here is what you have to do.
  • ILI9341 compatible LCDs - any 320x240 with SPI interface will work; does not need touch controller.

2 comments :

  1. i neeeeeeeds more info on setting up the device list please

    ReplyDelete
  2. Congratulations on the project.. Although only I could be this cool in programming.

    ReplyDelete

Please read the comments policy before publishing your comment.