Adrian Smith
Published © CC BY-NC-SA

ESP8266 based NTP clock with YouTube statistics display

This is an universal 8 digit internet connected display which displays the time from the NTP service and your YouTube channel statistics.

IntermediateFull instructions provided2 hours711
ESP8266 based NTP clock with YouTube statistics display

Things used in this project

Hardware components

NodeMCU V3 ESP8266-12E
×1
Renesas ICM7228AIPIZ
×1
Broadcom HDSP-5501
×8
Capacitor 100 nF
Capacitor 100 nF
×2
33uf capacitor 10V (generic)
×1
Turned pin M-F header (LED sockets)
×1
Micro-B USB cable, right angle
×1
Deep photo frame 6x4"
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

PCB design files (KiCad)

Schematic and PCB design files in KiCad format

Code

Firmware

C/C++
The main code (excluding libraries)
// ***DESCRIPTION, HOW TO USE AND CREDITS***

// Full instructions can be found at https://adrian-smith31.co.uk/blog

// This program will create an access point to configure wifi. SSID "YTcounter", password is "password"
// Connect to this AP with an Android or IOS phone and sign in to the network. Config box will appear.
// Once wifi is established go to IP address displayed at boot and enter youtube channel ID and API key.
// The counter will do a display test and display dashes followed by time display when connected to the internet.
// Pressing the function button will cycle between time,subscriber count and view count.
// Pressing and holding the function button for more than 2 seconds then releasing toggles Daylight Saving Time.

// Primary function uses portions of code originally written by Brian Lough https://github.com/witnessmenow 
// Uses open source WiFi manager for easy wireless configuration by Tzapu https://github.com/tzapu
// Rest of libraries part of ESP8266 Arduino repository and installed by library manager in Arduino IDE.

// Subscriber counts above 999 will be rounded down to lowest 10 e.g 1003 subs will show 1000 subscribers.
// this is a change Youtube implemented and nothing can be done about this. Realistically only for small channels.

// Written for NodeMCU version 1.0 on ESP-12E V3 module. Should work with others, pin assignments may need changing.
// Settings: CPU clock 80Mhz, flash size 4Mb, V2 lower memory, Vtables=flash, erase flash: only sketch, legacy

// ***TROUBLESHOOTING & PROBLEMS***

// Please use a terminal program such as terraterm to monitor serial output. Any error messages
// will be shown such as HTTP status codes plus any debug messages from the NodeMCU RTOS and / or debugger.
// Serial settings: 115200 baud, 8 bits, no parity, one stop bit. First line will show garbage, that's normal.
// If no stats are shown error 403 is generally incorrect / expired API key. 400 or 404 is wrong channel ID.
// If there is a problem connecting to the internet or WiFi config isn't set, display will show --HELP--
// Reason for being unable to connect and / or get YouTube data will be shown in the serial terminal.
// use ESP8266 community V2.6.0 and change stack size to #define _stackSize (6748/4) 
// in stackthunk.cpp in the ESP8266 core files otherwise it just crashes with later 
// versions of the libraries. Stackthunk.cpp is in local appdata/arduinoxx where xx version no

#include <WiFiManager.h> // https://github.com/tzapu/WiFiManager
#include <ESP8266WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <WiFiClientSecure.h> // for connecting to SSL sites
#include <EEPROM.h>
#include <YoutubeApi.h> // https://github.com/witnessmenow/arduino-youtube-api
#include <ArduinoJson.h>
#include <stackthunk.h>
#include <ICM7218.h> // library also works with the ICM7228 with limited functions. Enough for this though.
#include <RTClib.h>
#include <Time.h>

// Configure the 10 OUTPUT pins used to interface with ICM7218: D0-D7, mode, write
ICM7218 myLED(D1, D2, D5, D6, D4, ICM7218::NO_PIN, ICM7218::NO_PIN, D3, D7, D8);

// Pins ID5 and ID6 are not used. Hardwired to CODEB decode mode.
// Accepts numbers 0-9, characters H,E,L,P,-,space and full stop. Full stop lights decimal point.
// Pin numbers not in sequence as some have pullups or specific functions at boot. Onboard LED will
// light when display is in normal operation. 
// CODEB character mode: supports digits 0-9, characters 'H', 'E', 'L', 'P',
// space (' ') and hyphen ('-') plus decimal points

//===============================================================
// This is the HTML code that is served for the config page
//===============================================================

const char MAIN_page[] PROGMEM = R"=====( 
<!DOCTYPE html>
<html>
<body>

<h2>Config Settings<h2>
<h3>Youtube Counter V2.0</h3>

<form action="/action_page">
  Channel ID:<br>
  <input type="text" name="Channel ID" value="">
  <br>
  API key:<br>
  <input type="text" name="API Key" value="">
  <br><br>
  <input type="submit" value="Submit">
</form> 

</body>
</html>
)=====";

// end HTML code //

ESP8266WebServer server(80); // configure built in web server to port 80

uint IDaddr = 0; // eeprom starting address for channel ID
uint APIaddr = 32; // eeprom starting address for API Key

char API_KEY[48] = ""; // leave blank - data will be copied from eeprom later
WiFiClientSecure client;
YoutubeApi *api;

unsigned long timeBetweenRequests = 300000; // poll Youtube every 5 mins (300,000 milliseconds)
unsigned long nextRunTime;
unsigned long subs;
unsigned long views;
int buttonState; // stores state of the mode select / DST adjust button
long utcOffsetInSeconds = 0; // change this if this gets sold outside of the UK. 3600 seconds per hour.
int timeset = 0;
int dst = 1; // inital high state so DST is off. Button goes low to turn it on.
int dstset = 0;
byte mode = 0;
const int buttonPin = D0;
const int shortPresstime = 500; // half a second
const int longPresstime = 2000; // 2 seconds
int lastBtnstate = 0;
unsigned long btnPressedtime = 0;
unsigned long btnReleasedtime = 0;

char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds); // a more local NTP server may work better
RTC_Millis rtc; // setup software RTC using CPU clock as timebase. This gets autocorrected every hour.

void setup() 
{
  pinMode(buttonPin, INPUT); // Pin D0 only has a pulldown resistor, not pullup. Use external pullup instead.
  myLED.setMode(ICM7218::CODEB);
  myLED.print("8.8.8.8.8.8.8.8."); // do a display test at power up
  delay(1000);
  myLED.print("--------"); // indicate a dash to show it's trying to connect to the internet
  WiFi.hostname("YTcounter1049"); // set up hostname consisting of YTcounter and last 4 digits of MAC address
  WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP
  Serial.begin(115200);
  EEPROM.begin(512); // start eeprom emulation and allocate 512 bytes. That is plenty.
  WiFiManager wm;
  wm.setAPCallback(configModeCallback); // this runs if it can't connect to the internet. Function for this below.
  timeClient.begin(); // start NTP client
  
   //reset settings - wipe network config (for testing purposes only)
    //wm.resetSettings();

    // Automatically connect using saved credentials,
    // if connection fails, it starts an access point with the specified name ( "YTcounterAP"),
    // if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
    // then goes into a blocking loop awaiting configuration and will return success result

    bool res;   
    res = wm.autoConnect("YTCounterAP","password"); // password protected ap
        
    Serial.print("*NET: IP address: "); 
    Serial.println(WiFi.localIP());
    char IP[12];
    String LocalIP = String() + "1P" + "." + WiFi.localIP()[2] + "." + WiFi.localIP()[3]; // display last two octets of IP address
    strcpy(IP, LocalIP.c_str());
    myLED.print(IP);
    delay(3000);
    Serial.print("*YTC: Channel ID:");
    String IDstr;   
    for(int i=0;i<32;i++) // channel ID is 24 characters long, allow up to 32 characters
     {
       IDstr = IDstr + char(EEPROM.read(IDaddr+i));     
     }  
    Serial.println(IDstr);  
    Serial.print("*YTC: API Key:");
    String APIstr;   
    for(int i=0;i<48;i++) // API key is 40 characters long, allow up to 48 characters
     {
       APIstr = APIstr + char(EEPROM.read(APIaddr+i));     
     }  
    Serial.println(APIstr);  
    Serial.println("*NET: Waiting for data...");

    server.on("/", handleRoot);      //Which routine to handle at root location
    server.on("/action_page", handleForm); //form action is handled here
    server.begin();                  //Start server
    client.setInsecure(); // required for version 2.5 and above
    strcpy(API_KEY, APIstr.c_str());
    api = new YoutubeApi(API_KEY, client);
  
    // sync software RTC with NTP clock at boot. 
    timesync();
}

void loop() 
{
   server.handleClient(); 
   getstats();
   displaydata();
   DateTime now = rtc.now();
   
   if (now.minute() == 0 && now.second() == 0 && timeset == 0)

   {
    timesync(); // sync the time on the hour. Avoids repeated NTP calls & possibly getting IP banned by NTP service.
    timeset = 1; // this prevents timesync running over and over till zero second has passed.
    Serial.println(F("NTP server sync run. If time still incorrect, power cycle unit or NTP server is down"));    
   }

   if (now.minute() == 10 && now.second() == 0 && timeset == 1)

   {
     timeset = 0; // reset timeset flag after 10 mins so time will update again on the next hour
   }

   if (now.hour() == 23 && now.minute() == 59 && now.second() == 59) // turn display off at midnight
      {
        myLED.displayShutdown();
      }

   if (now.hour() == 6 && now.minute() == 0 && now.second() == 0) // turn it back on again at 6am.
      
      {
        myLED.displayWakeup();
      }
}

//===============================================================
// This function is run when you open the webpage in a browser
//===============================================================
void handleRoot() 
{
 delay(10); // delay required otherwise it crashes and reboots when entering data.
 String s = MAIN_page; //Read HTML contents
 server.send(200, "text/html", s); //Send web page
}
//===============================================================
// This function is run when you press submit
//===============================================================
void handleForm() 
{
 String ChannelID = server.arg("Channel ID"); 
 String APIkey = server.arg("API Key"); 
// erase eeprom before writing new data. On ESP8266 can't write to just one part of the (emulated) eeprom. 
// you have to erase and program whole eeprom in one go unlike the AVR based Arduino that has a real eeprom.
 for (int i = 0; i < 512; i++) 
    {
      EEPROM.write(i, 0); 
    }
    
    EEPROM.commit(); // have to call this to actually write contents to flash area assigned to emulated eeprom
    delay(500); // must have delay here to allow time for contents to be written

 Serial.print("Channel ID:");
 Serial.println(ChannelID);
 
   for(int i=0;i<ChannelID.length();i++)
  {
    EEPROM.write(IDaddr+i, ChannelID[i]); 
  }
   
 Serial.print("API Key:");
 Serial.println(APIkey);
 
  for(int i=0;i<APIkey.length();i++)
  {
    EEPROM.write(APIaddr+i, APIkey[i]); 
  }

 EEPROM.commit();
 String s = "<a href='/'> Stored! You can close your browser tab or click on this text if you made a mistake. </a>";
 server.send(200, "text/html", s); //Send web page
}

//===============================================================
// This function gets the Youtube data
//===============================================================

void getstats()
{
  String IDstr;   
    for(int i=0;i<32;i++) // channel ID is 24 characters long, allow up to 32 characters
     {
       IDstr = IDstr + char(EEPROM.read(IDaddr+i));     
     }  
  
   if (millis() > nextRunTime)  
   {
    if(api->getChannelStatistics(IDstr))
    {
      Serial.println(F("---------Stats---------"));
      Serial.print(F("Subscriber Count: "));
      Serial.println(api->channelStats.subscriberCount);
      Serial.print(F("View Count: "));
      Serial.println(api->channelStats.viewCount);
      Serial.print(F("Comment Count: "));
      Serial.println(api->channelStats.commentCount);
      Serial.print(F("Video Count: "));
      Serial.println(api->channelStats.videoCount);
      Serial.println(F("------------------------"));
           
      Serial.printf("Free heap size: %u\n", ESP.getFreeHeap()); // for testing. If stack heap overflows it reboots
      Serial.printf("Free cont stack size: %u\n", ESP.getFreeContStack()); // for testing

      subs = api->channelStats.subscriberCount;
      views = api->channelStats.viewCount;

       DateTime now = rtc.now();
       Serial.print(F("Update Time "));
       Serial.print(now.hour());
       Serial.print(":");
       Serial.print(now.minute());
       Serial.print(":");
       Serial.print(now.second());
       Serial.println(" ");
    
    }
    nextRunTime = millis() + timeBetweenRequests;
  }
}

//===============================================================
// This function runs if it can't connect to wifi network
//===============================================================

void configModeCallback (WiFiManager *myWiFiManager) 
    {
    Serial.println("Entered config mode");
    Serial.println(WiFi.softAPIP());
    Serial.println(myWiFiManager->getConfigPortalSSID());
    myLED.print("--HELP--"); 
    }

//===============================================================
// This function monitors the button & selects data to display
//===============================================================

void displaydata()
{
  
  buttonState = digitalRead(buttonPin);

  if (lastBtnstate == 1 && buttonState == 0) // button pressed
     btnPressedtime = millis();
  else if (lastBtnstate == 0 && buttonState == 1) // button released
  {
     btnReleasedtime = millis();

  long btnPressduration = btnReleasedtime - btnPressedtime;

  if (btnPressduration < shortPresstime)
     mode++;

  if (btnPressduration > longPresstime)
     dst = !dst; // toggle DST mode
  }

  lastBtnstate = buttonState;
      
   if (mode == 3)
     mode = 0;
      
    switch (mode) 
    {
      case 0:
        timedisplay();
        break;
      case 1:
        char subsdata[8];
        itoa(subs, subsdata, 10); // convert integer returned by views to char array
        myLED.print(subsdata); 
        delay(10); // 10ms delay to allow ICM7228 to update. RTC keeps going whilst function is delayed
        break;
      case 2:
        char viewsdata[8];
        itoa(views, viewsdata, 10); // convert integer returned by views to char array
        myLED.print(viewsdata);
        delay(10); // 10ms delay to allow ICM7228 to update. RTC keeps going whilst function is delayed
        break;
    }
     
 }

//===============================================================
// This function syncronises the software RTC with NTP time
//===============================================================

void timesync()
{
    timeClient.update();
    DateTime now = rtc.now();
    int hr,mins,sec,days;
    sec = timeClient.getSeconds();
    mins = timeClient.getMinutes();
    hr = timeClient.getHours();
    days = timeClient.getDay();
    rtc.adjust(DateTime(2021,1,1,hr,mins,sec)); // set the time (date doesn't really matter)
}

//===============================================================
// This function displays the time with leading zero correction
//===============================================================


void timedisplay() // if / else statements ahoy! A mess. But it works...

{
    DateTime now = rtc.now();
    char timedata[10];
    char temp[8]; 
   
    if (dst == 1 && dstset == 0)
      {
        Serial.println("DST ON");
        dstset = 1;
      }

   else if (dst == 0 && dstset == 1 ) 
      {
        Serial.println("DST OFF");
        dstset = 0; 
      }

    if (now.hour() <10)
    
     {
        if (dstset == 1 && (now.hour() == 9)) // do not add 0 if hour +1 is 10
        {
          itoa(now.hour()+1, temp, 10); // convert integer output from RTC, add 1 then store in char temp array
          strcpy(timedata, temp); // copy to timedata array
          strcat(timedata, "-"); // append to timedata array
        }
    
       else if (dstset == 1) // only add leading zero if hour +1 is 9 or less
       
        {
          itoa(0, temp, 10); // convert integer 0 to char and put in temp array
          strcpy(timedata, temp); // copy temp array to timedata array
          itoa(now.hour()+1, temp, 10); // convert integer output from RTC, add 1 then store in char temp array
          strcat(timedata, temp); // append to timedata array
          strcat(timedata, "-"); // append to timedata array
        }
        
        else
        {
          itoa(0, temp, 10);
          strcpy(timedata, temp);
          itoa(now.hour(), temp, 10); // convert int output from RTC, DO NOT add 1 then store in char temp array
          strcat(timedata, temp);
          strcat(timedata, "-");
        }
     }

     else
     {
        if (dstset == 1)
        {
          // this replaces 24 with 00 at midnight if DST is on as it adds one hour to whatever now.hour() is.
          if (now.hour() == 23) 
          {
            itoa(0, temp, 10);
            strcpy(timedata, temp);
            strcat(timedata, "0");
            strcat(timedata, "-");
          }

          else 
          {
           itoa(now.hour()+1, temp, 10);
           strcpy(timedata, temp);
           strcat(timedata, "-");
          }
        }
        
        else
        {
          itoa(now.hour(), temp, 10);
          strcpy(timedata, temp);
          strcat(timedata, "-");
        }
     }

    if (now.minute() <10)
    {
      strcat(timedata, "0");
      itoa(now.minute(), temp, 10);
      strcat(timedata, temp);
      strcat(timedata, "-");
    }

    else 
    {
      itoa(now.minute(), temp, 10);
      strcat(timedata, temp);
      strcat(timedata, "-");
    }
    if (now.second() <10)
     {
       strcat(timedata, "0");
       itoa(now.second(), temp, 10);
       strcat(timedata, temp);
     }

     else
     {
       itoa(now.second(), temp, 10);
       strcat(timedata, temp);
     }

    myLED.print(timedata);
    delay(10); // 10ms delay to allow ICM7228 to update. RTC keeps going whilst function is delayed
}

Credits

Adrian Smith

Adrian Smith

6 projects • 2 followers
Electronics engineer for around 10 years, repair electronic LED signs and other old electronics from the 1980 / 1990's.
Thanks to Brian Lough and Tzapu.

Comments