// ***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
}
Comments