ESP Based Wi-Fi Network Indicator
This post will cover the use of a SSD1306 OLED display with Espressif based modules. The setup will display wireless statistics using inexpensive devices that are simple to connect.
This work was based on the following video demonstration.
The wiring for the ESP8266 Wemos module used the following I2C pins to connect to the display. The I2C connection uses fewer pins and frees up pins for other tasks.
The ESP32 C3 module connection uses different pins, so a cross platform stacking module is not possible.
There were two sizes of the SSD1306 developed in this post for the sake of demonstrating their differences. I identified them as “micro” for the 12mm (64 x 32 pixel) and “mini” for the 24 mm (128 x 64 pixel) display. Both displays are monochromatic with each pixel either on or off. The larger “mini” display has a color option for a portion of the display to be yellow or blue.
The “mini” Wi-Fi indicator starts automatically and displays a logo when powered on. This is followed by some information about the firmware. The firmware includes MQTT functionality, so the module will publish its readings to a defined broker, which can be useful if watching the display isn’t practical.
Once up and running, the module will cycle through three separate screens. The first provides IP and Mac Address details about the device and its connected network. The second provides operational details about the module. The last screen provides details about the Wi-Fi network signal. Each screen also includes a lower portion that contains details about AP count, Signal Noise Ratio, and Radio Usage.
The “micro” indicator is slimmed down and provides fewer details.
Here is the Arduino Code for each module type.
/************** Start of code Board = ESP8266 Boards (3.1.2) > LOLIN(WEMOS) D1 Mini (Clone), 4MB (FS:none OTA:~1019KB) ***************/ #include <Arduino.h> #include <U8g2lib.h> #include <Wire.h> #include <ESP8266WiFi.h> #include <PubSubClient.h> #include <math.h> #include <vector> extern "C" { #include "user_interface.h" } struct my_wifi_promiscuous_pkt { struct { int8_t rssi; // Received signal strength uint8_t rate; // Data rate code uint16_t sig_len; // Signal length (number of bytes) } rx_ctrl; }; // Initialize display with I2C (using default SDA/SCL pins for ESP8266) U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); WiFiClient espClient; PubSubClient mqttClient(espClient); const uint32_t scanDelay = 15000; // Delay between scans (ms) const uint32_t scanPeriod = scanDelay / 1000; // Scan cycle (sec) int scanCount = 0; float noiseFloor_dBm; int snr; String apCounter; // WiFi credentials struct WiFiCredentials { const char* myssid; const char* mypassword; }; WiFiCredentials wifi_networks[] = { {"ssid1", "open"}, {"ssid2", "password2"}, {"ssid3", "open"}, {"ssid4", "password4"} }; const char* mqtt_server = "mqtt_broker_ip"; int mqtt_status = 0; int mqtt_misCount = 0; int wifi_status = 0; int wifi_misCount = 0; // Global variable to accumulate airtime (in microseconds) volatile unsigned long totalAirtimeMicroseconds = 0; // Structure to store AP data from a scan struct APData { String ssid; int rssi; uint8_t encryption; int channel; String bssid; }; // Function Prototypes String encryptionTypeStr(uint8_t authmode); void mqttCallback(char* topic, byte* message, unsigned int length); bool tryReconnectMQTT(unsigned long timeout); void safePublish(const char* topic, const String & payload); void updateStats(const std::vector<APData>& apList); void wifiConnect(); void checkForNetworks(); void signalNoiseRatio(const std::vector<APData>& apList); void channelAirtimeUtilization(); float getChannelAirtimeUtilization(unsigned long measurementPeriodMs); // Returns a human-readable string for the WiFi encryption type. String encryptionTypeStr(uint8_t authmode) { switch (authmode) { case ENC_TYPE_NONE: return "Open"; case ENC_TYPE_WEP: return "WEP"; case ENC_TYPE_TKIP: return "WPA+PSK"; case ENC_TYPE_CCMP: return "WPA2+PSK"; case ENC_TYPE_AUTO: return "WPA+WPA2+PSK"; default: return "Unknown"; } } String WiFiIPAddress; String WiFiSubnet; String WiFiGWAddress; float utilizationPercent; // Connect to the WiFi network and configure the MQTT client. void wifiConnect() { WiFi.mode(WIFI_STA); Serial.println(" "); Serial.println("Connecting to WiFi"); int n = WiFi.scanNetworks(); if (n == 0) { wifi_status = 0; return; } for (int i = 0; i < sizeof(wifi_networks) / sizeof(wifi_networks[0]); i++) { for (int j = 0; j < n; j++) { if (strcmp(wifi_networks[i].myssid, WiFi.SSID(j).c_str()) == 0) { WiFi.begin(wifi_networks[i].myssid, wifi_networks[i].mypassword); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 25) { delay(200); attempts++; } if (WiFi.status() == WL_CONNECTED) { wifi_status = 1; } } } } mqttClient.setServer(mqtt_server, 1883); mqttClient.setCallback(mqttCallback); } // Callback for incoming MQTT messages. void mqttCallback(char* topic, byte* message, unsigned int length) { String messageTemp; for (unsigned int i = 0; i < length; i++) { messageTemp += (char)message[i]; } // Process incoming message if needed. } // Try to reconnect to the MQTT server for the specified timeout (in ms). // Returns true if connection was re-established, false otherwise. bool tryReconnectMQTT(unsigned long timeout) { unsigned long startTime = millis(); while (!mqttClient.connected() && (millis() - startTime < timeout)) { if (mqttClient.connect("Wemos-WifiScanner")) { mqttClient.subscribe("Wemos-WifiScanner/#"); return true; } delay(500); } return mqttClient.connected(); } // Publish to MQTT if connected. If not, print the message to Serial. void safePublish(const char* topic, const String & payload) { if (mqttClient.connected()) { mqtt_status = 1; mqttClient.publish(topic, payload.c_str()); } else { mqtt_status = 0; } } // Calculate the signal-to-noise ratio (SNR) using cached scan results. void signalNoiseRatio(const std::vector<APData>& apList) { if (WiFi.status() == WL_CONNECTED) { int rssiConnected = WiFi.RSSI(); int connectedChannel = WiFi.channel(); String connectedBSSID = WiFi.BSSIDstr(); float totalNoise_mW = 0.0; std::vector<String> uniqueBSSIDs; for (size_t i = 0; i < apList.size(); i++) { if (apList[i].bssid == connectedBSSID) continue; int diff = abs(apList[i].channel - connectedChannel); float weight = 0.0; if (diff == 0) { weight = 1.0; } else if (diff == 1) { weight = 0.7; } else if (diff == 2) { weight = 0.3; } else { continue; } bool duplicate = false; for (size_t j = 0; j < uniqueBSSIDs.size(); j++) { if (uniqueBSSIDs[j] == apList[i].bssid) { duplicate = true; break; } } if (!duplicate) { uniqueBSSIDs.push_back(apList[i].bssid); float mW = pow(10, apList[i].rssi / 10.0); totalNoise_mW += weight * mW; } } noiseFloor_dBm = (uniqueBSSIDs.size() > 0) ? (10 * log10(totalNoise_mW)) : -95; snr = rssiConnected - noiseFloor_dBm; } safePublish("Wemos-WifiScanner/Header", "=============================================="); Serial.println("=============================================="); safePublish("Wemos-WifiScanner/Header", "------------------( IP Info )-----------------"); Serial.println("------------------( IP Info )-----------------"); safePublish("Wemos-WifiScanner/HWAddress", WiFi.macAddress()); Serial.print("HWAddress: "); Serial.println(WiFi.macAddress()); WiFiIPAddress = WiFi.localIP().toString(); safePublish("Wemos-WifiScanner/IPAddress", WiFiIPAddress); Serial.print("IPAddress: "); Serial.println(WiFiIPAddress); WiFiSubnet = WiFi.subnetMask().toString(); safePublish("Wemos-WifiScanner/Subnet", WiFiSubnet); Serial.print("Subnet: "); Serial.println(WiFiSubnet); WiFiGWAddress = WiFi.gatewayIP().toString(); safePublish("Wemos-WifiScanner/GWAddress", WiFiGWAddress); Serial.print("GWAddress: "); Serial.println(WiFiGWAddress); safePublish("Wemos-WifiScanner/DNS", WiFi.dnsIP().toString()); Serial.print("DNS: "); Serial.println(WiFi.dnsIP().toString()); safePublish("Wemos-WifiScanner/MQTT", String(mqtt_server)); Serial.print("MQTT: "); Serial.println(mqtt_server); safePublish("Wemos-WifiScanner/Header", "-----------------( WiFi Info )----------------"); Serial.println("-----------------( WiFi Info )----------------"); safePublish("Wemos-WifiScanner/WifiNetwork", WiFi.SSID()); Serial.print("WifiNetwork: "); Serial.println(WiFi.SSID()); safePublish("Wemos-WifiScanner/WifiAP", WiFi.BSSIDstr()); Serial.print("WifiAP: "); Serial.println(WiFi.BSSIDstr()); safePublish("Wemos-WifiScanner/WifiChannel", String(WiFi.channel())); Serial.print("WifiChannel: "); Serial.println(WiFi.channel()); safePublish("Wemos-WifiScanner/WifiSignal", String(WiFi.RSSI())); Serial.print("WifiSignal: "); Serial.println(WiFi.RSSI()); safePublish("Wemos-WifiScanner/WifiNoiseFloor", String(noiseFloor_dBm)); Serial.print("WifiNoiseFloor: "); Serial.println(noiseFloor_dBm); safePublish("Wemos-WifiScanner/WifiSNR", String(snr)); Serial.print("WifiSNR: "); Serial.println(snr); safePublish("Wemos-WifiScanner/RadioUse", String(utilizationPercent, 1)); Serial.print("RadioUse: "); Serial.println(utilizationPercent, 1); safePublish("Wemos-WifiScanner/Header", "-----------------( ESP8266 Info )---------------"); Serial.println("-----------------( ESP8266 Info )---------------"); } // Publish system stats and each AP's info using the cached scan data. void updateStats(const std::vector<APData>& apList) { delay(250); if (wifi_status == 1) { Serial.print("WiFi Status: "); Serial.println("Connected"); } if (wifi_status == 0) { Serial.print("WiFi Status: "); Serial.println("Disconnected"); wifi_misCount = wifi_misCount + 1; } if (mqtt_status == 1) { Serial.print("MQTT Status: "); Serial.println("Online"); } else { Serial.print("MQTT Status: "); Serial.println("Offline"); mqtt_misCount = mqtt_misCount + 1; } safePublish("Wemos-WifiScanner/WiFiMissCount", String(wifi_misCount).c_str()); Serial.print("WiFi Miss Count: "); Serial.println(wifi_misCount); safePublish("Wemos-WifiScanner/MQTTMissCount", String(mqtt_misCount).c_str()); Serial.print("MQTT Miss Count: "); Serial.println(mqtt_misCount); scanCount = scanCount + 1; safePublish("Wemos-WifiScanner/ScanCount", String(scanCount).c_str()); Serial.print("ScanCount: "); Serial.println(scanCount); safePublish("Wemos-WifiScanner/ScanPeriod", String(scanPeriod)); Serial.print("ScanPeriod: "); Serial.println(scanPeriod); safePublish("Wemos-WifiScanner/Uptime", String(millis())); Serial.print("Uptime: "); Serial.println(millis()); safePublish("Wemos-WifiScanner/FreeHeapSize", String(ESP.getFreeHeap())); Serial.print("FreeHeapSize: "); Serial.println(ESP.getFreeHeap()); safePublish("Wemos-WifiScanner/Firmware", "Wemos-WifiScanner_ver1"); Serial.println("Firmware: Wemos-WifiScanner_ver1"); safePublish("Wemos-WifiScanner/Notes", "Version Releaase Jan 15, 2025"); Serial.println("Notes: Version Releaase Jan 15, 2025"); safePublish("Wemos-WifiScanner/Header", "----------------( AP Scan Info )--------------"); Serial.println("----------------( AP Scan Info )--------------"); safePublish("Wemos-WifiScanner/Networks-Found", String(apList.size())); Serial.print("Networks-Found: "); Serial.println(apList.size()); apCounter = String(apList.size()); for (size_t i = 0; i < apList.size(); i++) { String displaySSID = (apList[i].ssid != "") ? apList[i].ssid : "*-Hidden-*"; String encryption = encryptionTypeStr(apList[i].encryption); String apInfo = String(i + 1) + " " + String(apList[i].rssi) + " " + apList[i].bssid + " " + String(apList[i].channel) + " " + displaySSID + " " + encryption; safePublish("Wemos-WifiScanner/AP-Found", apInfo); Serial.print("AP-Found: "); Serial.println(apInfo); delay(250); } channelAirtimeUtilization(); if (WiFi.status() != WL_CONNECTED && wifi_status == 1) { wifiConnect(); } } // Promiscuous mode callback to calculate airtime per packet. void airtime_callback(uint8_t *buf, uint16_t len) { my_wifi_promiscuous_pkt *pkt = (my_wifi_promiscuous_pkt *)buf; uint8_t rateCode = pkt->rx_ctrl.rate; float dataRateMbps = 11; // Default fallback value switch(rateCode) { case 0: dataRateMbps = 1; break; case 1: dataRateMbps = 2; break; case 2: dataRateMbps = 5.5; break; case 3: dataRateMbps = 11; break; default: break; } float airtimePerByte = 8.0 / dataRateMbps; // uint16_t packetLen = pkt->rx_ctrl.sig_len; uint16_t packetLen = len; totalAirtimeMicroseconds += (unsigned long)(packetLen * airtimePerByte); } // Measures channel airtime utilization as a percentage. float getChannelAirtimeUtilization(unsigned long measurementPeriodMs) { totalAirtimeMicroseconds = 0; wifi_set_promiscuous_rx_cb(airtime_callback); // Changed function call for ESP8266 wifi_promiscuous_enable(1); // Enable promiscuous mode on ESP8266 delay(measurementPeriodMs); wifi_promiscuous_enable(0); // Disable promiscuous mode unsigned long totalMeasurementMicroseconds = measurementPeriodMs * 1000; float utilization = (totalAirtimeMicroseconds / (float)totalMeasurementMicroseconds) * 100.0; return (utilization > 100.0) ? 100.0 : utilization; } // Publishes the channel airtime utilization. void channelAirtimeUtilization() { utilizationPercent = getChannelAirtimeUtilization(2000); } // Perform a single WiFi scan and cache the results. void checkForNetworks() { int numAP = WiFi.scanNetworks(false, true); std::vector<APData> apList; for (int i = 0; i < numAP; i++) { APData ap; ap.ssid = WiFi.SSID(i); ap.rssi = WiFi.RSSI(i); ap.encryption = WiFi.encryptionType(i); ap.channel = WiFi.channel(i); ap.bssid = WiFi.BSSIDstr(i); apList.push_back(ap); } delay(100); if (!mqttClient.connected()) { if (!tryReconnectMQTT(10000)) { Serial.println("MQTT server offline, proceeding with Serial output."); } } mqttClient.loop(); // Use the same scan data for SNR and AP stats. signalNoiseRatio(apList); updateStats(apList); } const unsigned char startlogo [] PROGMEM = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x03, 0x1F, 0x60, 0xE0, 0xBF, 0xFB, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF8, 0xC1, 0x07, 0x3F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, 0xC1, 0x07, 0x1F, 0xF8, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, 0xE1, 0x07, 0x1F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x87, 0x1F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xE3, 0x8F, 0x1F, 0x30, 0xE0, 0x03, 0xF6, 0xDF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0x8F, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0xCF, 0x0F, 0x00, 0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, 0xF3, 0x8F, 0x0F, 0x00, 0xE0, 0x87, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xF7, 0xDF, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, 0xF7, 0x9F, 0x07, 0x00, 0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, 0xF7, 0xDF, 0x07, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xF8, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xFC, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0xFF, 0xFE, 0x03, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFE, 0x03, 0xFC, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0x7F, 0xFC, 0x03, 0xF8, 0xE0, 0x07, 0xFF, 0x87, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0x7F, 0xFC, 0x03, 0xF8, 0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xF8, 0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x3F, 0xF8, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x16, 0xA8, 0x00, 0x58, 0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x80, 0xDF, 0xEF, 0xF7, 0x7D, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; void displayLogo() { u8g2.clearBuffer(); u8g2.drawXBMP(0, 0, 128, 64, startlogo); u8g2.sendBuffer(); } void displayVersion() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("ESP8266 WiFi Scanner"); u8g2.setCursor(0, 15); u8g2.print("Version 1"); u8g2.setCursor(0, 33); u8g2.print("MQTT Enabled"); u8g2.setCursor(0, 62); u8g2.print("(c) 2025 - CloudACM"); u8g2.sendBuffer(); } void displayIPInfo() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("IP "); u8g2.print(WiFiIPAddress); u8g2.setCursor(0, 15); u8g2.print("Mac "); u8g2.print(WiFi.macAddress()); u8g2.setCursor(0, 24); u8g2.print("SN "); u8g2.print(WiFiSubnet); u8g2.setCursor(0, 33); u8g2.print("GW "); u8g2.print(WiFiGWAddress); u8g2.setCursor(0, 42); u8g2.print("DNS "); u8g2.print(WiFi.dnsIP().toString()); u8g2.setCursor(0, 62); u8g2.print("RadioUse "); u8g2.print(utilizationPercent, 1); u8g2.print(" %"); u8g2.sendBuffer(); } void displayESPInfo() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("Scan Count "); u8g2.print(scanCount); u8g2.setCursor(0, 15); u8g2.print("WiFi Miscount "); u8g2.print(wifi_misCount); u8g2.setCursor(0, 24); u8g2.print("MQTT Miscount "); u8g2.print(mqtt_misCount); u8g2.setCursor(0, 33); u8g2.print("Uptime "); u8g2.print(millis()); u8g2.print(" ms"); u8g2.setCursor(0, 42); u8g2.print("FreeHeap "); u8g2.print(ESP.getFreeHeap()); u8g2.setCursor(0, 62); u8g2.print("APs Found "); u8g2.print(apCounter); u8g2.sendBuffer(); } void displayWiFiInfo() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("SSID "); u8g2.print(WiFi.SSID()); u8g2.setCursor(0, 15); u8g2.print("AP "); u8g2.print(WiFi.BSSIDstr()); u8g2.setCursor(0, 24); u8g2.print("Channel "); u8g2.print(WiFi.channel()); u8g2.setCursor(0, 33); u8g2.print("Signal "); u8g2.print(WiFi.RSSI()); u8g2.print(" dBm"); u8g2.setCursor(0, 42); u8g2.print("Noise "); u8g2.print(noiseFloor_dBm); u8g2.print(" dBm"); u8g2.setCursor(0, 62); u8g2.print("SNR "); u8g2.print(snr); u8g2.print(" dB"); u8g2.sendBuffer(); } void setup() { Serial.begin(115200); delay(50); // For ESP8266, use default I2C initialization (or set custom pins if needed) u8g2.begin(); u8g2.setFlipMode(1); displayLogo(); delay(50); wifiConnect(); delay(200); displayVersion(); delay(2000); } void loop() { checkForNetworks(); displayIPInfo(); delay(5000); displayESPInfo(); delay(5000); displayWiFiInfo(); } /************** End of code ***************/
/************** Start of code Board = ESP32 Arduino > ESP32C3 Dev Module, Disabled, Disabled, Huge APP (3MB No OTA/1MB SPIFFS) ***************/ #include <Arduino.h> #include <U8g2lib.h> #include <Wire.h> #include <WiFi.h> #include <esp_wifi.h> #include <PubSubClient.h> #include <math.h> #include <vector> U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE); WiFiClient espClient; PubSubClient mqttClient(espClient); const uint32_t scanDelay = 15000; // Delay between scans (ms) const uint32_t scanPeriod = scanDelay / 1000; // Scan cycle (sec) int scanCount = 0; float noiseFloor_dBm; int snr; String apCounter; // WiFi credentials struct WiFiCredentials { const char* myssid; const char* mypassword; }; WiFiCredentials wifi_networks[] = { {"ssid1", "open"}, {"ssid2", "password2"}, {"ssid3", "open"}, {"ssid4", "password4"} }; const char* mqtt_server = "mqtt_broker_ip"; int mqtt_status = 0; int mqtt_misCount = 0; int wifi_status = 0; int wifi_misCount = 0; // Global variable to accumulate airtime (in microseconds) volatile unsigned long totalAirtimeMicroseconds = 0; // Structure to store AP data from a scan struct APData { String ssid; int rssi; uint8_t encryption; int channel; String bssid; }; // Function Prototypes String encryptionTypeStr(uint8_t authmode); void mqttCallback(char* topic, byte* message, unsigned int length); bool tryReconnectMQTT(unsigned long timeout); void safePublish(const char* topic, const String & payload); void updateStats(const std::vector<APData>& apList); void wifiConnect(); void checkForNetworks(); void signalNoiseRatio(const std::vector<APData>& apList); void channelAirtimeUtilization(); void airtime_callback(void *buf, wifi_promiscuous_pkt_type_t type); float getChannelAirtimeUtilization(unsigned long measurementPeriodMs); float utilizationPercent; // Returns a human-readable string for the WiFi encryption type. String encryptionTypeStr(uint8_t authmode) { switch (authmode) { case WIFI_AUTH_OPEN: return "Open"; 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+WPA2"; case WIFI_AUTH_WPA2_ENTERPRISE: return "WPA2-EAP"; case WIFI_AUTH_WPA3_PSK: return "WPA3"; case WIFI_AUTH_WPA2_WPA3_PSK: return "WPA2+WPA3"; case WIFI_AUTH_WAPI_PSK: return "WAPI"; default: return "Unknown"; } } // Connect to the WiFi network and configure the MQTT client. void wifiConnect() { WiFi.mode(WIFI_STA); Serial.println(" "); Serial.println("Connecting to WiFi"); int n = WiFi.scanNetworks(); if (n == 0) { wifi_status = 0; return; } for (int i = 0; i < sizeof(wifi_networks) / sizeof(wifi_networks[0]); i++) { for (int j = 0; j < n; j++) { if (strcmp(wifi_networks[i].myssid, WiFi.SSID(j).c_str()) == 0) { WiFi.begin(wifi_networks[i].myssid, wifi_networks[i].mypassword); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 25) { delay(200); attempts++; } if (WiFi.status() == WL_CONNECTED) { wifi_status = 1; } else { wifi_status = 0; } } } } mqttClient.setServer(mqtt_server, 1883); mqttClient.setCallback(mqttCallback); } // Callback for incoming MQTT messages. void mqttCallback(char* topic, byte* message, unsigned int length) { String messageTemp; for (unsigned int i = 0; i < length; i++) { messageTemp += (char)message[i]; } // Process incoming message if needed. } // Try to reconnect to the MQTT server for the specified timeout (in ms). // Returns true if connection was re-established, false otherwise. bool tryReconnectMQTT(unsigned long timeout) { unsigned long startTime = millis(); while (!mqttClient.connected() && (millis() - startTime < timeout)) { if (mqttClient.connect("ESP32-C3-WifiScanner")) { mqttClient.subscribe("ESP32-C3-WifiScanner/#"); return true; } delay(500); } return mqttClient.connected(); } // Publish to MQTT if connected. If not, print the message to Serial. void safePublish(const char* topic, const String & payload) { if (mqttClient.connected()) { mqtt_status = 1; mqttClient.publish(topic, payload.c_str()); } else { mqtt_status = 0; } } // Calculate the signal-to-noise ratio (SNR) using cached scan results. void signalNoiseRatio(const std::vector<APData>& apList) { if (WiFi.status() == WL_CONNECTED) { int rssiConnected = WiFi.RSSI(); int connectedChannel = WiFi.channel(); String connectedBSSID = WiFi.BSSIDstr(); float totalNoise_mW = 0.0; std::vector<String> uniqueBSSIDs; for (size_t i = 0; i < apList.size(); i++) { if (apList[i].bssid == connectedBSSID) continue; int diff = abs(apList[i].channel - connectedChannel); float weight = 0.0; if (diff == 0) { weight = 1.0; } else if (diff == 1) { weight = 0.7; } else if (diff == 2) { weight = 0.3; } else { continue; } bool duplicate = false; for (size_t j = 0; j < uniqueBSSIDs.size(); j++) { if (uniqueBSSIDs[j] == apList[i].bssid) { duplicate = true; break; } } if (!duplicate) { uniqueBSSIDs.push_back(apList[i].bssid); float mW = pow(10, apList[i].rssi / 10.0); totalNoise_mW += weight * mW; } } noiseFloor_dBm = (uniqueBSSIDs.size() > 0) ? (10 * log10(totalNoise_mW)) : -95; snr = rssiConnected - noiseFloor_dBm; } safePublish("ESP32-C3-WifiScanner/Header", "=============================================="); Serial.println("=============================================="); safePublish("ESP32-C3-WifiScanner/Header", "------------------( IP Info )-----------------"); Serial.println("------------------( IP Info )-----------------"); safePublish("ESP32-C3-WifiScanner/HWAddress", WiFi.macAddress()); Serial.print("HWAddress: "); Serial.println(WiFi.macAddress()); safePublish("ESP32-C3-WifiScanner/IPAddress", WiFi.localIP().toString()); Serial.print("IPAddress: "); Serial.println(WiFi.localIP().toString()); safePublish("ESP32-C3-WifiScanner/Subnet", WiFi.subnetMask().toString()); Serial.print("Subnet: "); Serial.println(WiFi.subnetMask().toString()); safePublish("ESP32-C3-WifiScanner/GWAddress", WiFi.gatewayIP().toString()); Serial.print("GWAddress: "); Serial.println(WiFi.gatewayIP().toString()); safePublish("ESP32-C3-WifiScanner/DNS", WiFi.dnsIP().toString()); Serial.print("DNS: "); Serial.println(WiFi.dnsIP().toString()); safePublish("ESP32-C3-WifiScanner/MQTT", String(mqtt_server)); Serial.print("MQTT: "); Serial.println(mqtt_server); safePublish("ESP32-C3-WifiScanner/Header", "-----------------( WiFi Info )----------------"); Serial.println("-----------------( WiFi Info )----------------"); safePublish("ESP32-C3-WifiScanner/WifiNetwork", WiFi.SSID()); Serial.print("WifiNetwork: "); Serial.println(WiFi.SSID()); safePublish("ESP32-C3-WifiScanner/WifiAP", WiFi.BSSIDstr()); Serial.print("WifiAP: "); Serial.println(WiFi.BSSIDstr()); safePublish("ESP32-C3-WifiScanner/WifiChannel", String(WiFi.channel())); Serial.print("WifiChannel: "); Serial.println(WiFi.channel()); safePublish("ESP32-C3-WifiScanner/WifiSignal", String(WiFi.RSSI())); Serial.print("WifiSignal: "); Serial.println(WiFi.RSSI()); safePublish("ESP32-C3-WifiScanner/WifiNoiseFloor", String(noiseFloor_dBm)); Serial.print("WifiNoiseFloor: "); Serial.println(noiseFloor_dBm); safePublish("ESP32-C3-WifiScanner/WifiSNR", String(snr)); Serial.print("WifiSNR: "); Serial.println(snr); channelAirtimeUtilization(); safePublish("ESP32-C3-WifiScanner/Header", "-----------------( ESP32 Info )---------------"); Serial.println("-----------------( ESP32 Info )---------------"); } // Publish system stats and each AP's info using the cached scan data. void updateStats(const std::vector<APData>& apList) { delay(250); if (wifi_status == 1) { Serial.print("WiFi Status: "); Serial.println("Connected"); } if (wifi_status == 0) { Serial.print("WiFi Status: "); Serial.println("Disconnected"); wifi_misCount = wifi_misCount + 1; } if (mqtt_status == 1) { Serial.print("MQTT Status: "); Serial.println("Online"); } else { Serial.print("MQTT Status: "); Serial.println("Offline"); mqtt_misCount = mqtt_misCount + 1; } mqttClient.publish("ESP32-C3-WifiScanner/WiFiMissCount", String(wifi_misCount).c_str()); Serial.print("WiFi Miss Count: "); Serial.println(wifi_misCount); mqttClient.publish("ESP32-C3-WifiScanner/MQTTMissCount", String(mqtt_misCount).c_str()); Serial.print("MQTT Miss Count: "); Serial.println(mqtt_misCount); scanCount = scanCount + 1; mqttClient.publish("ESP32-C3-WifiScanner/ScanCount", String(scanCount).c_str()); Serial.print("ScanCount: "); Serial.println(scanCount); safePublish("ESP32-C3-WifiScanner/ScanPeriod", String(scanPeriod)); Serial.print("ScanPeriod: "); Serial.println(scanPeriod); safePublish("ESP32-C3-WifiScanner/Uptime", String(millis())); Serial.print("Uptime: "); Serial.println(millis()); safePublish("ESP32-C3-WifiScanner/FreeHeapSize", String(ESP.getFreeHeap())); Serial.print("FreeHeapSize: "); Serial.println(ESP.getFreeHeap()); safePublish("ESP32-C3-WifiScanner/Firmware", "ESP32-C3-WifiScanner_ver1"); Serial.println("Firmware: ESP32-C3-WifiScanner_ver1"); safePublish("ESP32-C3-WifiScanner/Notes", "Version Releaase Jan 15, 2025"); Serial.println("Notes: Version Releaase Jan 15, 2025"); safePublish("ESP32-C3-WifiScanner/Header", "----------------( AP Scan Info )--------------"); Serial.println("----------------( AP Scan Info )--------------"); safePublish("ESP32-C3-WifiScanner/Networks-Found", String(apList.size())); Serial.print("Networks-Found: "); Serial.println(apList.size()); apCounter = String(apList.size()); for (size_t i = 0; i < apList.size(); i++) { String displaySSID = (apList[i].ssid != "") ? apList[i].ssid : "*-Hidden-*"; String encryption = encryptionTypeStr(apList[i].encryption); String apInfo = String(i + 1) + " " + String(apList[i].rssi) + " " + apList[i].bssid + " " + String(apList[i].channel) + " " + displaySSID + " " + encryption; safePublish("ESP32-C3-WifiScanner/AP-Found", apInfo); Serial.print("AP-Found: "); Serial.println(apInfo); delay(250); } if (WiFi.status() != WL_CONNECTED && wifi_status == 0) { wifiConnect(); } } // Promiscuous mode callback to calculate airtime per packet. void airtime_callback(void *buf, wifi_promiscuous_pkt_type_t type) { wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *) buf; uint8_t rateCode = pkt->rx_ctrl.rate; float dataRateMbps = 11; // Default fallback switch(rateCode) { case 0: dataRateMbps = 1; break; case 1: dataRateMbps = 2; break; case 2: dataRateMbps = 5.5; break; case 3: dataRateMbps = 11; break; default: break; } float airtimePerByte = 8.0 / dataRateMbps; uint16_t len = pkt->rx_ctrl.sig_len; totalAirtimeMicroseconds += (unsigned long)(len * airtimePerByte); } // Measures channel airtime utilization as a percentage. float getChannelAirtimeUtilization(unsigned long measurementPeriodMs) { totalAirtimeMicroseconds = 0; esp_wifi_set_promiscuous_rx_cb(airtime_callback); esp_wifi_set_promiscuous(true); delay(measurementPeriodMs); esp_wifi_set_promiscuous(false); // Re-enable station mode so WiFi can reconnect if needed. WiFi.mode(WIFI_STA); unsigned long totalMeasurementMicroseconds = measurementPeriodMs * 1000; float utilization = (totalAirtimeMicroseconds / (float)totalMeasurementMicroseconds) * 100.0; return (utilization > 100.0) ? 100.0 : utilization; } // Publishes the channel airtime utilization. void channelAirtimeUtilization() { utilizationPercent = getChannelAirtimeUtilization(2000); safePublish("ESP32-C3-WifiScanner/RadioUse", String(utilizationPercent, 1)); Serial.print("RadioUse: "); Serial.println(utilizationPercent, 1); } // Perform a single WiFi scan and cache the results. void checkForNetworks() { int numAP = WiFi.scanNetworks(false, true); std::vector<APData> apList; for (int i = 0; i < numAP; i++) { APData ap; ap.ssid = WiFi.SSID(i); ap.rssi = WiFi.RSSI(i); ap.encryption = WiFi.encryptionType(i); ap.channel = WiFi.channel(i); ap.bssid = WiFi.BSSIDstr(i); apList.push_back(ap); } delay(100); if (!mqttClient.connected()) { if (!tryReconnectMQTT(10000)) { Serial.println("MQTT server offline, proceeding with Serial output."); } } mqttClient.loop(); // Use the same scan data for SNR and AP stats. signalNoiseRatio(apList); updateStats(apList); } const unsigned char startlogo [] PROGMEM = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x03, 0x1F, 0x60, 0xE0, 0xBF, 0xFB, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF8, 0xC1, 0x07, 0x3F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, 0xC1, 0x07, 0x1F, 0xF8, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, 0xE1, 0x07, 0x1F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x87, 0x1F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xE3, 0x8F, 0x1F, 0x30, 0xE0, 0x03, 0xF6, 0xDF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0x8F, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0xCF, 0x0F, 0x00, 0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, 0xF3, 0x8F, 0x0F, 0x00, 0xE0, 0x87, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xF7, 0xDF, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, 0xF7, 0x9F, 0x07, 0x00, 0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, 0xF7, 0xDF, 0x07, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xF8, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xFC, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0xFF, 0xFE, 0x03, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFE, 0x03, 0xFC, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0x7F, 0xFC, 0x03, 0xF8, 0xE0, 0x07, 0xFF, 0x87, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0x7F, 0xFC, 0x03, 0xF8, 0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xF8, 0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x3F, 0xF8, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x16, 0xA8, 0x00, 0x58, 0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x80, 0xDF, 0xEF, 0xF7, 0x7D, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, }; void displayLogo() { u8g2.clearBuffer(); u8g2.drawXBMP(0, 0, 128, 64, startlogo); u8g2.sendBuffer(); } void displayVersion() { u8g2.clearBuffer(); // u8g2.setFont(u8g2_font_siji_t_6x10); // see, https://github.com/olikraus/u8g2/wiki/u8g2reference u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("ESP32-C3 WiFi Scanner"); u8g2.setCursor(0, 15); u8g2.print("Version 1"); u8g2.setCursor(0, 33); u8g2.print("MQTT Enabled"); u8g2.setCursor(0, 62); u8g2.print("(c) 2025 - CloudACM"); u8g2.sendBuffer(); } void displayIPInfo() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("IP "); u8g2.print(WiFi.localIP().toString()); u8g2.setCursor(0, 15); u8g2.print("Mac "); u8g2.print(WiFi.macAddress()); u8g2.setCursor(0, 24); u8g2.print("SN "); u8g2.print(WiFi.subnetMask().toString()); u8g2.setCursor(0, 33); u8g2.print("GW "); u8g2.print(WiFi.gatewayIP().toString()); u8g2.setCursor(0, 42); u8g2.print("DNS "); u8g2.print(WiFi.dnsIP().toString()); u8g2.setCursor(0, 62); u8g2.print("RadioUse "); u8g2.print(utilizationPercent, 1); u8g2.print(" %"); u8g2.sendBuffer(); } void displayWiFiInfo() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("SSID "); u8g2.print(WiFi.SSID()); u8g2.setCursor(0, 15); u8g2.print("AP "); u8g2.print(WiFi.BSSIDstr()); u8g2.setCursor(0, 24); u8g2.print("Channel "); u8g2.print(WiFi.channel()); u8g2.setCursor(0, 33); u8g2.print("Signal "); u8g2.print(WiFi.RSSI()); u8g2.print(" dBm"); u8g2.setCursor(0, 42); u8g2.print("Noise "); u8g2.print(noiseFloor_dBm); u8g2.print(" dBm"); u8g2.setCursor(0, 62); u8g2.print("SNR "); u8g2.print(snr); u8g2.print(" dB"); u8g2.sendBuffer(); } void displayESPInfo() { u8g2.clearBuffer(); u8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf); u8g2.setCursor(0, 6); u8g2.print("Scan Count "); u8g2.print(scanCount); u8g2.setCursor(0, 15); u8g2.print("WiFi Miscount "); u8g2.print(wifi_misCount); u8g2.setCursor(0, 24); u8g2.print("MQTT Miscount "); u8g2.print(mqtt_misCount); u8g2.setCursor(0, 33); u8g2.print("Uptime "); u8g2.print(millis()); u8g2.print(" ms"); u8g2.setCursor(0, 42); u8g2.print("FreeHeap "); u8g2.print(ESP.getFreeHeap()); u8g2.setCursor(0, 62); u8g2.print("APs Found "); u8g2.print(apCounter); u8g2.sendBuffer(); } void setup() { Serial.begin(115200); delay(50); Wire.begin(6, 10); u8g2.begin(); u8g2.setFlipMode(1); displayLogo(); delay(50); wifiConnect(); delay(1000); displayVersion(); delay(2500); } void loop() { checkForNetworks(); displayIPInfo(); delay(5000); displayESPInfo(); delay(5000); displayWiFiInfo(); delay(5000); } /************** End of code ***************/
The code functions can be modified as needed. Creating a logo image can be done with GIMP or Imagemagick by resizing and saving it in a XBM ascii format. Since the displays are monochromatic, not greyscale, dithering can be done to give a shading effect. The development community points to several file converters online that offer this, but there is an inherent risk of being injected with a malicious payload. Don’t get scammed by an online document converter, here’s a warning from the FBI Denver Field Office.
Here is the IM command used for the logo in the code above. The resulting XBM file can be opened in a text editor to copy the section that goes into the “startlogo” PROGMEM array.
convert input.png -negate -monochrome output.xbm
Since the MQTT functions were included, Node-Red was leveraged to provide a dashboard widget and to save the results to a log file.
This is the Node Red flow details that displays the readings in a console window on the dashboard.
1. Use a MQTT In Node and subscribe to the topic with .../AP-Found 2. Pass it to a Function Node titled "Message Stacker v2" with the following code: // Time threshold to identify a new burst (in milliseconds) const BURST_THRESHOLD = 5000; // 5 seconds // Get the last message timestamp and messages from flow context var lastMessageTime = flow.get('lastMessageTime') || 0; var currentTime = new Date().getTime(); var messages = flow.get('messages') || ''; // If time since last message exceeds threshold, consider it a new burst if (currentTime - lastMessageTime > BURST_THRESHOLD) { messages = ''; // Clear the messages buffer for new burst } // Add new message to buffer messages += msg.payload + "\n"; // Update flow context flow.set('lastMessageTime', currentTime); flow.set('messages', messages); // Set message payload msg.payload = messages; return msg; 3. Pass that to a Template Node with the following template code: <style> #myText { height: 200px; overflow-y: scroll; background-color: #242424; color: #d29200; font-size: 12px; font-family: monospace; white-space: pre-wrap; } </style> <div id="container"> <div id="myText">{{msg.payload}}</div> </div> <script> var container = document.getElementById("container"); container.scrollTop = container.scrollHeight; // Whenever new data is added to the container, scroll to the bottom container.addEventListener("DOMNodeInserted", function () { container.scrollTop = container.scrollHeight; }); </script>