{"id":4833,"date":"2025-03-28T08:00:58","date_gmt":"2025-03-28T15:00:58","guid":{"rendered":"https:\/\/www.cloudacm.com\/?p=4833"},"modified":"2025-03-27T22:55:33","modified_gmt":"2025-03-28T05:55:33","slug":"espressif-oled-display-stack","status":"publish","type":"post","link":"https:\/\/www.cloudacm.com\/?p=4833","title":{"rendered":"ESP Based Wi-Fi Network Indicator"},"content":{"rendered":"<p>This post will cover the use of a SSD1306 OLED display with Espressif based modules.\u00a0 The setup will display wireless statistics using inexpensive devices that are simple to connect.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4854 size-large\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator-1024x235.png\" alt=\"\" width=\"640\" height=\"147\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator-1024x235.png 1024w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator-300x69.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator-768x176.png 768w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator-1536x353.png 1536w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator-604x139.png 604w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wi-Fi-Indicator.png 1568w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/a><\/p>\n<p>This work was based on the following video demonstration.<\/p>\n<p><iframe loading=\"lazy\" title=\"Make Your Own WiFi Signal Strength Monitor Checker or Scanner | DIY WiFi Signal Strength Scanner\" width=\"640\" height=\"360\" src=\"https:\/\/www.youtube.com\/embed\/VM3Onk-PZtA?feature=oembed\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\" referrerpolicy=\"strict-origin-when-cross-origin\" allowfullscreen><\/iframe><\/p>\n<p>The wiring for the ESP8266 Wemos module used the following I2C pins to connect to the display.\u00a0 The I2C connection uses fewer pins and frees up pins for other tasks.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wemos_SSD1306_Wiring.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4836 size-medium\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wemos_SSD1306_Wiring-115x300.jpg\" alt=\"\" width=\"115\" height=\"300\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wemos_SSD1306_Wiring-115x300.jpg 115w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wemos_SSD1306_Wiring-103x270.jpg 103w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Wemos_SSD1306_Wiring.jpg 297w\" sizes=\"auto, (max-width: 115px) 100vw, 115px\" \/><\/a><\/p>\n<p>The ESP32 C3 module connection uses different pins, so a cross platform stacking module is not possible.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/ESP32-C3_Pinout.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-medium wp-image-4846\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/ESP32-C3_Pinout-300x169.png\" alt=\"\" width=\"300\" height=\"169\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/ESP32-C3_Pinout-300x169.png 300w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/ESP32-C3_Pinout-480x270.png 480w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/ESP32-C3_Pinout.png 533w\" sizes=\"auto, (max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>There were two sizes of the SSD1306 developed in this post for the sake of demonstrating their differences.\u00a0 I identified them as &#8220;micro&#8221; for the 12mm (64 x 32 pixel) and &#8220;mini&#8221; for the 24 mm (128 x 64 pixel) display.\u00a0 Both displays are monochromatic with each pixel either on or off.\u00a0 The larger &#8220;mini&#8221; display has a color option for a portion of the display to be yellow or blue.<\/p>\n<p>The &#8220;mini&#8221; Wi-Fi indicator starts automatically and displays a logo when powered on.\u00a0 This is followed by some information about the firmware.\u00a0 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&#8217;t practical.<\/p>\n<p>Once up and running, the module will cycle through three separate screens.\u00a0 The first provides IP and Mac Address details about the device and its connected network.\u00a0 The second provides operational details about the module.\u00a0 The last screen provides details about the Wi-Fi network signal.\u00a0 Each screen also includes a lower portion that contains details about AP count, Signal Noise Ratio, and Radio Usage.<\/p>\n<p>The &#8220;micro&#8221; indicator is slimmed down and provides fewer details.<\/p>\n<p>Here is the Arduino Code for each module type.<\/p>\n<pre>\/**************\r\nStart of code\r\nBoard = ESP8266 Boards (3.1.2) &gt; LOLIN(WEMOS) D1 Mini (Clone), 4MB (FS:none OTA:~1019KB)\r\n***************\/\r\n\r\n#include &lt;Arduino.h&gt;\r\n#include &lt;U8g2lib.h&gt;\r\n#include &lt;Wire.h&gt;\r\n#include &lt;ESP8266WiFi.h&gt; \r\n#include &lt;PubSubClient.h&gt;\r\n#include &lt;math.h&gt;\r\n#include &lt;vector&gt;\r\n\r\nextern \"C\" {\r\n#include \"user_interface.h\"\r\n}\r\n\r\nstruct my_wifi_promiscuous_pkt {\r\nstruct {\r\nint8_t rssi; \/\/ Received signal strength\r\nuint8_t rate; \/\/ Data rate code\r\nuint16_t sig_len; \/\/ Signal length (number of bytes)\r\n} rx_ctrl;\r\n};\r\n\r\n\/\/ Initialize display with I2C (using default SDA\/SCL pins for ESP8266)\r\nU8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, \/* reset=*\/ U8X8_PIN_NONE);\r\n\r\nWiFiClient espClient;\r\nPubSubClient mqttClient(espClient);\r\n\r\nconst uint32_t scanDelay = 15000; \/\/ Delay between scans (ms)\r\nconst uint32_t scanPeriod = scanDelay \/ 1000; \/\/ Scan cycle (sec)\r\nint scanCount = 0;\r\n\r\nfloat noiseFloor_dBm;\r\nint snr;\r\nString apCounter;\r\n\r\n\/\/ WiFi credentials \r\nstruct WiFiCredentials {\r\nconst char* myssid;\r\nconst char* mypassword;\r\n};\r\n\r\nWiFiCredentials wifi_networks[] = {\r\n{\"ssid1\", \"open\"},\r\n{\"ssid2\", \"password2\"},\r\n{\"ssid3\", \"open\"},\r\n{\"ssid4\", \"password4\"}\r\n};\r\n\r\nconst char* mqtt_server = \"mqtt_broker_ip\";\r\nint mqtt_status = 0;\r\nint mqtt_misCount = 0;\r\nint wifi_status = 0;\r\nint wifi_misCount = 0;\r\n\r\n\/\/ Global variable to accumulate airtime (in microseconds)\r\nvolatile unsigned long totalAirtimeMicroseconds = 0;\r\n\r\n\/\/ Structure to store AP data from a scan\r\nstruct APData {\r\nString ssid;\r\nint rssi;\r\nuint8_t encryption;\r\nint channel;\r\nString bssid;\r\n};\r\n\r\n\/\/ Function Prototypes\r\nString encryptionTypeStr(uint8_t authmode);\r\nvoid mqttCallback(char* topic, byte* message, unsigned int length);\r\nbool tryReconnectMQTT(unsigned long timeout);\r\nvoid safePublish(const char* topic, const String &amp; payload);\r\nvoid updateStats(const std::vector&lt;APData&gt;&amp; apList);\r\nvoid wifiConnect();\r\nvoid checkForNetworks();\r\nvoid signalNoiseRatio(const std::vector&lt;APData&gt;&amp; apList);\r\nvoid channelAirtimeUtilization();\r\n\r\nfloat getChannelAirtimeUtilization(unsigned long measurementPeriodMs);\r\n\r\n\/\/ Returns a human-readable string for the WiFi encryption type.\r\nString encryptionTypeStr(uint8_t authmode) {\r\nswitch (authmode) {\r\ncase ENC_TYPE_NONE: return \"Open\";\r\ncase ENC_TYPE_WEP: return \"WEP\";\r\ncase ENC_TYPE_TKIP: return \"WPA+PSK\";\r\ncase ENC_TYPE_CCMP: return \"WPA2+PSK\";\r\ncase ENC_TYPE_AUTO: return \"WPA+WPA2+PSK\";\r\ndefault: return \"Unknown\";\r\n}\r\n}\r\n\r\nString WiFiIPAddress;\r\nString WiFiSubnet;\r\nString WiFiGWAddress;\r\nfloat utilizationPercent;\r\n\r\n\/\/ Connect to the WiFi network and configure the MQTT client.\r\nvoid wifiConnect() {\r\n\r\nWiFi.mode(WIFI_STA);\r\nSerial.println(\" \");\r\nSerial.println(\"Connecting to WiFi\");\r\nint n = WiFi.scanNetworks();\r\nif (n == 0) {\r\nwifi_status = 0;\r\nreturn;\r\n}\r\nfor (int i = 0; i &lt; sizeof(wifi_networks) \/ sizeof(wifi_networks[0]); i++) {\r\nfor (int j = 0; j &lt; n; j++) {\r\nif (strcmp(wifi_networks[i].myssid, WiFi.SSID(j).c_str()) == 0) {\r\nWiFi.begin(wifi_networks[i].myssid, wifi_networks[i].mypassword);\r\nint attempts = 0;\r\nwhile (WiFi.status() != WL_CONNECTED &amp;&amp; attempts &lt; 25) {\r\ndelay(200);\r\nattempts++;\r\n}\r\nif (WiFi.status() == WL_CONNECTED) {\r\nwifi_status = 1;\r\n} \r\n}\r\n}\r\n}\r\n\r\nmqttClient.setServer(mqtt_server, 1883);\r\nmqttClient.setCallback(mqttCallback);\r\n}\r\n\r\n\/\/ Callback for incoming MQTT messages.\r\nvoid mqttCallback(char* topic, byte* message, unsigned int length) {\r\nString messageTemp;\r\nfor (unsigned int i = 0; i &lt; length; i++) {\r\nmessageTemp += (char)message[i];\r\n}\r\n\/\/ Process incoming message if needed.\r\n}\r\n\r\n\/\/ Try to reconnect to the MQTT server for the specified timeout (in ms).\r\n\/\/ Returns true if connection was re-established, false otherwise.\r\nbool tryReconnectMQTT(unsigned long timeout) {\r\nunsigned long startTime = millis();\r\nwhile (!mqttClient.connected() &amp;&amp; (millis() - startTime &lt; timeout)) {\r\nif (mqttClient.connect(\"Wemos-WifiScanner\")) {\r\nmqttClient.subscribe(\"Wemos-WifiScanner\/#\");\r\nreturn true;\r\n}\r\ndelay(500);\r\n}\r\nreturn mqttClient.connected();\r\n}\r\n\r\n\/\/ Publish to MQTT if connected. If not, print the message to Serial.\r\nvoid safePublish(const char* topic, const String &amp; payload) {\r\nif (mqttClient.connected()) {\r\nmqtt_status = 1;\r\nmqttClient.publish(topic, payload.c_str());\r\n} else {\r\nmqtt_status = 0;\r\n}\r\n}\r\n\r\n\/\/ Calculate the signal-to-noise ratio (SNR) using cached scan results.\r\nvoid signalNoiseRatio(const std::vector&lt;APData&gt;&amp; apList) {\r\nif (WiFi.status() == WL_CONNECTED) {\r\nint rssiConnected = WiFi.RSSI();\r\nint connectedChannel = WiFi.channel();\r\nString connectedBSSID = WiFi.BSSIDstr();\r\n\r\nfloat totalNoise_mW = 0.0;\r\nstd::vector&lt;String&gt; uniqueBSSIDs;\r\n\r\nfor (size_t i = 0; i &lt; apList.size(); i++) {\r\nif (apList[i].bssid == connectedBSSID) continue;\r\nint diff = abs(apList[i].channel - connectedChannel);\r\nfloat weight = 0.0;\r\nif (diff == 0) {\r\nweight = 1.0;\r\n} else if (diff == 1) {\r\nweight = 0.7;\r\n} else if (diff == 2) {\r\nweight = 0.3;\r\n} else {\r\ncontinue;\r\n}\r\nbool duplicate = false;\r\nfor (size_t j = 0; j &lt; uniqueBSSIDs.size(); j++) {\r\nif (uniqueBSSIDs[j] == apList[i].bssid) {\r\nduplicate = true;\r\nbreak;\r\n}\r\n}\r\nif (!duplicate) {\r\nuniqueBSSIDs.push_back(apList[i].bssid);\r\nfloat mW = pow(10, apList[i].rssi \/ 10.0);\r\ntotalNoise_mW += weight * mW;\r\n}\r\n}\r\n\r\nnoiseFloor_dBm = (uniqueBSSIDs.size() &gt; 0) ? (10 * log10(totalNoise_mW)) : -95;\r\nsnr = rssiConnected - noiseFloor_dBm;\r\n}\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Header\", \"==============================================\");\r\nSerial.println(\"==============================================\");\r\nsafePublish(\"Wemos-WifiScanner\/Header\", \"------------------( IP Info )-----------------\");\r\nSerial.println(\"------------------( IP Info )-----------------\");\r\n\r\nsafePublish(\"Wemos-WifiScanner\/HWAddress\", WiFi.macAddress());\r\nSerial.print(\"HWAddress: \");\r\nSerial.println(WiFi.macAddress());\r\n\r\nWiFiIPAddress = WiFi.localIP().toString();\r\nsafePublish(\"Wemos-WifiScanner\/IPAddress\", WiFiIPAddress);\r\nSerial.print(\"IPAddress: \");\r\nSerial.println(WiFiIPAddress);\r\n\r\nWiFiSubnet = WiFi.subnetMask().toString();\r\nsafePublish(\"Wemos-WifiScanner\/Subnet\", WiFiSubnet);\r\nSerial.print(\"Subnet: \");\r\nSerial.println(WiFiSubnet);\r\n\r\nWiFiGWAddress = WiFi.gatewayIP().toString();\r\nsafePublish(\"Wemos-WifiScanner\/GWAddress\", WiFiGWAddress);\r\nSerial.print(\"GWAddress: \");\r\nSerial.println(WiFiGWAddress);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/DNS\", WiFi.dnsIP().toString());\r\nSerial.print(\"DNS: \");\r\nSerial.println(WiFi.dnsIP().toString());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/MQTT\", String(mqtt_server));\r\nSerial.print(\"MQTT: \");\r\nSerial.println(mqtt_server);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Header\", \"-----------------( WiFi Info )----------------\");\r\nSerial.println(\"-----------------( WiFi Info )----------------\");\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WifiNetwork\", WiFi.SSID());\r\nSerial.print(\"WifiNetwork: \");\r\nSerial.println(WiFi.SSID());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WifiAP\", WiFi.BSSIDstr());\r\nSerial.print(\"WifiAP: \");\r\nSerial.println(WiFi.BSSIDstr());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WifiChannel\", String(WiFi.channel()));\r\nSerial.print(\"WifiChannel: \");\r\nSerial.println(WiFi.channel());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WifiSignal\", String(WiFi.RSSI()));\r\nSerial.print(\"WifiSignal: \");\r\nSerial.println(WiFi.RSSI());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WifiNoiseFloor\", String(noiseFloor_dBm));\r\nSerial.print(\"WifiNoiseFloor: \");\r\nSerial.println(noiseFloor_dBm);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WifiSNR\", String(snr));\r\nSerial.print(\"WifiSNR: \");\r\nSerial.println(snr);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/RadioUse\", String(utilizationPercent, 1));\r\nSerial.print(\"RadioUse: \");\r\nSerial.println(utilizationPercent, 1);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Header\", \"-----------------( ESP8266 Info )---------------\");\r\nSerial.println(\"-----------------( ESP8266 Info )---------------\");\r\n}\r\n\r\n\/\/ Publish system stats and each AP's info using the cached scan data.\r\nvoid updateStats(const std::vector&lt;APData&gt;&amp; apList) {\r\ndelay(250);\r\n\r\nif (wifi_status == 1) {\r\nSerial.print(\"WiFi Status: \");\r\nSerial.println(\"Connected\");\r\n}\r\nif (wifi_status == 0) {\r\nSerial.print(\"WiFi Status: \");\r\nSerial.println(\"Disconnected\");\r\nwifi_misCount = wifi_misCount + 1;\r\n}\r\n\r\nif (mqtt_status == 1) {\r\nSerial.print(\"MQTT Status: \");\r\nSerial.println(\"Online\");\r\n}\r\nelse {\r\nSerial.print(\"MQTT Status: \");\r\nSerial.println(\"Offline\");\r\nmqtt_misCount = mqtt_misCount + 1;\r\n}\r\n\r\nsafePublish(\"Wemos-WifiScanner\/WiFiMissCount\", String(wifi_misCount).c_str());\r\nSerial.print(\"WiFi Miss Count: \");\r\nSerial.println(wifi_misCount);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/MQTTMissCount\", String(mqtt_misCount).c_str());\r\nSerial.print(\"MQTT Miss Count: \");\r\nSerial.println(mqtt_misCount);\r\n\r\nscanCount = scanCount + 1;\r\nsafePublish(\"Wemos-WifiScanner\/ScanCount\", String(scanCount).c_str());\r\nSerial.print(\"ScanCount: \");\r\nSerial.println(scanCount);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/ScanPeriod\", String(scanPeriod));\r\nSerial.print(\"ScanPeriod: \");\r\nSerial.println(scanPeriod);\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Uptime\", String(millis()));\r\nSerial.print(\"Uptime: \");\r\nSerial.println(millis());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/FreeHeapSize\", String(ESP.getFreeHeap()));\r\nSerial.print(\"FreeHeapSize: \");\r\nSerial.println(ESP.getFreeHeap());\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Firmware\", \"Wemos-WifiScanner_ver1\");\r\nSerial.println(\"Firmware: Wemos-WifiScanner_ver1\");\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Notes\", \"Version Releaase Jan 15, 2025\");\r\nSerial.println(\"Notes: Version Releaase Jan 15, 2025\");\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Header\", \"----------------( AP Scan Info )--------------\");\r\nSerial.println(\"----------------( AP Scan Info )--------------\");\r\n\r\nsafePublish(\"Wemos-WifiScanner\/Networks-Found\", String(apList.size()));\r\nSerial.print(\"Networks-Found: \");\r\nSerial.println(apList.size());\r\n\r\napCounter = String(apList.size());\r\n\r\nfor (size_t i = 0; i &lt; apList.size(); i++) {\r\nString displaySSID = (apList[i].ssid != \"\") ? apList[i].ssid : \"*-Hidden-*\";\r\nString encryption = encryptionTypeStr(apList[i].encryption);\r\nString apInfo = String(i + 1) + \" \" + String(apList[i].rssi) + \" \" +\r\napList[i].bssid + \" \" + String(apList[i].channel) + \" \" +\r\ndisplaySSID + \" \" + encryption;\r\nsafePublish(\"Wemos-WifiScanner\/AP-Found\", apInfo);\r\nSerial.print(\"AP-Found: \");\r\nSerial.println(apInfo);\r\ndelay(250);\r\n}\r\nchannelAirtimeUtilization();\r\n\r\nif (WiFi.status() != WL_CONNECTED &amp;&amp; wifi_status == 1) {\r\nwifiConnect();\r\n}\r\n}\r\n\r\n\/\/ Promiscuous mode callback to calculate airtime per packet.\r\nvoid airtime_callback(uint8_t *buf, uint16_t len) {\r\nmy_wifi_promiscuous_pkt *pkt = (my_wifi_promiscuous_pkt *)buf;\r\nuint8_t rateCode = pkt-&gt;rx_ctrl.rate;\r\nfloat dataRateMbps = 11; \/\/ Default fallback value\r\nswitch(rateCode) {\r\ncase 0: dataRateMbps = 1; break;\r\ncase 1: dataRateMbps = 2; break;\r\ncase 2: dataRateMbps = 5.5; break;\r\ncase 3: dataRateMbps = 11; break;\r\ndefault: break;\r\n}\r\nfloat airtimePerByte = 8.0 \/ dataRateMbps;\r\n\r\n\/\/ uint16_t packetLen = pkt-&gt;rx_ctrl.sig_len;\r\nuint16_t packetLen = len;\r\n\r\ntotalAirtimeMicroseconds += (unsigned long)(packetLen * airtimePerByte);\r\n}\r\n\r\n\/\/ Measures channel airtime utilization as a percentage.\r\nfloat getChannelAirtimeUtilization(unsigned long measurementPeriodMs) {\r\ntotalAirtimeMicroseconds = 0;\r\nwifi_set_promiscuous_rx_cb(airtime_callback); \/\/ Changed function call for ESP8266\r\nwifi_promiscuous_enable(1); \/\/ Enable promiscuous mode on ESP8266\r\ndelay(measurementPeriodMs);\r\nwifi_promiscuous_enable(0); \/\/ Disable promiscuous mode\r\n\r\nunsigned long totalMeasurementMicroseconds = measurementPeriodMs * 1000;\r\nfloat utilization = (totalAirtimeMicroseconds \/ (float)totalMeasurementMicroseconds) * 100.0;\r\nreturn (utilization &gt; 100.0) ? 100.0 : utilization;\r\n}\r\n\r\n\/\/ Publishes the channel airtime utilization.\r\nvoid channelAirtimeUtilization() {\r\nutilizationPercent = getChannelAirtimeUtilization(2000);\r\n}\r\n\r\n\/\/ Perform a single WiFi scan and cache the results.\r\nvoid checkForNetworks() {\r\nint numAP = WiFi.scanNetworks(false, true);\r\nstd::vector&lt;APData&gt; apList;\r\nfor (int i = 0; i &lt; numAP; i++) {\r\nAPData ap;\r\nap.ssid = WiFi.SSID(i);\r\nap.rssi = WiFi.RSSI(i);\r\nap.encryption = WiFi.encryptionType(i);\r\nap.channel = WiFi.channel(i);\r\nap.bssid = WiFi.BSSIDstr(i);\r\napList.push_back(ap);\r\n}\r\n\r\ndelay(100);\r\n\r\nif (!mqttClient.connected()) {\r\nif (!tryReconnectMQTT(10000)) {\r\nSerial.println(\"MQTT server offline, proceeding with Serial output.\");\r\n}\r\n}\r\nmqttClient.loop();\r\n\r\n\/\/ Use the same scan data for SNR and AP stats.\r\nsignalNoiseRatio(apList);\r\nupdateStats(apList);\r\n}\r\n\r\nconst unsigned char startlogo [] PROGMEM = {\r\n\r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, \r\n0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, \r\n0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, \r\n0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, \r\n0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, \r\n0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, \r\n0x00, 0xFC, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, \r\n0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0x80, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x03, 0x1F, 0x60, 0xE0, 0xBF, 0xFB, 0xFF, \r\n0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF8, 0xC1, 0x07, 0x3F, 0xF8, \r\n0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, \r\n0xC1, 0x07, 0x1F, 0xF8, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xF0, 0xE1, 0x07, 0x1F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x87, 0x1F, 0xF8, \r\n0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, \r\n0xE3, 0x8F, 0x1F, 0x30, 0xE0, 0x03, 0xF6, 0xDF, 0x1F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0x8F, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0xCF, 0x0F, 0x00, \r\n0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, \r\n0xF3, 0x8F, 0x0F, 0x00, 0xE0, 0x87, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xE0, 0xF7, 0xDF, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, \r\n0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, 0xF7, 0x9F, 0x07, 0x00, \r\n0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, \r\n0xF7, 0xDF, 0x07, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xF8, 0xE0, 0x03, 0xE0, 0x87, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xFC, \r\n0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, \r\n0xFF, 0xFE, 0x03, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFE, 0x03, 0xFC, 0xE0, 0x07, 0xE0, 0x83, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0x7F, 0xFC, 0x03, 0xF8, \r\n0xE0, 0x07, 0xFF, 0x87, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, \r\n0x7F, 0xFC, 0x03, 0xF8, 0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x83, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xF8, \r\n0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, \r\n0x3F, 0xF8, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x7F, 0x00, 0x16, 0xA8, 0x00, 0x58, 0xF0, 0xFF, 0xFF, 0xFF, \r\n0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, \r\n0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xFE, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, \r\n0x00, 0x00, 0x00, 0x80, 0xDF, 0xEF, 0xF7, 0x7D, 0x80, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, \r\n0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x45, 0x00, 0x00, 0x00, \r\n0x00, 0x00, 0x00, 0x8A, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, \r\n0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, \r\n0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, \r\n0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF,\r\n};\r\n\r\nvoid displayLogo() {\r\nu8g2.clearBuffer();\r\nu8g2.drawXBMP(0, 0, 128, 64, startlogo);\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayVersion() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"ESP8266 WiFi Scanner\");\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"Version 1\");\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"MQTT Enabled\");\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"(c) 2025 - CloudACM\");\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayIPInfo() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"IP \");\r\nu8g2.print(WiFiIPAddress);\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"Mac \");\r\nu8g2.print(WiFi.macAddress());\r\nu8g2.setCursor(0, 24);\r\nu8g2.print(\"SN \");\r\nu8g2.print(WiFiSubnet);\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"GW \");\r\nu8g2.print(WiFiGWAddress);\r\nu8g2.setCursor(0, 42);\r\nu8g2.print(\"DNS \");\r\nu8g2.print(WiFi.dnsIP().toString());\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"RadioUse \");\r\nu8g2.print(utilizationPercent, 1);\r\nu8g2.print(\" %\");\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayESPInfo() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"Scan Count \");\r\nu8g2.print(scanCount);\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"WiFi Miscount \");\r\nu8g2.print(wifi_misCount);\r\nu8g2.setCursor(0, 24);\r\nu8g2.print(\"MQTT Miscount \");\r\nu8g2.print(mqtt_misCount);\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"Uptime \");\r\nu8g2.print(millis());\r\nu8g2.print(\" ms\");\r\nu8g2.setCursor(0, 42);\r\nu8g2.print(\"FreeHeap \");\r\nu8g2.print(ESP.getFreeHeap());\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"APs Found \");\r\nu8g2.print(apCounter);\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayWiFiInfo() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"SSID \");\r\nu8g2.print(WiFi.SSID());\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"AP \");\r\nu8g2.print(WiFi.BSSIDstr());\r\nu8g2.setCursor(0, 24);\r\nu8g2.print(\"Channel \");\r\nu8g2.print(WiFi.channel());\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"Signal \");\r\nu8g2.print(WiFi.RSSI());\r\nu8g2.print(\" dBm\");\r\nu8g2.setCursor(0, 42);\r\nu8g2.print(\"Noise \");\r\nu8g2.print(noiseFloor_dBm);\r\nu8g2.print(\" dBm\");\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"SNR \");\r\nu8g2.print(snr);\r\nu8g2.print(\" dB\");\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid setup() {\r\nSerial.begin(115200);\r\ndelay(50);\r\n\r\n\/\/ For ESP8266, use default I2C initialization (or set custom pins if needed)\r\nu8g2.begin();\r\nu8g2.setFlipMode(1);\r\ndisplayLogo();\r\ndelay(50);\r\n\r\nwifiConnect();\r\ndelay(200);\r\ndisplayVersion();\r\ndelay(2000);\r\n}\r\n\r\nvoid loop() {\r\ncheckForNetworks();\r\ndisplayIPInfo();\r\ndelay(5000);\r\ndisplayESPInfo();\r\ndelay(5000);\r\ndisplayWiFiInfo();\r\n}\r\n\r\n\/**************\r\nEnd of code\r\n***************\/<\/pre>\n<p>&nbsp;<\/p>\n<pre>\/**************\r\nStart of code\r\nBoard = ESP32 Arduino &gt; ESP32C3 Dev Module, Disabled, Disabled, Huge APP (3MB No OTA\/1MB SPIFFS)\r\n***************\/\r\n\r\n#include &lt;Arduino.h&gt;\r\n#include &lt;U8g2lib.h&gt;\r\n#include &lt;Wire.h&gt;\r\n#include &lt;WiFi.h&gt;\r\n#include &lt;esp_wifi.h&gt;\r\n#include &lt;PubSubClient.h&gt;\r\n#include &lt;math.h&gt;\r\n#include &lt;vector&gt;\r\n\r\nU8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, \/* reset=*\/ U8X8_PIN_NONE);\r\n\r\nWiFiClient espClient;\r\nPubSubClient mqttClient(espClient);\r\n\r\nconst uint32_t scanDelay = 15000; \/\/ Delay between scans (ms)\r\nconst uint32_t scanPeriod = scanDelay \/ 1000; \/\/ Scan cycle (sec)\r\nint scanCount = 0;\r\n\r\nfloat noiseFloor_dBm;\r\nint snr;\r\nString apCounter;\r\n\r\n\/\/ WiFi credentials \r\nstruct WiFiCredentials {\r\nconst char* myssid;\r\nconst char* mypassword;\r\n};\r\n\r\nWiFiCredentials wifi_networks[] = {\r\n{\"ssid1\", \"open\"}, \r\n{\"ssid2\", \"password2\"}, \r\n{\"ssid3\", \"open\"}, \r\n{\"ssid4\", \"password4\"}\r\n};\r\n\r\nconst char* mqtt_server = \"mqtt_broker_ip\";\r\nint mqtt_status = 0;\r\nint mqtt_misCount = 0;\r\nint wifi_status = 0;\r\nint wifi_misCount = 0;\r\n\r\n\/\/ Global variable to accumulate airtime (in microseconds)\r\nvolatile unsigned long totalAirtimeMicroseconds = 0;\r\n\r\n\/\/ Structure to store AP data from a scan\r\nstruct APData {\r\nString ssid;\r\nint rssi;\r\nuint8_t encryption;\r\nint channel;\r\nString bssid;\r\n};\r\n\r\n\/\/ Function Prototypes\r\nString encryptionTypeStr(uint8_t authmode);\r\nvoid mqttCallback(char* topic, byte* message, unsigned int length);\r\nbool tryReconnectMQTT(unsigned long timeout);\r\nvoid safePublish(const char* topic, const String &amp; payload);\r\nvoid updateStats(const std::vector&lt;APData&gt;&amp; apList);\r\nvoid wifiConnect();\r\nvoid checkForNetworks();\r\nvoid signalNoiseRatio(const std::vector&lt;APData&gt;&amp; apList);\r\nvoid channelAirtimeUtilization();\r\nvoid airtime_callback(void *buf, wifi_promiscuous_pkt_type_t type);\r\nfloat getChannelAirtimeUtilization(unsigned long measurementPeriodMs);\r\nfloat utilizationPercent;\r\n\r\n\/\/ Returns a human-readable string for the WiFi encryption type.\r\nString encryptionTypeStr(uint8_t authmode) {\r\nswitch (authmode) {\r\ncase WIFI_AUTH_OPEN: return \"Open\";\r\ncase WIFI_AUTH_WEP: return \"WEP\";\r\ncase WIFI_AUTH_WPA_PSK: return \"WPA\";\r\ncase WIFI_AUTH_WPA2_PSK: return \"WPA2\";\r\ncase WIFI_AUTH_WPA_WPA2_PSK: return \"WPA+WPA2\";\r\ncase WIFI_AUTH_WPA2_ENTERPRISE: return \"WPA2-EAP\";\r\ncase WIFI_AUTH_WPA3_PSK: return \"WPA3\";\r\ncase WIFI_AUTH_WPA2_WPA3_PSK: return \"WPA2+WPA3\";\r\ncase WIFI_AUTH_WAPI_PSK: return \"WAPI\";\r\ndefault: return \"Unknown\";\r\n}\r\n}\r\n\r\n\/\/ Connect to the WiFi network and configure the MQTT client.\r\nvoid wifiConnect() {\r\n\r\nWiFi.mode(WIFI_STA);\r\nSerial.println(\" \");\r\nSerial.println(\"Connecting to WiFi\");\r\nint n = WiFi.scanNetworks();\r\nif (n == 0) {\r\nwifi_status = 0;\r\nreturn;\r\n}\r\nfor (int i = 0; i &lt; sizeof(wifi_networks) \/ sizeof(wifi_networks[0]); i++) {\r\nfor (int j = 0; j &lt; n; j++) {\r\nif (strcmp(wifi_networks[i].myssid, WiFi.SSID(j).c_str()) == 0) {\r\nWiFi.begin(wifi_networks[i].myssid, wifi_networks[i].mypassword);\r\nint attempts = 0;\r\nwhile (WiFi.status() != WL_CONNECTED &amp;&amp; attempts &lt; 25) {\r\ndelay(200);\r\nattempts++;\r\n}\r\nif (WiFi.status() == WL_CONNECTED) {\r\nwifi_status = 1;\r\n} else {\r\nwifi_status = 0;\r\n}\r\n}\r\n}\r\n}\r\n\r\nmqttClient.setServer(mqtt_server, 1883);\r\nmqttClient.setCallback(mqttCallback);\r\n}\r\n\r\n\/\/ Callback for incoming MQTT messages.\r\nvoid mqttCallback(char* topic, byte* message, unsigned int length) {\r\nString messageTemp;\r\nfor (unsigned int i = 0; i &lt; length; i++) {\r\nmessageTemp += (char)message[i];\r\n}\r\n\/\/ Process incoming message if needed.\r\n}\r\n\r\n\/\/ Try to reconnect to the MQTT server for the specified timeout (in ms).\r\n\/\/ Returns true if connection was re-established, false otherwise.\r\nbool tryReconnectMQTT(unsigned long timeout) {\r\nunsigned long startTime = millis();\r\nwhile (!mqttClient.connected() &amp;&amp; (millis() - startTime &lt; timeout)) {\r\nif (mqttClient.connect(\"ESP32-C3-WifiScanner\")) {\r\nmqttClient.subscribe(\"ESP32-C3-WifiScanner\/#\");\r\nreturn true;\r\n}\r\ndelay(500);\r\n}\r\nreturn mqttClient.connected();\r\n}\r\n\r\n\/\/ Publish to MQTT if connected. If not, print the message to Serial.\r\nvoid safePublish(const char* topic, const String &amp; payload) {\r\nif (mqttClient.connected()) {\r\nmqtt_status = 1;\r\nmqttClient.publish(topic, payload.c_str());\r\n} else {\r\nmqtt_status = 0;\r\n}\r\n}\r\n\r\n\/\/ Calculate the signal-to-noise ratio (SNR) using cached scan results.\r\nvoid signalNoiseRatio(const std::vector&lt;APData&gt;&amp; apList) {\r\n\r\nif (WiFi.status() == WL_CONNECTED) {\r\nint rssiConnected = WiFi.RSSI();\r\nint connectedChannel = WiFi.channel();\r\nString connectedBSSID = WiFi.BSSIDstr();\r\n\r\nfloat totalNoise_mW = 0.0;\r\nstd::vector&lt;String&gt; uniqueBSSIDs;\r\n\r\nfor (size_t i = 0; i &lt; apList.size(); i++) {\r\nif (apList[i].bssid == connectedBSSID) continue;\r\nint diff = abs(apList[i].channel - connectedChannel);\r\nfloat weight = 0.0;\r\nif (diff == 0) {\r\nweight = 1.0;\r\n} else if (diff == 1) {\r\nweight = 0.7;\r\n} else if (diff == 2) {\r\nweight = 0.3;\r\n} else {\r\ncontinue;\r\n}\r\nbool duplicate = false;\r\nfor (size_t j = 0; j &lt; uniqueBSSIDs.size(); j++) {\r\nif (uniqueBSSIDs[j] == apList[i].bssid) {\r\nduplicate = true;\r\nbreak;\r\n}\r\n}\r\nif (!duplicate) {\r\nuniqueBSSIDs.push_back(apList[i].bssid);\r\nfloat mW = pow(10, apList[i].rssi \/ 10.0);\r\ntotalNoise_mW += weight * mW;\r\n}\r\n}\r\n\r\nnoiseFloor_dBm = (uniqueBSSIDs.size() &gt; 0) ? (10 * log10(totalNoise_mW)) : -95;\r\nsnr = rssiConnected - noiseFloor_dBm;\r\n}\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Header\", \"==============================================\");\r\nSerial.println(\"==============================================\");\r\nsafePublish(\"ESP32-C3-WifiScanner\/Header\", \"------------------( IP Info )-----------------\");\r\nSerial.println(\"------------------( IP Info )-----------------\");\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/HWAddress\", WiFi.macAddress());\r\nSerial.print(\"HWAddress: \");\r\nSerial.println(WiFi.macAddress());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/IPAddress\", WiFi.localIP().toString());\r\nSerial.print(\"IPAddress: \");\r\nSerial.println(WiFi.localIP().toString());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Subnet\", WiFi.subnetMask().toString());\r\nSerial.print(\"Subnet: \");\r\nSerial.println(WiFi.subnetMask().toString());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/GWAddress\", WiFi.gatewayIP().toString());\r\nSerial.print(\"GWAddress: \");\r\nSerial.println(WiFi.gatewayIP().toString());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/DNS\", WiFi.dnsIP().toString());\r\nSerial.print(\"DNS: \");\r\nSerial.println(WiFi.dnsIP().toString());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/MQTT\", String(mqtt_server));\r\nSerial.print(\"MQTT: \");\r\nSerial.println(mqtt_server);\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Header\", \"-----------------( WiFi Info )----------------\");\r\nSerial.println(\"-----------------( WiFi Info )----------------\");\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/WifiNetwork\", WiFi.SSID());\r\nSerial.print(\"WifiNetwork: \");\r\nSerial.println(WiFi.SSID());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/WifiAP\", WiFi.BSSIDstr());\r\nSerial.print(\"WifiAP: \");\r\nSerial.println(WiFi.BSSIDstr());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/WifiChannel\", String(WiFi.channel()));\r\nSerial.print(\"WifiChannel: \");\r\nSerial.println(WiFi.channel());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/WifiSignal\", String(WiFi.RSSI()));\r\nSerial.print(\"WifiSignal: \");\r\nSerial.println(WiFi.RSSI());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/WifiNoiseFloor\", String(noiseFloor_dBm));\r\nSerial.print(\"WifiNoiseFloor: \");\r\nSerial.println(noiseFloor_dBm);\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/WifiSNR\", String(snr));\r\nSerial.print(\"WifiSNR: \");\r\nSerial.println(snr);\r\n\r\nchannelAirtimeUtilization();\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Header\", \"-----------------( ESP32 Info )---------------\");\r\nSerial.println(\"-----------------( ESP32 Info )---------------\");\r\n}\r\n\r\n\/\/ Publish system stats and each AP's info using the cached scan data.\r\nvoid updateStats(const std::vector&lt;APData&gt;&amp; apList) {\r\ndelay(250);\r\n\r\nif (wifi_status == 1) {\r\nSerial.print(\"WiFi Status: \");\r\nSerial.println(\"Connected\");\r\n}\r\nif (wifi_status == 0) {\r\nSerial.print(\"WiFi Status: \");\r\nSerial.println(\"Disconnected\");\r\nwifi_misCount = wifi_misCount + 1;\r\n}\r\n\r\nif (mqtt_status == 1) {\r\nSerial.print(\"MQTT Status: \");\r\nSerial.println(\"Online\");\r\n}\r\nelse {\r\nSerial.print(\"MQTT Status: \");\r\nSerial.println(\"Offline\");\r\nmqtt_misCount = mqtt_misCount + 1;\r\n}\r\n\r\nmqttClient.publish(\"ESP32-C3-WifiScanner\/WiFiMissCount\", String(wifi_misCount).c_str());\r\nSerial.print(\"WiFi Miss Count: \");\r\nSerial.println(wifi_misCount);\r\n\r\nmqttClient.publish(\"ESP32-C3-WifiScanner\/MQTTMissCount\", String(mqtt_misCount).c_str());\r\nSerial.print(\"MQTT Miss Count: \");\r\nSerial.println(mqtt_misCount);\r\n\r\nscanCount = scanCount + 1;\r\nmqttClient.publish(\"ESP32-C3-WifiScanner\/ScanCount\", String(scanCount).c_str());\r\nSerial.print(\"ScanCount: \");\r\nSerial.println(scanCount);\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/ScanPeriod\", String(scanPeriod));\r\nSerial.print(\"ScanPeriod: \");\r\nSerial.println(scanPeriod);\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Uptime\", String(millis()));\r\nSerial.print(\"Uptime: \");\r\nSerial.println(millis());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/FreeHeapSize\", String(ESP.getFreeHeap()));\r\nSerial.print(\"FreeHeapSize: \");\r\nSerial.println(ESP.getFreeHeap());\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Firmware\", \"ESP32-C3-WifiScanner_ver1\");\r\nSerial.println(\"Firmware: ESP32-C3-WifiScanner_ver1\");\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Notes\", \"Version Releaase Jan 15, 2025\");\r\nSerial.println(\"Notes: Version Releaase Jan 15, 2025\");\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Header\", \"----------------( AP Scan Info )--------------\");\r\nSerial.println(\"----------------( AP Scan Info )--------------\");\r\n\r\nsafePublish(\"ESP32-C3-WifiScanner\/Networks-Found\", String(apList.size()));\r\nSerial.print(\"Networks-Found: \");\r\nSerial.println(apList.size());\r\n\r\napCounter = String(apList.size());\r\n\r\nfor (size_t i = 0; i &lt; apList.size(); i++) {\r\nString displaySSID = (apList[i].ssid != \"\") ? apList[i].ssid : \"*-Hidden-*\";\r\nString encryption = encryptionTypeStr(apList[i].encryption);\r\nString apInfo = String(i + 1) + \" \" + String(apList[i].rssi) + \" \" +\r\napList[i].bssid + \" \" + String(apList[i].channel) + \" \" +\r\ndisplaySSID + \" \" + encryption;\r\nsafePublish(\"ESP32-C3-WifiScanner\/AP-Found\", apInfo);\r\nSerial.print(\"AP-Found: \");\r\nSerial.println(apInfo);\r\ndelay(250);\r\n}\r\n\r\nif (WiFi.status() != WL_CONNECTED &amp;&amp; wifi_status == 0) {\r\nwifiConnect();\r\n}\r\n}\r\n\r\n\/\/ Promiscuous mode callback to calculate airtime per packet.\r\nvoid airtime_callback(void *buf, wifi_promiscuous_pkt_type_t type) {\r\nwifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *) buf;\r\nuint8_t rateCode = pkt-&gt;rx_ctrl.rate;\r\nfloat dataRateMbps = 11; \/\/ Default fallback\r\nswitch(rateCode) {\r\ncase 0: dataRateMbps = 1; break;\r\ncase 1: dataRateMbps = 2; break;\r\ncase 2: dataRateMbps = 5.5; break;\r\ncase 3: dataRateMbps = 11; break;\r\ndefault: break;\r\n}\r\nfloat airtimePerByte = 8.0 \/ dataRateMbps;\r\nuint16_t len = pkt-&gt;rx_ctrl.sig_len;\r\ntotalAirtimeMicroseconds += (unsigned long)(len * airtimePerByte);\r\n}\r\n\r\n\/\/ Measures channel airtime utilization as a percentage.\r\nfloat getChannelAirtimeUtilization(unsigned long measurementPeriodMs) {\r\ntotalAirtimeMicroseconds = 0;\r\nesp_wifi_set_promiscuous_rx_cb(airtime_callback);\r\nesp_wifi_set_promiscuous(true);\r\ndelay(measurementPeriodMs);\r\nesp_wifi_set_promiscuous(false);\r\n\/\/ Re-enable station mode so WiFi can reconnect if needed.\r\nWiFi.mode(WIFI_STA);\r\nunsigned long totalMeasurementMicroseconds = measurementPeriodMs * 1000;\r\nfloat utilization = (totalAirtimeMicroseconds \/ (float)totalMeasurementMicroseconds) * 100.0;\r\nreturn (utilization &gt; 100.0) ? 100.0 : utilization;\r\n}\r\n\r\n\/\/ Publishes the channel airtime utilization.\r\nvoid channelAirtimeUtilization() {\r\nutilizationPercent = getChannelAirtimeUtilization(2000);\r\nsafePublish(\"ESP32-C3-WifiScanner\/RadioUse\", String(utilizationPercent, 1));\r\nSerial.print(\"RadioUse: \");\r\nSerial.println(utilizationPercent, 1);\r\n}\r\n\r\n\/\/ Perform a single WiFi scan and cache the results.\r\nvoid checkForNetworks() {\r\nint numAP = WiFi.scanNetworks(false, true);\r\nstd::vector&lt;APData&gt; apList;\r\nfor (int i = 0; i &lt; numAP; i++) {\r\nAPData ap;\r\nap.ssid = WiFi.SSID(i);\r\nap.rssi = WiFi.RSSI(i);\r\nap.encryption = WiFi.encryptionType(i);\r\nap.channel = WiFi.channel(i);\r\nap.bssid = WiFi.BSSIDstr(i);\r\napList.push_back(ap);\r\n}\r\n\r\ndelay(100);\r\n\r\nif (!mqttClient.connected()) {\r\nif (!tryReconnectMQTT(10000)) {\r\nSerial.println(\"MQTT server offline, proceeding with Serial output.\");\r\n}\r\n}\r\nmqttClient.loop();\r\n\r\n\/\/ Use the same scan data for SNR and AP stats.\r\nsignalNoiseRatio(apList);\r\nupdateStats(apList);\r\n}\r\n\r\nconst unsigned char startlogo [] PROGMEM = {\r\n\r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, \r\n0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0xE0, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, \r\n0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, \r\n0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, \r\n0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, \r\n0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, \r\n0x00, 0xFC, 0xFF, 0xFF, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, \r\n0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0x80, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x03, 0x1F, 0x60, 0xE0, 0xBF, 0xFB, 0xFF, \r\n0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF8, 0xC1, 0x07, 0x3F, 0xF8, \r\n0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xF0, \r\n0xC1, 0x07, 0x1F, 0xF8, 0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xF0, 0xE1, 0x07, 0x1F, 0xF8, 0xE0, 0x07, 0xE0, 0x87, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, 0xC1, 0x87, 0x1F, 0xF8, \r\n0xE0, 0x07, 0xE0, 0x87, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xF0, \r\n0xE3, 0x8F, 0x1F, 0x30, 0xE0, 0x03, 0xF6, 0xDF, 0x1F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0x8F, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xE0, 0xE3, 0xCF, 0x0F, 0x00, \r\n0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xE0, \r\n0xF3, 0x8F, 0x0F, 0x00, 0xE0, 0x87, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xE0, 0xF7, 0xDF, 0x0F, 0x00, 0xE0, 0x07, 0xFF, 0xFF, \r\n0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, 0xF7, 0x9F, 0x07, 0x00, \r\n0xE0, 0x83, 0xFF, 0xFF, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xC0, \r\n0xF7, 0xDF, 0x07, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xF8, 0xE0, 0x03, 0xE0, 0x87, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0xC0, 0xFF, 0xFE, 0x07, 0xFC, \r\n0xE0, 0x03, 0xE0, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, \r\n0xFF, 0xFE, 0x03, 0xF8, 0xE0, 0x07, 0xE0, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0x80, 0xFF, 0xFE, 0x03, 0xFC, 0xE0, 0x07, 0xE0, 0x83, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, 0x7F, 0xFC, 0x03, 0xF8, \r\n0xE0, 0x07, 0xFF, 0x87, 0x0F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x80, \r\n0x7F, 0xFC, 0x03, 0xF8, 0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x83, \r\n0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x7F, 0xFC, 0x01, 0xF8, \r\n0xE0, 0x83, 0xFF, 0x83, 0x1F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, \r\n0x3F, 0xF8, 0x01, 0xFC, 0xE0, 0x07, 0xFF, 0x87, 0x1F, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x7F, 0x00, 0x16, 0xA8, 0x00, 0x58, 0xF0, 0xFF, 0xFF, 0xFF, \r\n0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xF0, 0xFF, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0x07, 0xFE, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, \r\n0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xFE, 0xFF, 0xFF, 0xFF, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, \r\n0x00, 0x00, 0x00, 0x80, 0xDF, 0xEF, 0xF7, 0x7D, 0x80, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, \r\n0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, \r\n0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \r\n0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x45, 0x00, 0x00, 0x00, \r\n0x00, 0x00, 0x00, 0x8A, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, \r\n0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, \r\n0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0x01, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xF0, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, \r\n0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0x03, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, \r\n0xFF, 0xFF, 0xFF, 0xFF,\r\n};\r\n\r\nvoid displayLogo() {\r\nu8g2.clearBuffer();\r\nu8g2.drawXBMP(0, 0, 128, 64, startlogo);\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayVersion() {\r\nu8g2.clearBuffer();\r\n\/\/ u8g2.setFont(u8g2_font_siji_t_6x10);\r\n\/\/ see, https:\/\/github.com\/olikraus\/u8g2\/wiki\/u8g2reference\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"ESP32-C3 WiFi Scanner\");\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"Version 1\");\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"MQTT Enabled\");\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"(c) 2025 - CloudACM\");\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayIPInfo() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"IP \");\r\nu8g2.print(WiFi.localIP().toString());\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"Mac \");\r\nu8g2.print(WiFi.macAddress());\r\nu8g2.setCursor(0, 24);\r\nu8g2.print(\"SN \");\r\nu8g2.print(WiFi.subnetMask().toString());\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"GW \");\r\nu8g2.print(WiFi.gatewayIP().toString());\r\nu8g2.setCursor(0, 42);\r\nu8g2.print(\"DNS \");\r\nu8g2.print(WiFi.dnsIP().toString());\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"RadioUse \");\r\nu8g2.print(utilizationPercent, 1);\r\nu8g2.print(\" %\");\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayWiFiInfo() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"SSID \");\r\nu8g2.print(WiFi.SSID());\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"AP \");\r\nu8g2.print(WiFi.BSSIDstr());\r\nu8g2.setCursor(0, 24);\r\nu8g2.print(\"Channel \");\r\nu8g2.print(WiFi.channel());\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"Signal \");\r\nu8g2.print(WiFi.RSSI());\r\nu8g2.print(\" dBm\");\r\nu8g2.setCursor(0, 42);\r\nu8g2.print(\"Noise \");\r\nu8g2.print(noiseFloor_dBm);\r\nu8g2.print(\" dBm\");\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"SNR \");\r\nu8g2.print(snr);\r\nu8g2.print(\" dB\");\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid displayESPInfo() {\r\nu8g2.clearBuffer();\r\nu8g2.setFont(u8g2_font_boutique_bitmap_9x9_tf);\r\nu8g2.setCursor(0, 6);\r\nu8g2.print(\"Scan Count \");\r\nu8g2.print(scanCount);\r\nu8g2.setCursor(0, 15);\r\nu8g2.print(\"WiFi Miscount \");\r\nu8g2.print(wifi_misCount);\r\nu8g2.setCursor(0, 24);\r\nu8g2.print(\"MQTT Miscount \");\r\nu8g2.print(mqtt_misCount);\r\nu8g2.setCursor(0, 33);\r\nu8g2.print(\"Uptime \");\r\nu8g2.print(millis());\r\nu8g2.print(\" ms\");\r\nu8g2.setCursor(0, 42);\r\nu8g2.print(\"FreeHeap \");\r\nu8g2.print(ESP.getFreeHeap());\r\nu8g2.setCursor(0, 62);\r\nu8g2.print(\"APs Found \");\r\nu8g2.print(apCounter);\r\nu8g2.sendBuffer();\r\n}\r\n\r\nvoid setup() {\r\nSerial.begin(115200);\r\ndelay(50);\r\n\r\nWire.begin(6, 10);\r\nu8g2.begin();\r\nu8g2.setFlipMode(1);\r\ndisplayLogo();\r\ndelay(50);\r\n\r\nwifiConnect();\r\ndelay(1000);\r\ndisplayVersion();\r\ndelay(2500);\r\n}\r\n\r\nvoid loop() {\r\ncheckForNetworks();\r\ndisplayIPInfo();\r\ndelay(5000);\r\ndisplayESPInfo();\r\ndelay(5000);\r\ndisplayWiFiInfo();\r\ndelay(5000);\r\n}\r\n\r\n\/**************\r\nEnd of code\r\n***************\/<\/pre>\n<p>The code functions can be modified as needed.\u00a0 Creating a logo image can be done with GIMP or Imagemagick by resizing and saving it in a XBM ascii format.\u00a0 Since the displays are monochromatic, not greyscale, dithering can be done to give a shading effect.\u00a0 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.\u00a0 Don&#8217;t get scammed by an online document converter, here&#8217;s a warning from the <a href=\"https:\/\/www.fbi.gov\/contact-us\/field-offices\/denver\/news\/fbi-denver-warns-of-online-file-converter-scam\">FBI Denver Field Office<\/a>.<\/p>\n<p>Here is the IM command used for the logo in the code above.\u00a0 The resulting XBM file can be opened in a text editor to copy the section that goes into the &#8220;startlogo&#8221; PROGMEM array.<\/p>\n<pre>convert input.png -negate -monochrome output.xbm<\/pre>\n<p>Since the MQTT functions were included, Node-Red was leveraged to provide a dashboard widget and to save the results to a log file.<\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Top_ESP32-WifiScanner.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4848 size-full\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Top_ESP32-WifiScanner.png\" alt=\"\" width=\"301\" height=\"477\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Top_ESP32-WifiScanner.png 301w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Top_ESP32-WifiScanner-189x300.png 189w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Top_ESP32-WifiScanner-170x270.png 170w\" sizes=\"auto, (max-width: 301px) 100vw, 301px\" \/><\/a><\/p>\n<p><a href=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Bottom_ESP32-WifiScanner.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-4849 size-full\" src=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Bottom_ESP32-WifiScanner.png\" alt=\"\" width=\"305\" height=\"602\" srcset=\"https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Bottom_ESP32-WifiScanner.png 305w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Bottom_ESP32-WifiScanner-152x300.png 152w, https:\/\/www.cloudacm.com\/wp-content\/uploads\/2025\/03\/Node-Red_Widget-Bottom_ESP32-WifiScanner-137x270.png 137w\" sizes=\"auto, (max-width: 305px) 100vw, 305px\" \/><\/a><\/p>\n<p>This is the Node Red flow details that displays the readings in a console window on the dashboard.<\/p>\n<pre>1. Use a MQTT In Node and subscribe to the topic with ...\/AP-Found\r\n2. Pass it to a Function Node titled \"Message Stacker v2\" with the following code:\r\n\r\n\/\/ Time threshold to identify a new burst (in milliseconds)\r\nconst BURST_THRESHOLD = 5000; \/\/ 5 seconds\r\n\r\n\/\/ Get the last message timestamp and messages from flow context\r\nvar lastMessageTime = flow.get('lastMessageTime') || 0;\r\nvar currentTime = new Date().getTime();\r\nvar messages = flow.get('messages') || '';\r\n\r\n\/\/ If time since last message exceeds threshold, consider it a new burst\r\nif (currentTime - lastMessageTime &gt; BURST_THRESHOLD) {\r\nmessages = ''; \/\/ Clear the messages buffer for new burst\r\n}\r\n\r\n\/\/ Add new message to buffer\r\nmessages += msg.payload + \"\\n\";\r\n\r\n\/\/ Update flow context\r\nflow.set('lastMessageTime', currentTime);\r\nflow.set('messages', messages);\r\n\r\n\/\/ Set message payload\r\nmsg.payload = messages;\r\n\r\nreturn msg;\r\n\r\n3. Pass that to a Template Node with the following template code:\r\n\r\n&lt;style&gt;\r\n#myText {\r\nheight: 200px;\r\noverflow-y: scroll;\r\nbackground-color: #242424;\r\ncolor: #d29200;\r\nfont-size: 12px;\r\nfont-family: monospace;\r\nwhite-space: pre-wrap;\r\n}\r\n&lt;\/style&gt;\r\n\r\n&lt;div id=\"container\"&gt;\r\n&lt;div id=\"myText\"&gt;{{msg.payload}}&lt;\/div&gt;\r\n&lt;\/div&gt;\r\n\r\n&lt;script&gt;\r\nvar container = document.getElementById(\"container\");\r\ncontainer.scrollTop = container.scrollHeight;\r\n\r\n\/\/ Whenever new data is added to the container, scroll to the bottom\r\ncontainer.addEventListener(\"DOMNodeInserted\", function () {\r\ncontainer.scrollTop = container.scrollHeight;\r\n});\r\n&lt;\/script&gt;<\/pre>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post will cover the use of a SSD1306 OLED display with Espressif based modules.\u00a0 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.\u00a0 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&#8230;<\/p>\n<p class=\"read-more\"><a class=\"btn btn-default\" href=\"https:\/\/www.cloudacm.com\/?p=4833\"> Read More<span class=\"screen-reader-text\">  Read More<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-4833","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4833","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=4833"}],"version-history":[{"count":18,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4833\/revisions"}],"predecessor-version":[{"id":4835,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=\/wp\/v2\/posts\/4833\/revisions\/4835"}],"wp:attachment":[{"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=4833"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4833"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cloudacm.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4833"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}