ESP32 based YouTube Subscriber Counter
After a suggestion by a subscriber I have built a subscriber counter! Go on, push the button - you know you want to!
The code is below, but honestly you will want to download all the code, examples and other info from my github site.
/* PROJECT: OneCircuit YouTube Subscriber Tracker AUTHOR: OneCircuit and Gemini AI Sat 07 Mar 2026 12:39:22 AEDT https://www.youtube.com/@onecircuit-as https://onecircuit.blogspot.com/ https://github.com/bovineck/ COMPILE SETTINGS: Board: ESP32 Dev Module (or your specific variant) Partition Scheme: Minimal SPIFFS (1.9MB APP with OTA) <-- CRITICAL * [SECTION 1] - Dependencies Loads the core libraries for WiFi, Web Server hosting, and JSON parsing. Ref: https://github.com/bblanchon/ArduinoJson (Excellent documentation for data parsing) * [SECTION 2] - Initial Configuration These strings are only used if the device has never been configured before. Once saved via the dashboard, the device will prioritize the Flash memory settings. * [SECTION 3] - Auto-Hardware Detection Uses compiler "flags" to detect which ESP32 board you are using and automatically assigns the correct SPI pins for the LED matrix. No manual pin editing required. Ref: https://docs.espressif.com/projects/arduino-esp32/en/latest/api/gpio.html * [SECTION 4] - Global Objects & State Sets up the Parola display engine, the Web Server on port 80, and the Preferences "Flash" storage. Also contains the custom pixel data for the "blinking dog" animation. * [SECTION 5] - CSS Styling The "Visual DNA" of the dashboard. Uses CSS Flexbox to ensure the interface looks professional on both desktop monitors and mobile phone screens. * [SECTION 6] - Dashboard Logic The main HTML engine. It reads the current status from the hardware and generates the interactive "Live" dashboard that the user sees in their browser. * [SECTION 7] - Utility Routes Handles the "behind the scenes" web requests for saving settings, showing the wiring map, and triggering the secure OTA (Over-The-Air) firmware update process. Ref: https://github.com/espressif/arduino-esp32/tree/master/libraries/Update * [SECTION 8] - YouTube API Engine The "Heart" of the device. Connects to Google's servers, requests your stats, and updates the local display. It also manages the Sleep/Wake power-saving schedule. Ref: https://developers.google.com/youtube/v3/docs/channels/list * [SECTION 9] - System Setup Runs once at power-on. It wakes up the display, tries to find your WiFi, and if it fails, creates a "Rescue Hotspot" (AP Mode) so you can fix the settings. * [SECTION 10] - Execution Loop The non-stop worker. Animates the LED matrix, checks the YouTube API every 60 seconds, and keeps the web server responsive to your clicks. */ // [SECTION 1] - Dependencies #include <WiFi.h> #include <ESPmDNS.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <MD_Parola.h> #include <MD_MAX72xx.h> #include <SPI.h> #include <WebServer.h> #include <Update.h> #include <Preferences.h> #include <DNSServer.h> #include "time.h" // [SECTION 2] - Initial Configuration const char* INITIAL_SSID = "Your WiFi SSID"; const char* INITIAL_PASS = "Your WiFi Password"; const char* INITIAL_CNAME = "Your Channel Name"; const char* INITIAL_LOC = "Your Location"; const char* PROJECT_DESC = "A Universal YouTube Subscriber Tracker for ESP32. Featuring auto-hardware detection, OTA updates, and a mobile-friendly dashboard."; // [SECTION 3] - Auto-Hardware Detection #if defined(ARDUINO_SEEED_XIAO_ESP32C6) || defined(ARDUINO_XIAO_ESP32C6) || defined(ESP32C6) #define CS_PIN 1 #define MOSI_PIN 21 #define CLK_PIN 19 const char* HW_NAME = "ESP32 C6"; #elif defined(ARDUINO_SEEED_XIAO_ESP32C3) || defined(ARDUINO_XIAO_ESP32C3) || defined(ESP32C3) #define CS_PIN 3 #define MOSI_PIN 10 #define CLK_PIN 8 const char* HW_NAME = "ESP32 C3"; #elif defined(ARDUINO_SEEED_XIAO_ESP32S3) || defined(ARDUINO_XIAO_ESP32S3) || defined(ESP32S3) #define CS_PIN 1 #define MOSI_PIN 9 #define CLK_PIN 7 const char* HW_NAME = "ESP32 S3"; #elif defined(ARDUINO_ESP32C3_DEV) || defined(ARDUINO_ESP32C3_SUPERMINI) #define CS_PIN 7 #define MOSI_PIN 6 #define CLK_PIN 4 const char* HW_NAME = "ESP32-C3 SuperMini"; #elif defined(ARDUINO_SEEED_XIAO_ESP32) #define CS_PIN 5 #define MOSI_PIN 23 #define CLK_PIN 18 const char* HW_NAME = "Seeed XIAO ESP32"; #elif defined(ARDUINO_FEATHER_ESP32) #define CS_PIN 33 #define MOSI_PIN 18 #define CLK_PIN 5 const char* HW_NAME = "Adafruit Feather ESP32"; #elif defined(ARDUINO_ESP32S2_DEV) #define CS_PIN 15 #define MOSI_PIN 35 #define CLK_PIN 36 const char* HW_NAME = "ESP32-S2 DevKit"; #elif defined(ARDUINO_ESP32S3_DEV) #define CS_PIN 10 #define MOSI_PIN 11 #define CLK_PIN 12 const char* HW_NAME = "ESP32-S3 DevKit"; #elif defined(ARDUINO_ESP32_DEV) || defined(ESP32) #define CS_PIN 5 #define MOSI_PIN 23 #define CLK_PIN 18 const char* HW_NAME = "DevKit V1 (Standard)"; #else #define CS_PIN 5 #define MOSI_PIN 23 #define CLK_PIN 18 const char* HW_NAME = "Generic ESP32"; #endif // [SECTION 4] - Global Objects & State #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 8 MD_Parola myDisplay = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); WebServer server(80); Preferences prefs; DNSServer dnsServer; String apiKey, channelId, channelName, location; uint8_t sleepHour, wakeHour; bool isAPMode = false, isBlinking = false; long currentSubs = 0; uint32_t lastUpdate = 0, lastBlink = 0; char displayMsg[48] = "READY"; // keep small so the display doesn't choke up String lastTimeCheck = "Never"; unsigned long wifiTimeout = 0; uint8_t curTR = 10; uint8_t scrollState = 0; void handleRoot(); void handleSave(); void handleHelp(); void handlePins(); void updateYouTubeData(); void handleUpdate(); // [SECTION 5] - CSS Styling String getCSS() { String css = "<style>body{font-family:'Segoe UI',sans-serif; background:#1e293b; color:#f1f5f9; padding:15px; max-width:600px; margin:auto;} "; css += ".box{background:#334155; padding:18px; border-radius:12px; margin-bottom:20px; border-left:6px solid #38bdf8; "; css += "box-shadow: 0 10px 15px -3px rgba(0,0,0,0.4), 0 4px 6px -2px rgba(0,0,0,0.2); box-sizing:border-box;} "; css += ".faq-box{background:#334155; padding:18px; border-radius:12px; margin-bottom:15px; border-left:6px solid #fbbf24; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.4);} "; css += "input{display:block; width:100%; padding:14px; margin:10px 0; border-radius:8px; border:1px solid #475569; background:#1e293b; color:#fff; box-sizing: border-box;} "; css += "input:focus{outline:none; border-color:#38bdf8; box-shadow:0 0 0 2px rgba(56,189,248,0.2);} "; css += ".btn{padding:14px; border-radius:8px; color:#fff; cursor:pointer; text-decoration:none; display:inline-block; font-weight:bold; margin-top:10px; text-align:center; border:none; transition:0.2s;} "; css += ".btn:active{transform:scale(0.98);} .save{background:#10b981; width:100%; font-size:1.1em; margin-bottom:10px; box-shadow:0 4px 6px rgba(0,0,0,0.2);} "; css += ".hbtn{background:#f59e0b; flex:1; margin:5px;} .pbtn{background:#64748b; flex:1; margin:5px;} .reboot{background:#ef4444; flex:1; margin:5px;} "; css += ".pass-toggle{background:#475569; font-size:0.8em; padding:8px 12px; margin-bottom:15px; width:auto;} "; css += ".footer-nav{display:flex; flex-wrap:wrap; gap:10px; margin-top:25px; border-top:1px solid #475569; padding-top:15px;} "; css += ".footer-nav .btn{flex:1 1 140px; margin:0;} "; css += "a{color:#38bdf8; text-decoration:none;} table{width:100%; border-collapse: collapse; background:#334155; border-radius:8px; overflow:hidden;} "; css += "th, td{padding:12px; border-bottom:1px solid #475569; text-align:left;} th{background:#1e293b; color:#38bdf8;} .active{background:#0c4a6e; font-weight:bold;}</style>"; return css; } // [SECTION 6] - Dashboard Logic void handleRoot() { prefs.begin("config", true); String curSSID = prefs.getString("ssid", INITIAL_SSID); String curPASS = prefs.getString("pass", INITIAL_PASS); String curCN = prefs.getString("cname", INITIAL_CNAME); String curLO = prefs.getString("loc", INITIAL_LOC); String curAP = prefs.getString("api", ""); String curCI = prefs.getString("cid", ""); int curSL = prefs.getUChar("sleep", 23); int curWA = prefs.getUChar("wake", 7); curTR = prefs.getUChar("trigger", 10); prefs.end(); String html = "<html><head><title>Dashboard</title><meta name='viewport' content='width=device-width, initial-scale=1'>" + getCSS(); html += "<script>function togPass(){var p=document.getElementById('p'); p.type=(p.type==='password')?'text':'password';}</script></head><body>"; html += "<h1>OneCircuit YouTube Subscriber Tracker</h1>"; html += "<div class='box'><small>" + String(PROJECT_DESC) + "</small>"; html += "<div style='margin-top:12px; border-top:1px solid #475569; padding-top:10px;'>"; html += "<div style='font-size:0.8em; color:#94a3b8; margin-bottom:8px;'>Links:</div>"; html += "<div style='display:flex; flex-wrap:wrap; gap:10px;'>"; html += "<a href='https://www.youtube.com/@onecircuit-as' target='_blank' style='color:#38bdf8; font-size:0.85em; text-decoration:none;'>[ YouTube ]</a>"; html += "<a href='https://onecircuit.blogspot.com/' target='_blank' style='color:#38bdf8; font-size:0.85em; text-decoration:none;'>[ Blog ]</a>"; html += "<a href='https://github.com/bovineck/' target='_blank' style='color:#38bdf8; font-size:0.85em; text-decoration:none;'>[ GitHub ]</a>"; html += "</div></div></div>"; String statusColor = (WiFi.status() == WL_CONNECTED) ? "#10b981" : "#ef4444"; // Green if WiFi OK, else Red String statusText = (WiFi.status() == WL_CONNECTED) ? "Online" : "Offline"; html += "<div class='box' style='border-left-color: " + statusColor + "; text-align: center; background: #0f172a; padding: 20px;'>"; html += "<div style='display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 15px;'>"; html += "<span style='height: 10px; width: 10px; background-color: " + statusColor + "; border-radius: 50%; display: inline-block; box-shadow: 0 0 8px " + statusColor + ";'></span>"; html += "<span style='font-size: 0.75em; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px;'>" + statusText + "</span>"; html += "</div>"; html += "<div style='font-size: 1.1em; font-weight: bold; color: #38bdf8; margin-bottom: 5px; letter-spacing: 1px;'>" + curCN + " : " + curLO + "</div>"; html += "<div style='font-size: 2.8em; font-weight: 800; color: #fff; margin: 5px 0;'>" + String(currentSubs) + "</div>"; html += "<div style='font-size: 0.8em; color: #94a3b8;'>Last Sync: " + lastTimeCheck + "</div>"; html += "<div style='margin-top: 15px; padding-top: 10px; border-top: 1px solid #1e293b; font-family: monospace; font-size: 0.85em; color: #64748b;'>"; html += "IP: " + WiFi.localIP().toString() + "<br>"; html += "URL: <a href='http://onecircuit.local' style='color: #38bdf8;'>http://onecircuit.local</a>"; html += "</div>"; html += "<a href='/refresh' class='btn' style='margin-top: 15px; text-decoration: none; display: block; background: #059669; color: white; font-weight: bold; padding: 10px; border-radius: 8px;'>Check YouTube Now</a>"; html += "</div>"; html += "<form action='/save' method='POST'>"; html += "<h3>Identity</h3>"; html += "<input name='cn' placeholder='Your Channel Name' value='" + curCN + "'>"; html += "<input name='lo' placeholder='Your Location' value='" + curLO + "'>"; html += "<h3>Network</h3>"; html += "<input name='ss' placeholder='Your Wifi SSID' value='" + curSSID + "'>"; html += "<input type='password' id='p' name='pa' placeholder='Your WiFi Password' value='" + curPASS + "'>"; html += "<button type='button' class='btn pass-toggle' onclick='togPass()'>Show/Hide Password</button>"; html += "<h3>Your YouTube API Key</h3>"; html += "<input name='ap' placeholder='Your Channel API Key' value='" + curAP + "'>"; html += "<h3>YouTube Channel ID</h3>"; html += "<input name='ci' placeholder='Your Channel ID' value='" + curCI + "'>"; html += "<h3>Schedule</h3>"; html += "Sleep (0-23): <input type='number' name='sl' value='" + String(curSL) + "'>"; html += "Wake (0-23): <input type='number' name='wa' value='" + String(curWA) + "'>"; html += "<h3>Settings</h3>"; html += "Scroll Cycle (Mins): <input type='number' name='tr' value='" + String(curTR) + "'>"; html += "<input type='submit' value='Save & Preview Receipt' class='btn save'>"; html += "</form>"; html += "<div class='footer-nav'>"; html += "<a href='/help' class='btn hbtn'>Help</a>"; html += "<a href='/pins' class='btn pbtn'>Pin Wiring</a>"; html += "<a href='/update' class='btn pbtn' style='background:#6366f1;'>Update Firmware</a>"; html += "</div>"; html += "<a href='/reboot_exec' class='btn reboot' style='width:100%; box-sizing:border-box; margin-top:10px;' onclick='return confirm(\"Reboot the device now?\")'>System Reboot</a>"; html += "</body></html>"; server.send(200, "text/html", html); } // [SECTION 7] - Utility Routes void handleUpdate() { String html = "<html><head><title>System Update</title><meta name='viewport' content='width=device-width, initial-scale=1'>" + getCSS(); html += "<script>"; html += "function startUpdate() {"; html += " document.getElementById('updater').style.display='none';"; html += " document.getElementById('status').style.display='block';"; html += "}"; html += "</script></head><body>"; html += "<h1>Firmware Update</h1>"; html += "<div id='status' class='box' style='display:none; text-align:center; border-left-color:#fbbf24;'>"; html += "<h3>Flash in Progress...</h3>"; html += "<p>Uploading binary to ESP32. <b>Do not power off.</b></p>"; html += "<div style='margin:20px; font-weight:bold; color:#fbbf24;'>[ UPLOADING... ]</div></div>"; html += "<div id='updater' class='box' style='border-left-color: #ef4444;'>"; html += "<h3>Select .bin File</h3>"; html += "<form method='POST' action='/update_exec' enctype='multipart/form-data' onsubmit='startUpdate()'>"; html += "<input type='file' name='update' accept='.bin' style='padding:10px; border: 1px dashed #475569; background: #1e293b; margin-bottom:15px;'>"; html += "<input type='submit' value='Begin Update' class='btn reboot' style='width:100%;'>"; html += "</form></div>"; html += "<a href='/' class='btn pbtn' style='width:100%; box-sizing:border-box;'>← Cancel</a>"; html += "</body></html>"; server.send(200, "text/html", html); } /// Saves settings and generates printable receipt. void handleSave() { // 1. Capture and Save Settings to Flash String s_cn = server.arg("cn"), s_lo = server.arg("lo"), s_ss = server.arg("ss"), s_ap = server.arg("ap"), s_ci = server.arg("ci"), s_tr = server.arg("tr"), s_sl = server.arg("sl"), s_wa = server.arg("wa"); prefs.begin("config", false); prefs.putString("ssid", s_ss); prefs.putString("pass", server.arg("pa")); prefs.putString("cname", s_cn); prefs.putString("loc", s_lo); prefs.putString("api", s_ap); prefs.putString("cid", s_ci); prefs.putUChar("sleep", (uint8_t)s_sl.toInt()); prefs.putUChar("wake", (uint8_t)s_wa.toInt()); prefs.putUChar("trigger", (uint8_t)s_tr.toInt()); curTR = (uint8_t)s_tr.toInt(); lastUpdate = millis(); prefs.end(); // 2. Build the HTML String String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>" + getCSS() + "</head><body>"; // Receipt Box html += "<div class='box' style='background:#f8fafc; color:#1e293b; border-left-color:#10b981; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.5);'>"; html += "<h1>Config Receipt</h1><p>Verify details before rebooting.</p>"; auto row = [&](String k, String v) { html += "<div style='border-bottom:1px solid #e2e8f0; padding:10px 0;'><b>" + k + ":</b> " + v + "</div>"; }; row("Channel Name", s_cn); row("Channel ID", s_ci); row("Wi-Fi SSID", s_ss); row("Scroll Cycle", s_tr + " minutes"); row("Schedule", "Sleep at " + s_sl + ":00 / Wake at " + s_wa + ":00"); html += "<div style='padding:10px 0;'><b>YouTube API Key:</b><br><small style='color:#64748b; word-break:break-all;'>" + s_ap + "</small></div>"; // Action Buttons html += "<div style='display:flex; flex-direction:column; gap:10px; margin-top:15px;'>"; html += "<button class='btn pbtn' onclick='window.print()'>1. Print / Save PDF</button>"; html += "<a href='/reboot_exec' class='btn reboot'>2. Commit & Reboot Device</a>"; html += "<a href='/' class='btn hbtn'>← Return to Edit</a>"; html += "</div></div>"; // 3. System Health Check uint32_t freeHeap = ESP.getFreeHeap(); uint32_t sketchSize = ESP.getSketchSize(); html += "<div class='box' style='background:#0f172a; border-left-color:#38bdf8; margin-top:20px; font-family:monospace; font-size:0.85em;'>"; html += "<h3 style='color:#38bdf8; margin-top:0;'>System Health</h3>"; html += "Free RAM: " + String(freeHeap / 1024) + " KB<br>"; html += "Binary Size: " + String(sketchSize / 1024) + " KB / 1920 KB"; html += "</div>"; // 4. Danger Zone html += "<div class='box' style='border-left-color: #ef4444; margin-top: 40px; background:#1e293b;'>"; html += "<h2>Danger Zone</h2><p style='font-size: 0.8em; color: #94a3b8;'>Wipe all WiFi credentials and API keys.</p>"; html += "<a href='/wipe_exec' class='btn reboot' style='width:100%;' onclick=\"return confirm('Are you sure?')\">Factory Reset Device</a>"; html += "</div>"; html += "</body></html>"; server.send(200, "text/html", html); } /// Displays FAQ and partitioning guide. void handleHelp() { String html = "<html><head><meta charset='UTF-8'><title>FAQ</title><meta name='viewport' content='width=device-width, initial-scale=1'>" + getCSS() + "</head><body>"; html += "<h1>User Guide</h1>"; // 1. OTA Troubleshooting html += "<div class='faq-box'><h2>OTA Update Information</h2>"; html += "<b>1. Partition Scheme:</b> In Arduino IDE, please select: <br><i>Tools -> Partition Scheme -> Minimal SPIFFS. e.g. 1.9MB APP with OTA</i>.<br><br>"; html += "Ref: <a href='https://docs.espressif.com/projects/arduino-esp32/en/latest/tutorials/partition_table.html' target='_blank'>Espressif Partition Guide</a>.<br><br>"; html += "<b>2. The right file:</b> Upload <b>YourVersion.ino.bin</b>. <span style='color:#ef4444;'>⚠️ DO NOT upload 'YourVersion.merged.bin' or 'YourVersion.bootloader.bin'</span> ... they will be too large for OTA and will likely cause an 'UPDATE FAIL' message.</div>"; // 2. YouTube API html += "<div class='faq-box'><h2>YouTube API Key</h2>Enable 'YouTube Data API v3' at the <a href='https://console.cloud.google.com/' target='_blank'>Google Cloud Console</a>. Make sure your key has no IP restrictions that might block the ESP32.</div>"; // 3. Channel ID html += "<div class='faq-box'><h2>Channel ID</h2>Your ID starts with 'UC...'. You can find it in your <a href='https://www.youtube.com/account_advanced' target='_blank'>YouTube Advanced Settings</a> or by clicking your profile icon > Settings > Advanced.</div>"; // 4. Scroll Cycle html += "<div class='faq-box'><h2>Scroll Cycle</h2>The number of minutes between info scrolls. The display will show the Subscriber count primarily, then cycle through Location and Local Time based on this setting.</div>"; // 5. Sleep & Wake html += "<div class='faq-box'><h2>Sleep & Wake Hr</h2>Uses 24-hour format (0-23). To extend the life of your 32x8 LED matrices (and save power!), the display will turn off during the Sleep Hour and resume at the Wake Hour.</div>"; // 6. AP Mode html += "<div class='faq-box'><h2>AP Mode</h2>If the device cannot connect to your Wi-Fi, it creates its own hotspot: 'OneCircuit-Config'. <br>Connect with your phone and browse to 192.168.4.1 to update settings.</div>"; html += "<a href='/' class='btn hbtn' style='width:100%; box-sizing:border-box;'>← Back to Dashboard</a>"; html += "</body></html>"; server.send(200, "text/html", html); } /// Displays wiring table. void handlePins() { String html = "<html><head><title>Wiring Map</title><meta name='viewport' content='width=device-width, initial-scale=1'>" + getCSS() + "</head><body>"; html += "<h1>Wiring Map</h1><div class='box'>Detected: <b>" + String(HW_NAME) + "</b></div><table><tr><th>Model</th><th>CS</th><th>MOSI</th><th>CLK</th></tr>"; auto addR = [&](String n, int c, int m, int cl) { html += "<tr class='" + String(n == HW_NAME ? "active" : "") + "'><td>" + n + "</td><td>" + String(c) + "</td><td>" + String(m) + "</td><td>" + String(cl) + "</td></tr>"; }; addR("Seeed XIAO C6", 1, 21, 19); addR("Seeed XIAO C3", 3, 10, 8); addR("Seeed XIAO S3", 1, 9, 7); addR("ESP32-C3 SuperMini", 7, 6, 4); addR("Seeed XIAO ESP32", 5, 23, 18); addR("Adafruit Feather", 33, 18, 5); addR("ESP32-S2 DevKit", 15, 35, 36); addR("ESP32-S3 DevKit", 10, 11, 12); addR("DevKit V1 (30p)", 5, 23, 18); addR("Generic ESP32", 5, 23, 18); html += "</table><p>Official docs: <a href='https://docs.espressif.com/projects/esp-idf/en/latest/esp32/hw-reference/index.html' target='_blank'>Espressif Hardware Reference</a>.</p>"; html += "<a href='/' class='btn pbtn'>← Back</a></body></html>"; server.send(200, "text/html", html); } /// Persistent redirect countdown page. void sendTransitionPage(String title, String msg, int duration) { String html = "<html><head><meta name='viewport' content='width=device-width, initial-scale=1'>"; html += "<script>var seconds = " + String(duration) + "; function countdown(){seconds--; var el=document.getElementById('timer'); if(el) el.textContent=seconds; "; html += "if(seconds<=0){ document.getElementById('status-msg').innerHTML='<h2 style=\"color:#f59e0b;\">Action Complete</h2><p>Waiting for device to reconnect...</p><p style=\"font-size:0.8em; color:#94a3b8;\">If this takes too long, please press the <b>RESET</b> button, or cycle the power to the ESP32.</p><a href=\"/\" style=\"color:#38bdf8; text-decoration:none;\">Return to Dashboard</a>'; "; html += "setInterval(function(){window.location.href='/';},2000); } else {setTimeout(countdown,1000);}} setTimeout(countdown,500);</script>"; html += "<style>body{background:#0f172a;color:white;font-family:sans-serif;display:flex;flex-direction:column;align-items:center;justify-content:center;height:100vh;margin:0;text-align:center;padding:20px;} .loader{border:4px solid #1e293b;border-top:4px solid #38bdf8;border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;margin:0 auto 20px auto;} @keyframes spin{0%{transform:rotate(0deg);}100%{transform:rotate(360deg);}}</style></head><body>"; html += "<div id='status-msg'><div class='loader'></div><h2>" + title + "</h2><p style='color:#94a3b8;'>" + msg + " in <span id='timer' style='color:#38bdf8; font-weight:bold;'>" + String(duration) + "</span> seconds...</p></div></body></html>"; server.sendHeader("Connection", "close"); server.send(200, "text/html", html); } // [SECTION 8] - YouTube API Engine void updateYouTubeData() { Serial.println("\n>>> [SYNC START]"); if (WiFi.status() != WL_CONNECTED) return; HTTPClient http; http.setTimeout(3000); String url = "https://www.googleapis.com/youtube/v3/channels?part=statistics&id=" + channelId + "&key=" + apiKey; http.begin(url); int httpCode = http.GET(); if (httpCode == 200) { StaticJsonDocument<1536> doc; deserializeJson(doc, http.getString()); const char* subString = doc["items"][0]["statistics"]["subscriberCount"]; if (subString) { currentSubs = atol(subString); struct tm timeinfo; if (getLocalTime(&timeinfo)) { char timeStringBuff[50]; strftime(timeStringBuff, sizeof(timeStringBuff), "%H:%M (%d %b)", &timeinfo); lastTimeCheck = String(timeStringBuff); } Serial.println(">>> DATA FETCHED SUCCESSFULLY."); } } else { Serial.printf(">>> API ERROR: %d\n", httpCode); } http.end(); delay(100); Serial.println(">>> [SYNC COMPLETE]"); } // [SECTION 9] - System Setup void setup() { // 1. Start Serial Debugging Serial.begin(115200); delay(1000); Serial.println("\n--- DIAGNOSTIC BOOT ---"); Serial.print("Hardware Detected: "); Serial.println(HW_NAME); // 2. Load Configuration from Flash prefs.begin("config", false); channelName = prefs.getString("cname", INITIAL_CNAME); location = prefs.getString("loc", INITIAL_LOC); apiKey = prefs.getString("api", ""); channelId = prefs.getString("cid", ""); sleepHour = prefs.getUChar("sleep", 23); wakeHour = prefs.getUChar("wake", 7); curTR = prefs.getUChar("trigger", 10); // Load the saved value on boot String cSSID = prefs.getString("ssid", INITIAL_SSID); String cPASS = prefs.getString("pass", INITIAL_PASS); prefs.end(); // 3. Initialize Hardware Display pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); // Ensure the display is "Deselected" to start SPI.begin(CLK_PIN, -1, MOSI_PIN, CS_PIN); SPI.setFrequency(200000); // slow SPI for stability myDisplay.begin(); myDisplay.setIntensity(2); myDisplay.displayClear(); myDisplay.print("READY"); Serial.println("Display Initialized."); // 4. Robust WiFi Handshake WiFi.persistent(false); WiFi.disconnect(true); delay(200); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); // Disable WiFi sleep to prevent the router from dropping the ESP32 WiFi.setSleep(WIFI_PS_NONE); Serial.print("Connecting to WiFi: "); Serial.println(cSSID); WiFi.begin(cSSID.c_str(), cPASS.c_str()); // Wait 15 seconds for connection with visual feedback unsigned long wifiTimeout = millis(); while (WiFi.status() != WL_CONNECTED && millis() - wifiTimeout < 15000) { delay(500); Serial.print("."); myDisplay.print("."); } // 5. Post-Connection Logic if (WiFi.status() == WL_CONNECTED) { Serial.println("\nCONNECTED!"); Serial.print("IP: "); Serial.println(WiFi.localIP()); // Start mDNS: http://onecircuit.local if (MDNS.begin("onecircuit")) { Serial.println("mDNS responder started."); } // Scroll IP Address across the matrix once if (WiFi.status() == WL_CONNECTED) { String ipAddr = WiFi.localIP().toString(); myDisplay.displayClear(); myDisplay.displayText(ipAddr.c_str(), PA_CENTER, 80, 2000, PA_SCROLL_LEFT, PA_SCROLL_UP); while (!myDisplay.displayAnimate()) { yield(); } // FORCE START STATE: Ensure the first thing after IP is the Name snprintf(displayMsg, 48, "%s", channelName.c_str()); scrollState = 0; } // Sync Time for Sleep/Wake Schedule configTzTime("AEST-10AEDT,M10.1.0,M4.1.0/3", "pool.ntp.org"); updateYouTubeData(); } else { // Access Point Fallback (Rescue Mode) Serial.println("\nCONNECTION FAILED. Starting AP Mode."); isAPMode = true; WiFi.mode(WIFI_AP); WiFi.softAP("OneCircuit-Config", NULL, 1, 0, 4); dnsServer.start(53, "*", WiFi.softAPIP()); myDisplay.displayText("AP MODE", PA_CENTER, 0, 0, PA_PRINT, PA_NO_EFFECT); } // 6. Web Server Routes server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.on("/help", handleHelp); server.on("/pins", handlePins); server.on("/refresh", []() { updateYouTubeData(); server.sendHeader("Location", "/"); server.send(303); }); server.on("/wipe_exec", []() { prefs.begin("config", false); prefs.clear(); prefs.end(); sendTransitionPage("Factory Reset", "Wiping NVS and restarting...", 20); delay(1000); ESP.restart(); }); server.on("/reboot_exec", []() { sendTransitionPage("System Reboot", "Restarting device...", 20); delay(1000); WiFi.disconnect(true); WiFi.mode(WIFI_OFF); delay(2500); ESP.restart(); }); server.on("/update", handleUpdate); server.on( "/update_exec", HTTP_POST, []() { if (Update.hasError()) { server.send(200, "text/html", "Update Failed. Check Serial Monitor."); } else { sendTransitionPage("Update Successful", "Rebooting into new firmware...", 60); delay(2000); ESP.restart(); } }, []() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { myDisplay.displayShutdown(true); // Turn off LEDs during flash if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial); } else if (upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial); } else if (upload.status == UPLOAD_FILE_END) { if (Update.end(true)) Serial.printf("Update Success: %u bytes\n", upload.totalSize); else Update.printError(Serial); } }); server.begin(); Serial.println("HTTP Server Started."); Serial.println("Setup Finished."); } // [SECTION 10] - Execution Loop (v12.7 "Perfect Rhythm") void loop() { server.handleClient(); yield(); if (isAPMode) { dnsServer.processNextRequest(); } else { if (myDisplay.displayAnimate()) { myDisplay.displayShutdown(true); delay(50); static bool toggleName = true; // The heartbeat master bool triggerSequence = (millis() - lastUpdate > (curTR * 60000UL)); if (triggerSequence || scrollState > 0) { // --- THE INFO SEQUENCE --- if (scrollState == 0) { updateYouTubeData(); scrollState = 1; } switch (scrollState) { case 1: snprintf(displayMsg, 48, "CH: %s", channelName.c_str()); scrollState = 2; break; case 2: snprintf(displayMsg, 48, "LOC: %s", location.c_str()); scrollState = 3; break; case 3: snprintf(displayMsg, 48, "SYNC: %s", lastTimeCheck.c_str()); scrollState = 4; break; case 4: lastUpdate = millis(); scrollState = 0; toggleName = true; // RESET RHYTHM: Next message MUST be the Name snprintf(displayMsg, 48, "%s", channelName.c_str()); break; } myDisplay.displayText(displayMsg, PA_CENTER, 80, 2000, PA_SCROLL_LEFT, PA_SCROLL_UP); } else { // --- THE NORMAL HEARTBEAT (Name <-> Subs) --- if (toggleName) { snprintf(displayMsg, 48, "%s", channelName.c_str()); myDisplay.displayText(displayMsg, PA_CENTER, 80, 2000, PA_SCROLL_LEFT, PA_SCROLL_UP); } else { snprintf(displayMsg, 48, "Subs: %ld", currentSubs); myDisplay.displayText(displayMsg, PA_CENTER, 80, 2000, PA_SCROLL_UP, PA_SCROLL_UP); } toggleName = !toggleName; // Flip for next time } myDisplay.displayClear(); myDisplay.displayShutdown(false); myDisplay.displayReset(); } if (millis() - lastBlink > (isBlinking ? 150 : 3500)) { isBlinking = !isBlinking; lastBlink = millis(); } } }
Pay attention to the hardware requirements if you are going to do this project - the capacitors, resistors and wire placement is pretty crucial when doing SPI data transfers.
The whole project can be accessed and even updated from a webserver on the ESP32 - it couldn't be easier to make your own.
In fact you don't even need the LED matrix modules, you can monitor your subscribers from anywhere on your network by dialling into http://onecircuit.local/ once the credentials have all been entered.
Thanks for dropping by - don't forget to subscribe and together we can watch the numbers climb!

No comments:
Post a Comment