ericBcreator
Published © GPL3+

1024 LED Matrix WiFi Message Board with Menu + Web Interface

ESP32 powered LED matrix displaying real-time news, weather, stock, date, time, barometer data, menu and web interface with notification.

IntermediateShowcase (no instructions)3 hours3,817
1024 LED Matrix WiFi Message Board with Menu + Web Interface

Things used in this project

Hardware components

ESP-WROOM-02
Espressif ESP-WROOM-02
×1
MAX7219 8x8 led matrix
×16
SparkFun Atmospheric Sensor Breakout - BME280
SparkFun Atmospheric Sensor Breakout - BME280
×1
Rotary Encoder with Push-Button
Rotary Encoder with Push-Button
×1
Buzzer
Buzzer
×1

Software apps and online services

Arduino IDE
Arduino IDE

Story

Read more

Schematics

16x8x8 led matrix board schematic

Code

LED Matrix board 20190411 shared

Arduino
/*
***************************************************************************
  A 1024 (16x8x8) LED matrix WiFi message board with rotary encoder,
  BME280 sensor and a web interface with new message notification
  by ericBcreator

  It connects to Wifi and a timeserver and then displays date, time,
  sunrise and sunset for a set location, sensors reading of the 
  temperature, humidity and air pressure, current weather and forecasts 
  from openweather.map for multiple cities, top headlines from 
  NewsApi.org, stock data from Alpha Vantage and random Wikipedia items.
  
  A rotary encoder selects between the default display 'carousel',
  one news category, stock or weather data, no messages or it resets 
  the ESP.

  There is web access (enter the IP address of the ESP32 which is 
  displayed at startup in a browser) for displaying custom texts and 
  access to some controls. A buzzer plays a notification sound whenever 
  a new message is received.
***************************************************************************
  components:
  - ESP32
  - 16 x 8x8 MAX7219 LED matrices
  - BME280 sensor
  - rotary encoder
  - buzzer
  - light sensor
***************************************************************************
 last updated 20190411 by ericBcreator
  20181231: solved Raeren Openweather error
  20190126: added Youtube api support
  20190202: added IP to menu and menuItem numbers
  20190328: fixed webinterface for menuItem numbers
  20190406: added na_maxCallsPerDay for newsapi
            added clickable urls for newsapi and wikipedia for the webserver
  20190410: added light sensor support for setting the brightness of the leds
  
  TODO: adding buffering for newsapi
***************************************************************************
  This code is free for personal use, not for commercial purposes.
  Please leave this header intact.

  contact: ericBcreator@gmail.com
***************************************************************************
*/

#define FN_ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))  // Generic macro for obtaining number of elements of an array

//#define DEBUG                                 // enable debugging messages
//#define DEBUG_FAST                            // skip wifi/timeserver connect messages
//#define DEBUG_NEWSAPI_NC                      // do not connect to Newsapi.org (to save the number of daily requests)

//#define LOCAL_LANG                              // @EB-setup use local language (user-definable, otherwise use English)
#define CONVERT_UTF8                            // @EB-setup converts UTF8 'special' characters to the Parola font set
#define WEBSERVER                               // @EB-setup enable webserver and web access
#define BUZZER                                  // @EB-setup use buzzer for new messages from the webserver

//
// Include libraries
//

#include <Adafruit_BME280.h>                    // BME280 library
#include <ArduinoJson.h>                        // Arduino Json library
#include <Dusk2Dawn.h>                          // Dusk2Dawn sunrise sunset library
#include <MD_MAX72xx.h>                         // Needed for Parola
#include <MD_Parola.h>                          // Parola LED library
#include <RotaryEncoder.h>                      // Rotary encoder library
#include <SPI.h>                                // Needed for the radiohead library
#include <TimeLib.h>                            // Time library
#include <WiFiClientSecure.h>                   // WiFi library
#include <WiFiUdp.h>                            // Udp library
#include <YoutubeApi.h>                         // YouTube library

#include "Font_Data_Numeric.h"                  // local file: font data for time

#ifdef CONVERT_UTF8
  #include "Font_Data_UTF8.h"                   // local file: font data for UTF8 support
#endif


//
// LED matrix setup and pins definition
//

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW       // @EB-setup define the type of hardware connected
#define CLK_PIN     18                          // @EB-setup CLK / SCK pin number  (EB setup: yellow)
#define DATA_PIN    23                          // @EB-setup DIN / MOSI pin number (EB setup: brown)
#define CS_PIN      5                           // @EB-setup LOAD / SS pin number  (EB setup: green)
#define MAX_DEVICES 16                          // @EB-setup num of LED matrices
#define MAX_ZONES   3                           // num of zones
MD_Parola PD = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

//
// BME280 setup
//

Adafruit_BME280 bme;                            // connect via I2C pin 21 SDA and 22 SCL
uint8_t bme_address = 0x76;                     // @EB-setup set to 0x76 or 0x77

//
// Rotary encoder setup
//

#define RE_PINA 16                              // @EB-setup pinA / clock pin number
#define RE_PINB 17                              // @EB-setup pinB / data pin number
#define RE_SWITCH 4                             // @EB-setup switch / switch pin number
RotaryEncoder RE_encoder(RE_PINA, RE_PINB);     // initialize the rotary encoder
int reLastPos = 0;
int reLastSwitchState = 0;

//
// Buzzer setup
//

#define BUZZER_PIN 15                           // @EB-setup buzzer pin
int buzz_volume = 50;                           // @EB-share buzzer volume (duty cycle 0-100)
int buzz_channel = 0;
int buzz_resolution = 8;

//
// Light sensor setup
//

//#define LS_ACTIVE                               // @EB-setup comment this line when not using a light sensor (set the brightness with pd_brightness instead)
#define LS_PIN 36                               // @EB-setup light sensor pin

int ls_minValue = 0;                            // @EB-setup min brightness (0-15)
int ls_maxValue = 15;                           // @EB-setup max brightness (0-15)
int ls_delay = 250;                             // @EB-setup delay time for checking the light sensor (in milliseconds)
int ls_triggerStep = 1;                         // @EB-setup if value 1 causes flickering try 2 or 3

long ls_lastTime = 0;

//
// Initialize variables
//

/* WiFi connection */
const char * ssid     = "######################";                   // @EB-setup set your WiFi SSID
const char * password = "######################";                   // @EB-setup set your WiFi password
int wifiTimeout = 30000;                                            // WiFi timeout (in milliseconds)

/* location for sunrise and sunset */
char d2d_City[] = "Amsterdam";                                      // @EB-share City name
float d2d_longitude = 52.37936;                                     // @EB-share
float d2d_lattitude = 4.900293;                                     // @EB-share
int d2d_timezone = 1;                                               // @EB-share UTC timezone offset

/* openweathermap connection */
const char * ow_servername = "api.openweathermap.org";              // remote server openweathermap.org
const int ow_port = 80;                                             // port
const String ow_APIKEY = "################################";        // @EB-setup personal api key for retrieving the weather data. leave empty to skip

#ifdef LOCAL_LANG
  const String ow_language = "NL";                                  // @EB-setup openweather language
#else
  const String ow_language = "EN";
#endif

int ow_cityIDLoop = 0;

// get the ID at https://openweathermap.org/ type the city, click search and click on the town
// then check the link, like this: https://openweathermap.org/city/5128581 5128581 is the ID for New York
// @EB-share 

String ow_cityIDs[] = {
  "2759794",      // Amsterdam
  "264371",       // Athens
  "3513090",      // Willemstad
  "524901",       // Moscow
  "5128581",      // New York
  "3369157",      // Cape Town 
};

/* newsapi connection */
const char * na_servername = "newsapi.org";                         // remote server newsapi.org
const int na_port = 80;                                             // port
const int na_maxCallsPerDay = 1000;                                 // @EB-setup limit the calls per day. set to zero to disable
                                                                    // 1000 max calls per day for free developer plan;

const String na_APIKEY = "################################";        // @EB-setup personal api key. leave empty to skip

#ifdef LOCAL_LANG
  const String na_country = "NL";                                   // @EB-setup country code for news
#else
  const String na_country = "US";
#endif

int na_article = 0;

/* alpha vantage stock connection */
const char * av_servername = "www.alphavantage.co";                 // remote server alphavantage
const int av_port = 443;                                            // port for secure connection

const String av_APIKEY = "################################";        // @EB-setup personal api key. leave empty to skip

// @EB-share
String av_symbolIDs[] = { "DJIA", "IXIC", "SPX", "AAPL", "GOOGL", "BTCUSD" } ;
String av_symbolName[] = { "Dow Jones index", "Nasdaq index", "S&P 500 index", "Apple", "Google", "Bitcoin" } ;

int av_symbolIDLoop = 0;

/* wikipedia connection */
const int wp_port = 443;                                            // port for secure connection

#ifdef LOCAL_LANG
  const char * wp_servername = "nl.wikipedia.org";                  // @EB-setup country code for wikipedia
#else
  const char * wp_servername = "en.wikipedia.org";
#endif

/* youtube connection */
const String yt_APIKEY = "";                                        // @EB-setup personal api key. leave empty to skip
const String yt_CHANNELID = "################################";     // @EB-setup personal channel ID

/* message definitions */
#ifdef LOCAL_LANG // @EB-share
  char msgWifiConnecting[] =                    "...wifi verbinden...";
  char msgWifiConnected[] =                     "verbonden met";
  char msgWifiFailed[] =                        "* wifi verbinding mislukt *";
  char msgConnectToTimeserver[] =               "...verbinden met de tijdserver...";
  char msgTimeserverFailed[] =                  "* verbinding met tijdserver mislukt *";
  char msgDSTon[] =                             "verbonden,   zomertijd  ";
  char msgDSToff[] =                            "verbonden,  wintertijd  ";  
  char msgResetting[] =                         "...auto reset...";
  char msgOpenweatherFailed[] =                 "* verbinding met Openweathermap mislukt *";
  char msgNewsapiFailed[] =                     "* verbinding met Newsapi.org mislukt *";
  char msgNewsapiError[] =                      "* fout bij ophalen van nieuws van Newsapi.org *";
  char msgAlphaVantageFailed[] =                "* verbinding met AlphaVantage.co mislukt *";
  char msgWikipediaFailed[] =                   "* Wikipedia *"; 
  
  char msgNews[] =                              "Laatste nieuws van";
  char msgSunrise[] =                           "Zonsopkomst";
  char msgSunset[] =                            "Zonsondergang";
  char msgCurrentWeather[] =                    "Het weer in";
  char msgForecast[] =                          "Weersverwachting voor";
  char msgTemp[] =                              "temperatuur";
  char msgHumidity[] =                          "vochtigheid";
  char msgClouds[] =                            "bewolking";
  char msgRain[] =                              "neerslag";
  char msgWind[] =                              "wind";
  char msgAVPrevious[] =                        "vorige slotkoers";
  char msgAVCurrent[] =                         "laatste koers";
  
  char msgSelected[] =                          "OK";
#else
  char msgWifiConnecting[] =                    "...wifi connecting...";
  char msgWifiConnected[] =                     "connected to";
  char msgWifiFailed[] =                        "*failed connecting to wifi*";
  char msgConnectToTimeserver[] =               "...connecting to the timeserver...";
  char msgTimeserverFailed[] =                  "* failed connecting to the timeserver *";
  char msgDSTon[] =                             "connected, DST active";
  char msgDSToff[] =                            "connected, no DST";
  char msgResetting[] =                         "...auto reset...";
  char msgOpenweatherFailed[] =                 "* failed connecting to Openweathermap *";
  char msgNewsapiFailed[] =                     "* failed connecting to Newsapi.org *";
  char msgNewsapiError[] =                      "* failed retrieving news from Newsapi.org ";
  char msgAlphaVantageFailed[] =                "* failed connecting to AlphaVantage.co *";
  char msgWikipediaFailed[] =                   "* failed connecting to Wikipedia *"; 
  
  char msgNews[] =                              "Latest news from";
  char msgSunrise[] =                           "Sunrise";
  char msgSunset[] =                            "Sunset";
  char msgCurrentWeather[] =                    "Current weather in";
  char msgForecast[] =                          "Weather forecast for";
  char msgTemp[] =                              "temperature";
  char msgHumidity[] =                          "humidity";
  char msgClouds[] =                            "clouds";
  char msgRain[] =                              "rain";
  char msgWind[] =                              "wind";
  char msgAVPrevious[] =                        "previous close";
  char msgAVCurrent[] =                         "latest";
  
  char msgSelected[] =                          "OK";
#endif

/* menu setup */
// @EB-share
#ifdef LOCAL_LANG
  const String menu[] { 
    "Standaard mozaek", 
    "Nieuws headlines", 
    "Economie", 
    "Technologie", 
    "Algemeen", 
    "Entertainment", 
    "Gezondheid", 
    "Wetenschap", 
    "Sport", 
    "Wikipedia willekeurig item",
    "Beursinfo van Alpha Vantage", 
    "Weerberichten", 
    "Youtube", 
    "Eigen bericht", 
    "IP adres", 
    "Geen berichten", 
    "- Reset" 
  } ;
#else
  const String menu[] { 
    "Default carousel",                         //  0
    "Top headlines",                            //  1
    "Business",                                 //  2
    "Technology",                               //  3
    "General",                                  //  4
    "Entertainment",                            //  5
    "Health",                                   //  6
    "Science",                                  //  7
    "Sports",                                   //  8
    "Wikipedia random item",                    //  9
    "Stock information from Alpha Vantage",     // 10
    "Weather information",                      // 11
    "Youtube",                                  // 12
    "User defined message",                     // 13
    "IP Address",                               // 14
    "No messages",                              // 15
    "- Reset"                                   // 16
  } ;
#endif

#define menuItemDefault    0
#define menuItemNews       1                    // 2 to 8 are news categories
#define menuItemWiki       9
#define menuItemStock     10
#define menuItemWeather   11
#define menuItemYouTube   12
#define menuItemUser      13
#define menuItemIP        14
#define menuItemNoMess    15
#define menuItemReset     16

const String menuNewsApi[] { "", "&category=business", "&category=technology", "&category=general", "&category=entertainment", "&category=health", "&category=science", "&category=sports" } ;

int menuItem0 = menuItemDefault;                // @EB-share default menu selection
int menuReturnDelay = 10000;                    // @EB-share delay before the menu exits

int prevMenuItem0 = menuItem0;
int menuLen = FN_ARRAY_SIZE(menu);
int menuNewsCat = 0;

/* time server */
int timeServerDelay = 1000;                     // delay for the time server to reply
int timeServerPasses = 4;                       // number of tries to connect to the time server before timing out
boolean timeServerConnected = false;            // is set to true when the time is read from the server

unsigned int localPort = 2390;                  // local port to listen for UDP packets
IPAddress timeServerIP;                         // IP address of random server
const char* ntpServerName = "pool.ntp.org";     // server pool
byte packetBuffer[48];                          // buffer to hold incoming and outgoing packets
int timeZoneoffsetGMT = d2d_timezone * 3600;    // offset from Greenwich Meridan Time
boolean DST = false;                            // daylight saving time
WiFiUDP clockUDP;                               // initialize a UDP instance

/* web server */
WiFiServer webServer(80);
int webpageRefreshTime = 15;                    // @EB-share delay time before the webserver page will auto refresh

/* Parola */
#define BUF_SIZE 250                            // @EB-share buffer size for zone 2

char pd_message0[9] = { "" };                   // message buffer zone 0 (time)
char pd_message1[20] = { "" };                  // message buffer zone 1
char pd_message2[BUF_SIZE] = { "" };            // message buffer zone 2
char tmpBuffer[BUF_SIZE] = { "" };

char pd_messageUser[BUF_SIZE] = { "--- set your message here ---" }; // @EB-share

char pd_url[BUF_SIZE] = { "" };                 // url (for newsapi and wikipedia)

uint8_t degC[] = { 6, 3, 3, 56, 68, 68, 68 };   // Deg C character
uint8_t degF[] = { 6, 3, 3, 124, 20, 20, 4 };   // Deg F character
uint8_t degCascii = 0x8F;
uint8_t degFascii = 0x90;

int pd_brightness = 3;                          // @EB-share brightness (0-15)
int scrollSpeedSetup = 20;                      // @EB-share frame delay setup
int scrollSpeedMain = 40;                       // @EB-share frame delay main loop
int scrollPause = 1500;                         // @EB-share delay after displaying the message (in milliseconds)
textEffect_t scrollEffect = PA_SCROLL_LEFT;     // @EB-share default effect
textPosition_t scrollAlign = PA_CENTER;         // @EB-share default align
textEffect_t zone1ScrollEffect = PA_SCROLL_UP;  // @EB-share default effect
int zone1Pause = 10000;                         // @EB-share delay after displaying the zone 1 message (in milliseconds)

/* general */
boolean tempCelsius = true;                     // @EB-setup set false for Fahrenheit

boolean colonFlag = true;
static uint8_t loopCounter1 = 0;
static uint8_t loopCounter2 = 0;
static uint32_t lastZone0Time = 0;
static uint32_t lastZone1Time = 0;

//
// Setup
//

void setup() {
#ifdef DEBUG
  Serial.begin(57600);                          // for debugging
  Serial.println("");
  Serial.println("Setup...");
#endif

#ifdef BUZZER  
  ledcSetup(buzz_channel, 0, buzz_resolution);  // setup the buzzer
#endif

  pinMode(RE_SWITCH, INPUT_PULLUP);             // setup the encoder button as input high

#ifdef LS_ACTIVE
//  pinMode(LS_PIN, INPUT_PULLDOWN);              // setup the light sensor input as input low
  checkLightSensor();
#endif

  setupPD();                                    // start the Parola display

  bme.begin(bme_address);                       // start the BME280 sensor

  connectToWifi();                              // connect to Wifi

  if (WiFi.status() == WL_CONNECTED) {
    getTimeFromServer();                        // connect to the timeserver and get the local time

#ifdef WEBSERVER
    webServer.begin();
    #ifdef DEBUG
      Serial.println("Webserver started");
    #endif
#endif
  }
  
  PD.setSpeed(2, scrollSpeedMain);
}

//
// Main loop
//

void loop() { 
  if ((millis() - lastZone0Time >= 1000)) {                                         // wait for 1 second before displaying the time
    displayTime();
    lastZone0Time = millis();
  }

  if ((millis() - lastZone1Time >= zone1Pause)) {                                   // display the zone 1 animation after a pause
    lastZone1Time = millis();

    switch (loopCounter1) {
      case 0:     displayDateShort();           loopCounter1++;   break;            // display short date
      case 1:     displayTemp();                loopCounter1++;   break;            // display temperature
      case 2:     displayHumidity();            loopCounter1++;   break;            // display humidity
      case 3:     displayPressure();            loopCounter1 = 0; break;            // display pressure
    }
  }

  if (readRotEnc() != 0) {                                                          // if the rotary encoder is turned go to the menu
    selectMenu0();
    if (menuItem0 == menuItemReset)                                                 // reset the ESP
      ESP.restart();
      
    if (menuItem0 != prevMenuItem0) {
      na_article = 0;
      menuNewsCat = 0;
      loopCounter2 = 0;
      prevMenuItem0 = menuItem0;
    }
  } else if (readRotEncSwitch() == true)                                            // if the rotary encoder is pushed go to the next item to display
    resetDisplayZone2();

#ifdef WEBSERVER
  checkWebAccess();                                                                 // display the website and check for input
#endif

  if (WiFi.status() != WL_CONNECTED) {                                              // if there is no wifi connection reset after 2 minutes to try to reconnect
    if (millis() > (2 * 60000)) {
      PD.setPause(2, 0);
      displayMessage(msgResetting);
      ESP.restart();
    }
  } else if (menuItem0 != menuItemNoMess) {	                                        // only display if menu item 'No messages' is not selected
    if (PD.getZoneStatus(2)) {                                                      // display the next item when the animation of the current item has finished
      sprintf(pd_url, "");                                                          // reset the url

#ifdef DEBUG
      if (loopCounter2 == 0) {
        Serial.println("");
        Serial.println(pd_message0);
      }
#endif

      if (menuItem0 == menuItemDefault) {                                           // default setting, loop trough all items
        switch (loopCounter2) {
          case 0:     displayDate();            loopCounter2++; break;              // display date
          case 1:     displaySunrise();         loopCounter2++; break;              // display sunrise time
          case 2:     displaySunset();          loopCounter2++; break;              // display sunset time
          case 3:     displayWeatherData();     loopCounter2++; break;              // 1st loop current weather, 2nd loop weather forecast
          case 4:     displayNewsapiData();     loopCounter2++; break;              // display newsapi items
          case 5:     displayAVStockData();     loopCounter2++; break;              // display alpha vantage stock data
          case 6:     displayWikipedia();       loopCounter2++; break;              // display a random Wikipedia item
          case 7:     displayYT();              loopCounter2++; break;              // display YouTube          
          default:    loopCounter2 = 0;
        }

      } else {
          if (loopCounter2 == 10)
            displayDate();
          else {
            switch (menuItem0) {
              case menuItemUser:      displayUserMessage();  break;                 // user defined message (or posted on the webserver)
              case menuItemIP:        displayIP();           break;                 // only show IP
              case menuItemYouTube:   displayYT();           break;                 // only show YouTube
              case menuItemWeather:   displayWeatherData();  break;                 // only show weather data
              case menuItemStock:     displayAVStockData();  break;                 // only show stock data
              case menuItemWiki:      displayWikipedia();    break;                 // only show Wikipedia
              
              default:                                                              // only show news                  
                menuNewsCat = menuItem0 - 1;
                displayNewsapiData();
            }
          }
        loopCounter2 = (++loopCounter2) % 11;                                       // show 10 items and then display the date again
      }
      
#ifdef CONVERT_UTF8
      if (menuItem0 != menuItemUser)                                                // don't do a double conversion of the webserver text, it will erase the UTF characters
        utf8AsciiStr(pd_message2).toCharArray(pd_message2, BUF_SIZE);               // convert UTF8 characters
#endif
    }
  }

  PD.displayAnimate();

#ifdef LS_ACTIVE
  if (millis() - ls_lastTime > ls_delay) {                                         // check the light sensor every quarter second
    checkLightSensor();
    ls_lastTime = millis();
  }
#endif
}

//
// Display functions
//

void setupPD() {
  PD.begin(MAX_ZONES);                          // start the Parola display

  PD.setZone(0, 0, 3);                          // zone 0 time
  PD.setZone(1, 4, 7);                          // zone 1 date, temperature, humidity
  PD.setZone(2, 8, MAX_DEVICES - 1);            // zone 2 messages

  PD.setFont(0, numeric7Seg);                   // set numeric font for the time

#ifdef CONVERT_UTF8
  PD.setFont(2, ExtASCII);                      // Parola font set with UTF8 support
#endif

  PD.setIntensity(pd_brightness);               // brightness (0-15)
  PD.setInvert(0, false);                       // invert false (text on, background off)
  PD.setInvert(1, false);                       // invert false (text on, background off)
  PD.setInvert(2, false);                       // invert false (text on, background off)

  PD.displayZoneText(0, pd_message0, PA_CENTER, 0, 0, PA_PRINT, PA_NO_EFFECT);
  PD.displayZoneText(1, pd_message1, PA_CENTER, scrollSpeedMain, scrollPause, zone1ScrollEffect, PA_NO_EFFECT);
  PD.displayZoneText(2, pd_message2, scrollAlign, scrollSpeedSetup, scrollPause, scrollEffect, scrollEffect);

  PD.addChar(degCascii, degC);
  PD.addChar(degFascii, degF);
}

void checkLightSensor() {
  int tmpValue;

  tmpValue = analogRead(LS_PIN);
  tmpValue = map(tmpValue, 0, 4095, ls_minValue, ls_maxValue);

  if (abs(tmpValue - pd_brightness) >= ls_triggerStep) {
    if (tmpValue < pd_brightness)
      pd_brightness--;      
    else
      pd_brightness++;
    PD.setIntensity(pd_brightness);
      
#ifdef DEBUG
    Serial.print("Brightness set to ");
    Serial.println(pd_brightness);
#endif
  }
}

void displayMessage(char * message) {
  snprintf(pd_message2, sizeof(pd_message2), "%s", message);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif

#if !defined (DEBUG_FAST)
  PD.displayReset(2);
  while (!PD.getZoneStatus(2))
    PD.displayAnimate();
#endif
}

void displayTime() {
  time_t t = now();

  if (hour(t) == 3 && minute(t) == 0)           // at 03:00 get the current time from the timeserver for readjusting and rereading DST
    getTimeFromServer();

  snprintf(pd_message0, sizeof(pd_message0), "%02d%c%02d", hour(t), (colonFlag ? ':' : ' '), minute(t));
  PD.displayReset(0);

  colonFlag = !colonFlag;
}

void displayDateShort() {
  time_t t = now();
  String monthStr = monthAsStringShort(month(t));
  snprintf(pd_message1, sizeof(pd_message1), "%d %s", day(t), monthStr.c_str());
  PD.displayReset(1);
}

void displayTemp() {
  float bmeTemp = bme.readTemperature();
  if (tempCelsius)
    sprintf(pd_message1, "%2.1f %c", bmeTemp, degCascii);
  else {
    bmeTemp = (bmeTemp * 9 / 5) + 32;
    sprintf(pd_message1, "%2.1f %c", bmeTemp, 129);
  }

#ifdef DEBUG
  Serial.println(pd_message1);
#endif
  PD.displayReset(1);
}

void displayHumidity() {
  float bmeHumidity = bme.readHumidity();
  sprintf(pd_message1, "%2.0f%% rh", bmeHumidity);
  
#ifdef DEBUG
  Serial.println(pd_message1);
#endif
  PD.displayReset(1);
}

void displayPressure() {
  float bmePressure = bme.readPressure() / 100.0f;
  sprintf(pd_message1, "%4.0f h", bmePressure); // hPa doesn't fit...
  
#ifdef DEBUG
  Serial.println(pd_message1);
#endif
  PD.displayReset(1);
}

void displayDate() {
  time_t t = now();
  String dayStr = dayAsString(weekday(t));
  String monthStr = monthAsString(month(t));
  sprintf(pd_message2, "%s %d %s %04d", dayStr.c_str(), day(t), monthStr.c_str(), year(t));
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displaySunrise() {
  time_t t = now();
  char d2dSunrise[] = "00:00";

  Dusk2Dawn d2d(d2d_longitude, d2d_lattitude, d2d_timezone);
  Dusk2Dawn::min2str(d2dSunrise, d2d.sunrise(year(t), month(t), day(t), DST));
  sprintf(pd_message2, "%s : %s %s", d2d_City, msgSunrise, d2dSunrise);
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displaySunset() {
  time_t t = now();
  char d2dSunset[] = "00:00";

  Dusk2Dawn d2d(d2d_longitude, d2d_lattitude, d2d_timezone);
  Dusk2Dawn::min2str(d2dSunset, d2d.sunset(year(t), month(t), day(t), DST));
  sprintf(pd_message2, "%s : %s %s", d2d_City, msgSunset, d2dSunset);
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displayUserMessage() {
  strcpy(pd_message2, pd_messageUser);
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displayIP() {
  sprintf(pd_message2, "%s (%d dBm)  %s ", msgWifiConnected, WiFi.RSSI(), WiFi.localIP().toString().c_str());
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void resetDisplayZone2() {
  strcpy(pd_message2, "");
  PD.displayClear(2);
  PD.displayReset(2);
}

//
// menu functions
//

void selectMenu0() {
#ifdef DEBUG
  Serial.println("Entering menu");
#endif

  int reResult;
  int lastReResult;
  int menuStartTime = millis();
  bool reSwitchPressed = false;

//  resetDisplayZone2;  // @EB: () missing, no use?? check

  PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, scrollPause, PA_PRINT, PA_CLOSING);
  displayMenuItem0();

  do {
    reResult = readRotEnc();

    if (reResult < 0) {             // down
      --menuItem0;
      if (menuItem0 < 0)
        menuItem0 = FN_ARRAY_SIZE(menu) - 1;
      PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, scrollPause, PA_SCROLL_UP, PA_SCROLL_UP);

    } else if (reResult > 0) {      // up
      menuItem0 = (++menuItem0) % FN_ARRAY_SIZE(menu);
      PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, scrollPause, PA_SCROLL_DOWN, PA_SCROLL_DOWN);
    }

    if (reResult != 0) {
      menuStartTime = millis();
      displayMenuItem0();
      lastReResult = reResult;
    } else {
      if (PD.getZoneStatus(2)) {
        if (lastReResult < 0)
          PD.setTextEffect(2, PA_SCROLL_LEFT, PA_SCROLL_UP);
        else
          PD.setTextEffect(2, PA_SCROLL_LEFT, PA_SCROLL_DOWN);
        PD.displayReset(2);
      }
    }

    reSwitchPressed = readRotEncSwitch();

    if (millis() - lastZone0Time >= 1000) {
      displayTime();
      lastZone0Time = millis();
    }

    PD.displayAnimate();

  } while ((millis() - menuStartTime < menuReturnDelay) && reSwitchPressed == false);

  if (reSwitchPressed == true) {
    strcpy(pd_message2, msgSelected);
    PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, 1000, PA_PRINT, PA_NO_EFFECT);
    while (!PD.getZoneStatus(2))
      PD.displayAnimate();

#ifdef DEBUG
    Serial.println(pd_message2);
#endif
  } else
    menuItem0 = prevMenuItem0;

  resetDisplayZone2();
  PD.displayZoneText(2, pd_message2, scrollAlign, scrollSpeedMain, scrollPause, scrollEffect, scrollEffect);

#ifdef DEBUG
  Serial.println("Exiting menu");
#endif
}

void displayMenuItem0() {
  sprintf(pd_message2, "%2d %s", menuItem0, menu[menuItem0].c_str());

#ifdef CONVERT_UTF8
  utf8AsciiStr(pd_message2).toCharArray(pd_message2, BUF_SIZE);
#endif
 
  PD.displayReset(2);

#ifdef DEBUG
  Serial.print("Menu: ");
  Serial.println(pd_message2);
#endif
}

int readRotEnc() {
  int reCurrentPos;
  int reResult = 0;

  RE_encoder.tick();

  reCurrentPos = RE_encoder.getPosition();
  if (reCurrentPos < reLastPos)
    reResult = -1;
  else if (reCurrentPos > reLastPos)
    reResult = 1;

  reLastPos = reCurrentPos;
  return reResult;
}

bool readRotEncSwitch() {
  int reSwitchState = digitalRead(RE_SWITCH);
  if (reSwitchState == LOW && reLastSwitchState == HIGH) {
    reLastSwitchState = reSwitchState;
    return true;
  }
  reLastSwitchState = reSwitchState;
  return false;
}

//
// web server
//

void checkWebAccess() {
  String currentLine = "";
  char oldMessage[BUF_SIZE] = { "" };
  char c;
  int customText, endOfText;  
  WiFiClient client = webServer.available();

  if (!client)
    return;
    
#ifdef DEBUG
  Serial.println("Webserver client connected");
#endif

  strcpy(oldMessage, pd_message2);
  
  for (int i = 0; i < BUF_SIZE; i++) {
    if (oldMessage[i] == '\0') break;
    if (oldMessage[i] == degCascii) oldMessage[i] = 'C';                    // replace Celsius symbol with C for web page
  }
  
  for (int i = 0; i < BUF_SIZE; i++) {
    if (oldMessage[i] == '\0') break;
    if (oldMessage[i] == degFascii) oldMessage[i] = 'F';                    // replace Fahrenheid symbol with F for page
  }
  
  while (client.connected()) {
    if (client.available()) {
      c = client.read();

      if (c == '\n') {
        
#ifdef DEBUG
  if (currentLine.indexOf("GET /") >= 0) {
    Serial.print("Webserver client request: ");
    Serial.println(currentLine);
  }
#endif

        // check the GET statemnt

        customText = currentLine.indexOf("GET /?customtext=");
        if (customText >= 0) {
          endOfText = currentLine.indexOf("HTTP") - 1;
          parseGet((customText + 17), endOfText, currentLine).toCharArray(pd_messageUser, BUF_SIZE);

#ifdef DEBUG
          Serial.print("Webserver custom text: ");
          Serial.println(pd_messageUser);
#endif
          strcpy(oldMessage, pd_messageUser);
          menuItem0 = menuItemUser;
          loopCounter2 = 0;
          resetDisplayZone2();
          
#ifdef BUZZER
          playBuzzer();
#endif

        } else if (currentLine.indexOf("GET /nextItem") >= 0)
          resetDisplayZone2();
        else if (currentLine.indexOf("GET /carousel") >= 0)
          menuItem0 = menuItemDefault;
        else if (currentLine.indexOf("GET /nomessages") >= 0) {
          resetDisplayZone2();
          menuItem0 = menuItemNoMess;
        } else if (currentLine.indexOf("GET /youtube") >= 0)
          menuItem0 = menuItemYouTube;
        else if (currentLine.indexOf("GET /weather") >= 0)
          menuItem0 = menuItemWeather;
        else if (currentLine.indexOf("GET /stock") >= 0)
          menuItem0 = menuItemStock;
        else if (currentLine.indexOf("GET /wikipedia") >= 0)
          menuItem0 = menuItemWiki;
        else if (currentLine.indexOf("GET /reset") >= 0)
          ESP.restart();
      
        // display the web page
        if (currentLine.length() == 0) {
#ifdef DEBUG
  Serial.println("Displaying website");
#endif

          client.println("HTTP/1.1 200 OK");
          client.println("Content-type:text/html");
          client.println("Connection: close");
          client.println();
        
          // display the HTML web page
          client.println("<!DOCTYPE html><html>");
          client.println("<head>");
          client.println("<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\">");
          client.print("<meta http-equiv=\"refresh\" content=\"");
          client.print(webpageRefreshTime);
          client.print(";http://");
          client.print(WiFi.localIP());
          client.println("\" >");
          client.println("<style>html { font-family: Helvetica; margin: 0px 25%; text-align: center; } body { background-color: #EEEEEE; }");
          client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 6px 16px;");
          client.println("text-decoration: none; font-size: 14px; font-weight: bold; margin: 4px; cursor: pointer;}");
          client.println("</style>");
          client.println("</head>");
        
          client.println("<body>");
          client.println("<h2>LED Message Board Web Interface</h2>");
          client.println("<h4>by ericBcreator</h4><br>");
          client.println("<p>Last displayed: </p>");
          client.println("<font color=\"red\"><b><p>");

          if (strlen(pd_url) == 0)
            client.println(oldMessage);
          else {
            client.print("<a href=\"");
            client.print(pd_url);
            client.print("\" target=\"_blank\">");
            client.print(oldMessage);
            client.println("</a>");
          }
            
          client.println("</b></p></font><br>");
          
          client.println("<form action=\"/\" method=\"get\">");
          client.println("<br>Input your message to display and click 'Send'<br><br>");
          client.println("<INPUT type=\"text\" size=\"60\" name=\"customtext\"<br>");
          client.println("<INPUT type=\"submit\" value=\"Send\">");
          client.println("</form><br>");

          client.print("<p>");
          client.println("<a href=\"/nextItem\"><button class=\"button\">Next item</button></a>");
          client.println("<a href=\"/carousel\"><button class=\"button\">Default carousel</button></a>");
          client.println("<a href=\"/wikipedia\"><button class=\"button\">Wikipedia</button></a>");
          client.println("<a href=\"/weather\"><button class=\"button\">Weather</button></a>");
          client.println("<a href=\"/stock\"><button class=\"button\">Stock info</button></a>");
          client.print("</p><p>");
          client.println("<a href=\"/youtube\"><button class=\"button\">Youtube</button></a>");
          client.println("<a href=\"/\"><button class=\"button\">Refresh</button></a>");
          client.println("<a href=\"/nomessages\"><button class=\"button\">No messages</button></a>");
          client.println("<a href=\"/reset\"><button class=\"button\">Reset the ESP32</button></a>");
          client.print("<p>");
          
          client.println("</body></html>");
          client.println();

          break;
        } else
          currentLine = "";

      } else if (c != '\r')
        currentLine += c;
    }
  }
 
  client.stop();

...

This file has been truncated, please download it to see its full contents.

LED Matrix board 20181111 shared

Arduino
/*
***************************************************************************
  A 1024 (16x8x8) LED matrix WiFi message board with rotary encoder,
  BME280 sensor and a web interface with new message notification
  by ericBcreator

  It connects to Wifi and a timeserver and then displays date, time,
  sunrise and sunset for a set location, sensors reading of the 
  temperature, humidity and air pressure, current weather and forecasts 
  from openweather.map for multiple cities, top headlines from NewsApi.org 
  and stock data from Alpha Vantage.
  
  A rotary encoder selects between the default display 'carousel',
  one news category, stock or weather data, no messages or it resets 
  the ESP.

  There is web access (enter the IP address of the ESP32 which is displayed
  at startup in a browser) for displaying custom texts and access to 
  some controls. A buzzer plays a notification sound whenever a new message
  is received.
***************************************************************************
  last updated 20181111 by ericBcreator

  components:
  - ESP32
  - 16 x 8x8 MAX7219 LED matrices
  - BME280 sensor
  - rotary encoder
***************************************************************************
  This code is free for personal use, not for commercial purposes.
  Please leave this header intact.

  contact: ericBcreator@gmail.com
***************************************************************************
*/

#define FN_ARRAY_SIZE(x) (sizeof(x)/sizeof(x[0]))  // Generic macro for obtaining number of elements of an array

//#define DEBUG                                 // enable debugging messages
//#define DEBUG_FAST                            // skip wifi/timerserver connect messages
//#define DEBUG_NEWSAPI_NC                      // do not connect to Newsapi.org (to save the number of daily requests)

//#define LOCAL_LANG                            // @EB-setup use local language (user-definable, otherwise use English)
#define CONVERT_UTF8                            // @EB-setup converts UTF8 'special' characters to the Parola font set
#define WEBSERVER                               // @EB-setup enable webserver and web access
#define BUZZER                                  // @EB-setup use buzzer for new messages from the webserver

//
// Include libraries
//

#include <Adafruit_BME280.h>                    // BME280 library
#include <ArduinoJson.h>                        // Arduino Json library
#include <Dusk2Dawn.h>                          // Dusk2Dawn sunrise sunset library
#include <MD_MAX72xx.h>                         // needed for Parola
#include <MD_Parola.h>                          // Parola LED library
#include <RotaryEncoder.h>                      // Rotary encoder library
#include <TimeLib.h>                            // Time library
#include <WiFiClientSecure.h>                   // WiFi library
#include <WiFiUdp.h>                            // Udp library

#include "font_data_numeric.h"                  // local file: font data for time

#ifdef CONVERT_UTF8
  #include "font_data_utf8.h"                   // local file: font data for UTF8 support
#endif


//
// LED matrix setup and pins definition
//

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW       // @EB-setup define the type of hardware connected
#define CLK_PIN     18                          // @EB-setup CLK / SCK pin number  (EB setup: yellow)
#define DATA_PIN    23                          // @EB-setup DIN / MOSI pin number (EB setup: brown)
#define CS_PIN      5                           // @EB-setup LOAD / SS pin number  (EB setup: green)
#define MAX_DEVICES 16                          // @EB-setup num of LED matrices
#define MAX_ZONES   3                           // num of zones
MD_Parola PD = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);

//
// BME280 setup
//

Adafruit_BME280 bme;                            // connect via I2C pin 21 SDA and 22 SCL
uint8_t bme_address = 0x76;                     // @EB-setup set to 0x76 or 0x77

//
// Rotary encoder setup
//

#define RE_PINA 16                              // @EB-setup pinA / clock pin number
#define RE_PINB 17                              // @EB-setup pinB / data pin number
#define RE_SWITCH 4                             // @EB-setup switch / switch pin number
RotaryEncoder RE_encoder(RE_PINA, RE_PINB);     // initialize the rotary encoder
int reLastPos = 0;
int reLastSwitchState = 0;

//
// Buzzer setup
//

#define BUZZER_PIN 15                           // @EB-setup buzzer pin
int buzz_volume = 50;                           // @EB-share buzzer volume 0-255
int buzz_channel = 0;
int buzz_resolution = 8;

//
// Initialize variables
//

/* WiFi connection */
char * ssid     = "######################";     // @EB-setup WiFi SSID
char * password = "######################";     // @EB-setup WiFi password
int wifiTimeout = 60000;                        // WiFi timeout (in milliseconds)

/* location for sunrise and sunset */
char d2d_City[] = "Amsterdam";                  // @EB-share City name
float d2d_longitude = 52.37936;                 // @EB-share
float d2d_lattitude = 4.900293;                 // @EB-share
int d2d_timezone = 1;                           // @EB-share UTC timezone offset

/* openweathermap connection */
char * ow_servername = "api.openweathermap.org";            // remote server openweathermap.org
const int ow_port = 80;                                     // port
String ow_APIKEY = "################################";      // @EB-setup personal api key for retrieving the weather data. leave empty to skip

#ifdef LOCAL_LANG
  const String ow_language = "NL";                          // @EB-setup openweather language
#else
  const String ow_language = "EN";
#endif

int ow_cityIDLoop = 0;

// get the ID at https://openweathermap.org/ type the city, click search and click on the town
// then check the link, like this: https://openweathermap.org/city/5128581 5128581 is the ID for New York
String ow_cityIDs[] = {
  "2759794",  // @EB-share Amsterdam
  "264371",   // @EB-share Athens
  "3513090",  // @EB-share Willemstad
};

/* newsapi connection */
char * na_servername = "newsapi.org";                       // remote server newsapi.org
const int na_port = 80;                                     // port

String na_APIKEY = "################################";      // @EB-setup personal api key. leave empty to skip

#ifdef LOCAL_LANG
  const String na_country = "NL";                           // @EB-setup country code for news
#else
  const String na_country = "US";
#endif

const int na_numOfArticles = 20;                            // @EB-share number of top news headlines to display
const int na_numOfArticlesNewsOnly = 20;                    // @EB-share number of selected news category headlines to display
int na_article = 0;

/* alpha vantage stock connection */
char * av_servername = "www.alphavantage.co";               // remote server alphavantage
const int av_port = 443;                                    // port for secure connection

String av_APIKEY = "################################";      // @EB-setup personal api key. leave empty to skip

// @EB-share
String av_symbolIDs[] = { "DJIA", "IXIC", "SPX", "AAPL", "GOOGL", "BTCUSD" } ;
String av_symbolName[] = { "Dow Jones index", "Nasdaq index", "S&P 500 index", "Apple", "Google", "Bitcoin" } ;

int av_symbolIDLoop = 0;

/* message definitions */
#ifdef LOCAL_LANG // @EB-share
  char msgWifiConnecting[] =                    "...wifi verbinden...";
  char msgWifiConnected[] =                     "verbonden met";
  char msgWifiFailed[] =                        "*wifi verbinding mislukt*";
  char msgConnectToTimeserver[] =               "...verbinden met de tijdserver...";
  char msgTimeserverFailed[] =                  "* verbinding met tijdserver mislukt *";
  char msgDSTon[] =                             "verbonden,   zomertijd  ";
  char msgDSToff[] =                            "verbonden,  wintertijd  ";
  char msgOpenweatherFailed[] =                 "* verbinding met Openweathermap mislukt *";
  char msgNewsapiFailed[] =                     "* verbinding met Newsapi.org mislukt *";
  char msgNewsapiError[] =                      "* fout bij ophalen van nieuws van Newsapi.org *";
  char msgAlphaVantageFailed[] =                "* verbinding met AlphaVantage.co mislukt *";
  
  char msgNews[] =                              "Laatste nieuws van";
  char msgSunrise[] =                           "Zonsopkomst";
  char msgSunset[] =                            "Zonsondergang";
  char msgCurrentWeather[] =                    "Het weer in";
  char msgForecast[] =                          "Weersverwachting voor";
  char msgTemp[] =                              "temperatuur";
  char msgHumidity[] =                          "vochtigheid";
  char msgClouds[] =                            "bewolking";
  char msgRain[] =                              "neerslag";
  char msgWind[] =                              "wind";
  char msgAVPrevious[] =                        "vorige slotkoers";
  char msgAVCurrent[] =                         "laatste koers";
  
  char msgSelected[] =                          "OK";
#else
  char msgWifiConnecting[] =                    "...wifi connecting...";
  char msgWifiConnected[] =                     "connected to";
  char msgWifiFailed[] =                        "*failed connecting to wifi*";
  char msgConnectToTimeserver[] =               "...connecting to the timeserver...";
  char msgTimeserverFailed[] =                  "* failed connecting to the timeserver *";
  char msgDSTon[] =                             "connected, DST active";
  char msgDSToff[] =                            "connected, no DST";
  char msgOpenweatherFailed[] =                 "* failed connecting to Openweathermap *";
  char msgNewsapiFailed[] =                     "* failed connecting to Newsapi.org *";
  char msgNewsapiError[] =                      "* failed retrieving news from Newsapi.org ";
  char msgAlphaVantageFailed[] =                "* failed connecting to AlphaVantage.co *";
  
  char msgNews[] =                              "Latest news from";
  char msgSunrise[] =                           "Sunrise";
  char msgSunset[] =                            "Sunset";
  char msgCurrentWeather[] =                    "Current weather in";
  char msgForecast[] =                          "Weather forecast for";
  char msgTemp[] =                              "temperature";
  char msgHumidity[] =                          "humidity";
  char msgClouds[] =                            "clouds";
  char msgRain[] =                              "rain";
  char msgWind[] =                              "wind";
  char msgAVPrevious[] =                        "previous close";
  char msgAVCurrent[] =                         "latest";
  
  char msgSelected[] =                          "OK";
#endif

/* menu setup */
// @EB-share
#ifdef LOCAL_LANG
  String menu[] { 
    "Standaard mozaek", 
    "Nieuws headlines", 
    "Economie", 
    "Technologie", 
    "Algemeen", 
    "Entertainment", 
    "Gezondheid", 
    "Wetenschap", 
    "Sport", 
    "Beursinfo van Alpha Vantage", 
    "Weerberichten", 
    "Eigen bericht", 
    "Geen berichten", 
    "- Reset" 
  } ;
#else
  String menu[] { 
    "Default carousel", 
    "Top headlines", 
    "Business", 
    "Technology", 
    "General", 
    "Entertainment", 
    "Health", 
    "Science", 
    "Sports", 
    "Stock info from Alpha Vantage", 
    "Weather information", 
    "User defined message",
    "No messages", 
    "- Reset" 
  } ;
#endif

String menuNewsApi[] { "", "&category=business", "&category=technology", "&category=general", "&category=entertainment", "&category=health", "&category=science", "&category=sports" } ;

int menuReturnDelay = 10000;                    // @EB-share delay before the menu exits
int menuItem0 = 0;                              // @EB-share default menu selection
int prevMenuItem0 = menuItem0;
int menuLen = FN_ARRAY_SIZE(menu);
int menuNewsCat = 0;

/* time server */
int timeServerDelay = 1000;                     // delay for the time server to reply
int timeServerPasses = 4;                       // number of tries to connect to the time server before timing out
boolean timeServerConnected = false;            // is set to true when the time is read from the server

unsigned int localPort = 2390;                  // local port to listen for UDP packets
IPAddress timeServerIP;                         // IP address of random server
const char* ntpServerName = "pool.ntp.org";     // server pool
byte packetBuffer[48];                          // buffer to hold incoming and outgoing packets
int timeZoneoffsetGMT = d2d_timezone * 3600;    // offset from Greenwich Meridan Time
boolean DST = false;                            // daylight saving time
WiFiUDP clockUDP;                               // initialize a UDP instance

/* web server */
WiFiServer webServer(80);
int webpageRefreshTime = 15;                    // @EB-share delay time before the webserver page will auto refresh

/* Parola */
#define BUF_SIZE 150                            // @EB-share buffer size for zone 2
char pd_message0[9] = { "" };                   // message buffer zone 0 (time)
char pd_message1[20] = { "" };                  // message buffer zone 1
char pd_message2[BUF_SIZE] = { "" };            // message buffer zone 2

char pd_messageUser[BUF_SIZE] = { "--- set your custom message here ---" }; // @EB-share

char tmpBuffer[BUF_SIZE] = { "" };

uint8_t degC[] = { 6, 3, 3, 56, 68, 68, 68 };   // Deg C character
uint8_t degF[] = { 6, 3, 3, 124, 20, 20, 4 };   // Deg F character

int pd_brightness = 3;                          // @EB-share brightness (0-15)
int scrollSpeedSetup = 20;                      // @EB-share frame delay setup
int scrollSpeedMain = 40;                       // @EB-share frame delay main loop
int scrollPause = 1500;                         // @EB-share delay after displaying the message (in milliseconds)
textEffect_t scrollEffect = PA_SCROLL_LEFT;     // @EB-share default effect
textPosition_t scrollAlign = PA_CENTER;         // @EB-share default align
textEffect_t zone1ScrollEffect = PA_SCROLL_UP;  // @EB-share default effect
int zone1Pause = 10000;                         // @EB-share delay after displaying the zone 1 message (in milliseconds)

/* general */
boolean tempCelsius = true;                     // @EB-setup set false for Fahrenheit

boolean colonFlag = true;
static uint8_t loopCounter1 = 0;
static uint8_t loopCounter2 = 0;
static uint32_t lastZone0Time = 0;
static uint32_t lastZone1Time = 0;

//
// Setup
//

void setup() {
#ifdef DEBUG
  Serial.begin(57600);                          // for debugging
  Serial.println("");
  Serial.println("Setup...");
#endif

#ifdef BUZZER  
  ledcSetup(buzz_channel, 0, buzz_resolution);  // setup the buzzer
  ledcAttachPin(BUZZER_PIN, buzz_channel);  
#endif

  setupPD();                                    // start the Parola display

  bme.begin(bme_address);                       // start the BME280 sensor

  connectToWifi();                              // connect to Wifi

  if (WiFi.status() == WL_CONNECTED) {
    getTimeFromServer();                        // connect to the timeserver and get the local time

#ifdef WEBSERVER
    webServer.begin();
    #ifdef DEBUG
      Serial.println("Webserver started");
    #endif
#endif
  }
  
  PD.setSpeed(2, scrollSpeedMain);
}

//
// Main loop
//

void loop() {
  if ((millis() - lastZone0Time >= 1000)) {                                         // wait for 1 second before displaying the time
    displayTime();
    lastZone0Time = millis();
  }

  if ((millis() - lastZone1Time >= zone1Pause)) {                                   // display the zone 1 animation after a pause
    lastZone1Time = millis();

    switch (loopCounter1) {
      case 0:     displayDateShort();           loopCounter1++;   break;            // display short date
      case 1:     displayTemp();                loopCounter1++;   break;            // display temperature
      case 2:     displayHumidity();            loopCounter1++;   break;            // display humidity
      case 3:     displayPressure();            loopCounter1 = 0; break;            // display pressure
    }
  }

  if (readRotEnc() != 0) {                                                          // if the rotary encoder is turned go to the menu
    selectMenu0();
    if (menuItem0 == menuLen - 1)                                                   // reset
      ESP.restart();
      
    if (menuItem0 != prevMenuItem0) {
      na_article = 0;
      menuNewsCat = 0;
      loopCounter2 = 0;
      prevMenuItem0 = menuItem0;
    }
  } else if (readRotEncSwitch() == true)                                            // if the rotary encoder is pushed go to the next item to display
    resetDisplayZone2();

#ifdef WEBSERVER
  doWebServer();                                                                    // display the website and check for input
#endif

  if (WiFi.status() == WL_CONNECTED && (menuItem0 != menuLen - 2)) {	              // only display messages if WiFi is connected and menu item 'No messages' is not selected
    if (PD.getZoneStatus(2)) {                                                      // display the next item when the animation of the current item has finished

#ifdef DEBUG
      if (loopCounter2 == 0) {
        Serial.println("");
        Serial.println(pd_message0);
      }
#endif

      if (menuItem0 == 0) {                                                         // default setting, loop trough all items
        switch (loopCounter2) {
          case 0:     displayDate();            loopCounter2++; break;              // display date
          case 1:     displaySunrise();         loopCounter2++; break;              // display sunrise time
          case 2:     displaySunset();          loopCounter2++; break;              // display sunset time
          case 3:     displayWeatherData();     loopCounter2++; break;              // 1st call current weather, 2nd call weather forecast and move to next city
          case 4:     displayNewsapiData();     loopCounter2++; break;              // display newsapi items
          case 5:     displayAVStockData();     loopCounter2++; break;              // display alpha vantage stock data
          default:    loopCounter2 = 0;
        }

      } else {
          if (loopCounter2 == 10)
            displayDate();
          else {
            if (menuItem0 == menuLen - 3)
              displayUserMessage();                                                 // user defined message (or posted on the webserver)
            else if (menuItem0 == menuLen - 4)       
              displayWeatherData();                                                 // only show weather data
            else if (menuItem0 == menuLen - 5)  
              displayAVStockData();                                                 // only show stock data
            else {                               
              menuNewsCat = menuItem0 - 1;
              displayNewsapiData();                                                 // only show news
            }
          }
        loopCounter2 = (++loopCounter2) % 11;                                       // show 10 items and then display the date again
      }

#ifdef CONVERT_UTF8
      if (menuItem0 != menuLen - 3)                                                 // don't do a double conversion of the webserver text, it will erase the UTF characters
        utf8AsciiStr(pd_message2).toCharArray(pd_message2, BUF_SIZE);               // convert UTF8 characters
#endif
    }
  }

  PD.displayAnimate();
}

//
// Display functions
//

void setupPD() {
  PD.begin(MAX_ZONES);                          // start the Parola display

  PD.setZone(0, 0, 3);                          // zone 0 time
  PD.setZone(1, 4, 7);                          // zone 1 date, temperature, humidity
  PD.setZone(2, 8, MAX_DEVICES - 1);            // zone 2 messages

  PD.setFont(0, numeric7Seg);                   // set numeric font for the time

#ifdef CONVERT_UTF8
  PD.setFont(2, ExtASCII);                      // Parola font set with UTF8 support
#endif

  PD.setIntensity(pd_brightness);               // brightness (0-15)
  PD.setInvert(0, false);                       // invert false (text on, background off)
  PD.setInvert(1, false);                       // invert false (text on, background off)
  PD.setInvert(2, false);                       // invert false (text on, background off)

  PD.displayZoneText(0, pd_message0, PA_CENTER, 0, 0, PA_PRINT, PA_NO_EFFECT);
  PD.displayZoneText(1, pd_message1, PA_CENTER, scrollSpeedMain, scrollPause, zone1ScrollEffect, PA_NO_EFFECT);
  PD.displayZoneText(2, pd_message2, scrollAlign, scrollSpeedSetup, scrollPause, scrollEffect, scrollEffect);

  PD.addChar('^', degC);
  PD.addChar('~', degF);
}

void displayMessage(char * message) {
  snprintf(pd_message2, sizeof(pd_message2), "%s", message);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif

#if !defined (DEBUG_FAST)
  PD.displayReset(2);
  while (!PD.getZoneStatus(2))
    PD.displayAnimate();
#endif
}

void displayTime() {
  time_t t = now();

  if (hour(t) == 3 && minute(t) == 0)           // at 03:00 get the current time from the timeserver for readjusting and rereading DST
    getTimeFromServer();

  snprintf(pd_message0, sizeof(pd_message0), "%02d%c%02d", hour(t), (colonFlag ? ':' : ' '), minute(t));
  PD.displayReset(0);

  colonFlag = !colonFlag;
}

void displayDateShort() {
  time_t t = now();
  String monthStr = monthAsStringShort(month(t));
  snprintf(pd_message1, sizeof(pd_message1), "%d %s", day(t), monthStr.c_str());
  PD.displayReset(1);
}

void displayTemp() {
  float bmeTemp = bme.readTemperature();
  if (tempCelsius)
    sprintf(pd_message1, "%2.1f ^", bmeTemp);
  else {
    bmeTemp = (bmeTemp * 9 / 5) + 32;
    sprintf(pd_message1, "%2.1f ~", bmeTemp);
  }

#ifdef DEBUG
  Serial.println(pd_message1);
#endif
  PD.displayReset(1);
}

void displayHumidity() {
  float bmeHumidity = bme.readHumidity();
  sprintf(pd_message1, "%2.0f%% rh", bmeHumidity);
  
#ifdef DEBUG
  Serial.println(pd_message1);
#endif
  PD.displayReset(1);
}

void displayPressure() {
  float bmePressure = bme.readPressure() / 100.0f;
  sprintf(pd_message1, "%4.0f h", bmePressure); // hPa doesn't fit...
  
#ifdef DEBUG
  Serial.println(pd_message1);
#endif
  PD.displayReset(1);
}

void displayDate() {
  time_t t = now();
  String dayStr = dayAsString(weekday(t));
  String monthStr = monthAsString(month(t));
  sprintf(pd_message2, "%s %d %s %04d", dayStr.c_str(), day(t), monthStr.c_str(), year(t));
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displaySunrise() {
  time_t t = now();
  char d2dSunrise[] = "00:00";

  Dusk2Dawn d2d(d2d_longitude, d2d_lattitude, d2d_timezone);
  Dusk2Dawn::min2str(d2dSunrise, d2d.sunrise(year(t), month(t), day(t), DST));
  sprintf(pd_message2, "%s : %s %s", d2d_City, msgSunrise, d2dSunrise);
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displaySunset() {
  time_t t = now();
  char d2dSunset[] = "00:00";

  Dusk2Dawn d2d(d2d_longitude, d2d_lattitude, d2d_timezone);
  Dusk2Dawn::min2str(d2dSunset, d2d.sunset(year(t), month(t), day(t), DST));
  sprintf(pd_message2, "%s : %s %s", d2d_City, msgSunset, d2dSunset);
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void displayUserMessage() {
  strcpy(pd_message2, pd_messageUser);
  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

void resetDisplayZone2() {
  strcpy(pd_message2, "");
  PD.displayClear(2);
  PD.displayReset(2);
}

//
// menu functions
//

void selectMenu0() {
#ifdef DEBUG
  Serial.println("Entering menu");
#endif

  int reResult;
  int lastReResult;
  int menuStartTime = millis();
  bool reSwitchPressed = false;

//  resetDisplayZone2;
  PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, scrollPause, PA_PRINT, PA_CLOSING);
  displayMenuItem0();

  do {
    reResult = readRotEnc();

    if (reResult < 0) {             // down
      --menuItem0;
      if (menuItem0 < 0)
        menuItem0 = FN_ARRAY_SIZE(menu) - 1;
      PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, scrollPause, PA_SCROLL_UP, PA_SCROLL_UP);

    } else if (reResult > 0) {      // up
      menuItem0 = (++menuItem0) % FN_ARRAY_SIZE(menu);
      PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, scrollPause, PA_SCROLL_DOWN, PA_SCROLL_DOWN);
    }

    if (reResult != 0) {
      menuStartTime = millis();
      displayMenuItem0();
      lastReResult = reResult;
    } else {
      if (PD.getZoneStatus(2)) {
        if (lastReResult < 0)
          PD.setTextEffect(2, PA_SCROLL_LEFT, PA_SCROLL_UP);
        else
          PD.setTextEffect(2, PA_SCROLL_LEFT, PA_SCROLL_DOWN);
        PD.displayReset(2);
      }
    }

    reSwitchPressed = readRotEncSwitch();

    if (millis() - lastZone0Time >= 1000) {
      displayTime();
      lastZone0Time = millis();
    }

    PD.displayAnimate();

  } while ((millis() - menuStartTime < menuReturnDelay) && reSwitchPressed == false);

  if (reSwitchPressed == true) {
    strcpy(pd_message2, msgSelected);
    PD.displayZoneText(2, pd_message2, PA_LEFT, scrollSpeedSetup, 1000, PA_PRINT, PA_NO_EFFECT);
    while (!PD.getZoneStatus(2))
      PD.displayAnimate();

#ifdef DEBUG
    Serial.println(pd_message2);
#endif
  } else
    menuItem0 = prevMenuItem0;

  resetDisplayZone2();
  PD.displayZoneText(2, pd_message2, scrollAlign, scrollSpeedMain, scrollPause, scrollEffect, scrollEffect);

#ifdef DEBUG
  Serial.println("Exiting menu");
#endif
}

void displayMenuItem0() {
  sprintf(pd_message2, "%2d %s", menuItem0, menu[menuItem0].c_str());

#ifdef CONVERT_UTF8
  utf8AsciiStr(pd_message2).toCharArray(pd_message2, BUF_SIZE);
#endif
 
  PD.displayReset(2);

#ifdef DEBUG
  Serial.print("Menu: ");
  Serial.println(pd_message2);
#endif
}

int readRotEnc() {
  int reCurrentPos;
  int reResult = 0;

  RE_encoder.tick();

  reCurrentPos = RE_encoder.getPosition();
  if (reCurrentPos < reLastPos)
    reResult = -1;
  else if (reCurrentPos > reLastPos)
    reResult = 1;

  reLastPos = reCurrentPos;
  return reResult;
}

bool readRotEncSwitch() {
  int reSwitchState = digitalRead(RE_SWITCH);
  if (reSwitchState == LOW && reLastSwitchState == HIGH) {
    reLastSwitchState = reSwitchState;
    return true;
  }
  reLastSwitchState = reSwitchState;
  return false;
}

//
// web server
//

void doWebServer() {
  String currentLine = "";
  char oldMessage[BUF_SIZE] = { "" };
  char c;
  int customText, endOfText;
  WiFiClient client = webServer.available();

  if (client) {
#ifdef DEBUG
  Serial.println("Webserver client connected");
#endif

  strcpy(oldMessage, pd_message2);
  
  while (client.connected()) {
    if (client.available()) {
      c = client.read();

      if (c == '\n') {
#ifdef DEBUG
  Serial.println(currentLine);
#endif

        // check the GET statemnt

        customText = currentLine.indexOf("GET /?customtext=");
        if (customText >= 0) {
          endOfText = currentLine.indexOf("HTTP") - 1;
          parseGet((customText + 17), endOfText, currentLine).toCharArray(pd_messageUser, BUF_SIZE);

#ifdef DEBUG
          Serial.print("Webserver custom text: ");
          Serial.println(pd_messageUser);
#endif
          strcpy(oldMessage, pd_messageUser);
          menuItem0 = menuLen - 3;
          loopCounter2 = 0;
          resetDisplayZone2();
          
#ifdef BUZZER
          playBuzzer();
#endif

        } else if (currentLine.indexOf("GET /nextItem") >= 0)
          resetDisplayZone2();
        else if (currentLine.indexOf("GET /carousel") >= 0)
          menuItem0 = 0;
        else if (currentLine.indexOf("GET /reset") >= 0)
          ESP.restart();
            
        // display web page

        if (currentLine.length() == 0) {

#ifdef DEBUG
  Serial.println("Display website");
#endif

          client.println("HTTP/1.1 200 OK");
          client.println("Content-type:text/html");
          client.println("Connection: close");
          client.println();
        
          // display the HTML web page
          client.println("<!DOCTYPE html><html>");
          client.println("<head>");
          client.println("<meta http-equiv=\"Content-Type\" content=\"text/html;charset=ISO-8859-1\">");
          client.print("<meta http-equiv=\"refresh\" content=\"");
          client.print(webpageRefreshTime);
          client.print(";http://");
          client.print(WiFi.localIP());
          client.println("\" >");
          client.println("<style>html { font-family: Helvetica; margin: 0px auto; text-align: center;}");
          client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 8px 20px;");
          client.println("text-decoration: none; font-size: 15px; margin: 2px; cursor: pointer;}");
          client.println("</style>");
          client.println("</head>");
        
          client.println("<body><h1>LED Matrix Board Web Server</h1><br>");
          client.println("<p>Last displayed: </p>");
          client.println("<font color=\"red\">");
          client.println(oldMessage);
          client.println("</font>");
          
          client.println("<form action=\"/\" method=\"get\">");
          client.println("<br><br>Input your custom text to display and click 'Send'<br><br>");
          client.println("<INPUT type=\"text\" size=\"50\" name=\"customtext\"<BR>");
          client.println("<INPUT type=\"submit\" value=\"Send\">");
          client.println("</form>");

          client.println("<p><a href=\"/nextItem\"><button class=\"button\">Next item</button></a>");
          client.println("<a href=\"/carousel\"><button class=\"button\">Default carousel</button></a></p>");
          client.println("<a href=\"/reset\"><button class=\"button\">Reset the ESP32</button></a></p>");
          
          client.println("</body></html>");
          client.println();

          break;
        } else
          currentLine = "";

      } else if (c != '\r')
        currentLine += c;
    }
  }
 
  client.stop();

#ifdef DEBUG
  Serial.println("Webserver client disconnected");
#endif
  }
}

//
// newsapi.org data
//

void displayNewsapiData() {
  if (na_APIKEY == "")
    return;

  String url;
  String result;
  WiFiClient client;

#ifdef DEBUG_NEWSAPI_NC                         // while debugging display a test message so the request counter doesn't increase
  sprintf(pd_message2, "debugging test getting news from Newsapi.org");
  PD.displayReset(2);
  Serial.println(pd_message2);
  return;
#endif

  if (!client.connect(na_servername, na_port))
    return;

  ++na_article;                                 // increase article counter

  url = "/v2/top-headlines?country=" + na_country + "&pageSize=1&page=" + String(na_article) + menuNewsApi[menuNewsCat].c_str() + "&apiKey=" + na_APIKEY;

  client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + na_servername + "\r\n" + "Connection: close\r\n\r\n");

#ifdef DEBUG
  Serial.println(String("GET ") + url + " HTTP/1.1\r\n");
#endif

  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 1000) {
      client.stop();
      return;
    }
  }

  result = "";
  while (client.available())
    result = client.readStringUntil('\r');

  client.stop();
  result.replace('[', ' ');
  result.replace(']', ' ');

  char jsonArray [result.length() + 1];
  result.toCharArray(jsonArray, sizeof(jsonArray));
  jsonArray[result.length() + 1] = '\0';

  StaticJsonBuffer<1024> json_buf;
  JsonObject &root = json_buf.parseObject(jsonArray);

  if (!root.success())
    strcpy(pd_message2, msgNewsapiFailed);
  else {
    if (root["status"] == "error") {
      sprintf(pd_message2, msgNewsapiError);
    } else {
      String asou = root["articles"]["source"]["name"];
      String atit = root["articles"]["title"];

      if (menuItem0 == 0) {                                         // the 'latest news' intro when displaying news items only
        snprintf(pd_message2, sizeof(pd_message2), "%s %s : %s", msgNews, asou.c_str(), atit.c_str());
        na_article = na_article % na_numOfArticles;                 // reset after na_numOfArticles articles
      } else {
        snprintf(pd_message2, sizeof(pd_message2), "%s : %s", asou.c_str(), atit.c_str());
        na_article = na_article % na_numOfArticlesNewsOnly;
      }
    }
  }

  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
}

//
// weatherdata
//

void displayWeatherData() {
  if (ow_APIKEY == "")
    return;

  static bool ow_current = true;
  String url;
  String result;
  WiFiClient client;

  if (!client.connect(ow_servername, ow_port))
    return;

  String cityID = ow_cityIDs[ow_cityIDLoop];

  if (ow_current)                               // get current weather check weather properties at https://openweathermap.org/current
    url = "/data/2.5/weather?id=";
  else                                          // get forecast check properties forecasts at https://openweathermap.org/forecast5
    url = "/data/2.5/forecast?id=";

  url = url + cityID + "&units=metric&cnt=1&lang=" + ow_language + "&APPID=" + ow_APIKEY;
  client.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + ow_servername + "\r\n" + "Connection: close\r\n\r\n");

  unsigned long timeout = millis();
  while (client.available() == 0) {
    if (millis() - timeout > 1000) {
      client.stop();
      return;
    }
  }

  result = "";
  while (client.available())
    result = client.readStringUntil('\r');

  client.stop();
  result.replace('[', ' ');
  result.replace(']', ' ');

  char jsonArray [result.length() + 1];
  result.toCharArray(jsonArray, sizeof(jsonArray));
  jsonArray[result.length() + 1] = '\0';

  StaticJsonBuffer<1024> json_buf;
  JsonObject &root = json_buf.parseObject(jsonArray);

  if (!root.success())
    strcpy(pd_message2, msgOpenweatherFailed);

  else {
    if (ow_current) {
      String wcit = root["name"];
      String wdes = root["weather"]["description"];
      float  wtem = root["main"]["temp"];
      float  whum = root["main"]["humidity"];
      float  wclo = root["clouds"]["all"];
      float  wrai = root["rain"]["3h"];
      float  wwsp = root["wind"]["speed"];
      int    wwde = root["wind"]["deg"];

      snprintf(pd_message2, sizeof(pd_message2), "%s %s : %s, %s %2.1f^  %s %3.0f%%  %s %3.0f%%  %s %3.1fmm  %s %2.0fm/s %s", msgCurrentWeather, wcit.c_str(), wdes.c_str(), msgTemp, wtem, msgHumidity, whum, msgClouds, wclo, msgRain, wrai, msgWind, wwsp, getWindDirection(wwde).c_str());
    } else {
      String wcit = root["city"]["name"];
      String wdes = root["list"]["weather"]["description"];
      float  wtmi = root["list"]["main"]["temp_min"];
      float  wtma = root["list"]["main"]["temp_max"];
      float  whum = root["list"]["main"]["humidity"];
      float  wclo = root["list"]["clouds"]["all"];
      float  wrai = root["list"]["rain"]["3h"];
      float  wwsp = root["list"]["wind"]["speed"];
      int    wwde = root["list"]["wind"]["deg"];

      snprintf(pd_message2, sizeof(pd_message2), "%s %s : %s, %s %2.1f^ - %2.1f^  %s %3.0f%%  %s %3.0f%%  %s %3.1fmm  %s %2.0fm/s %s", msgForecast, wcit.c_str(), wdes.c_str(), msgTemp, wtmi, wtma, msgHumidity, whum, msgClouds, wclo, msgRain, wrai, msgWind, wwsp, getWindDirection(wwde).c_str());
    }
  }

  PD.displayReset(2);

#ifdef DEBUG
  Serial.println(pd_message2);
#endif
...

This file has been truncated, please download it to see its full contents.

font_data_numeric

C Header File
// Data file for user example user defined fonts
#ifndef FONTDATA_H
#define FONTDATA_H

MD_MAX72XX::fontType_t numeric7Seg[] PROGMEM = 
{
	0,		// 0
	0,		// 1
	0,		// 2
	0,		// 3
	0,		// 4
	0,		// 5
	0,		// 6
	0,		// 7
	0,		// 8
	0,		// 9
	0,		// 10
	0,		// 11
	0,		// 12
	0,		// 13
	0,		// 14
	0,		// 15
	0,		// 16
	0,		// 17
	0,		// 18
	0,		// 19
	0,		// 20
	0,		// 21
	0,		// 22
	0,		// 23
	0,		// 24
	0,		// 25
	0,		// 26
	0,		// 27
	0,		// 28
	0,		// 29
	0,		// 30
	0,		// 31
	1, 0,		                      //  32 - 'Space'
	0,		// 33 - '!'
	0,		// 34 - '"'
	0,		// 35 - '#'
	0,  	// 36 - '$'
	0,		// 37 - '%'
	0,		// 38 - '&'
	0,		// 39 - '''
	0,		// 40 - '('
	0,		// 41 - ')'
	0,		// 42 - '*'
	0,		// 43 - '+'
	0,		// 44 - ','
	0,		// 45 - '-'
	1, 64,		                    //  46 - '.'
	0,		// 47 - '/'
	5, 127, 65, 65, 65, 127,	    //  48 - '0'
	5, 0, 0, 127, 0, 0,			      //  49 - '1'
	5, 121, 73, 73, 73, 79,		    //  50 - '2'
	5, 73, 73, 73, 73, 127,		    //  51 - '3'
	5, 15, 8, 8, 8, 127,		      //  52 - '4'
	5, 79, 73, 73, 73, 121,		    //  53 - '5'
	5, 127, 73, 73, 73, 121,	    //  54 - '6'
	5, 1, 1, 1, 1, 127, 		      //  55 - '7'
	5, 127, 73, 73, 73, 127,	    //  56 - '8'
	5, 79, 73, 73, 73, 127, 	    //  57 - '9'
	1, 20,		// 58 - ':'
	0,		// 59 - ';'
	0,		// 60 - '<'
	0,		// 61 - '='
	0,		// 62 - '>'
	0,		// 63 - '?'
	0,		// 64 - '@'
	5, 127, 9, 9, 9, 127,		      //  65 - 'A'
	5, 127, 73, 73, 73, 54,		    //  66 - 'B'
	5, 127, 65, 65, 65, 65,		    //  67 - 'C'
	5, 127, 65, 65, 65, 62,		    //  68 - 'D'
	5, 127, 73, 73, 73, 73,		    //  69 - 'E'
	5, 127, 9, 9, 9, 9,			      //  70 - 'F'
	0,		// 71 - 'G'
	0,		// 72 - 'H'
	0,		// 73 - 'I'
	0,		// 74 - 'J'
	0,		// 75 - 'K'
	0,		// 76 - 'L'
	0,		// 77 - 'M'
	0,		// 78 - 'N'
	0,		// 79 - 'O'
	0,		// 80 - 'P'
	0,		// 81 - 'Q'
	0,		// 82 - 'R'
	0,		// 83 - 'S'
	0,		// 84 - 'T'
	0,		// 85 - 'U'
	0,		// 86 - 'V'
	0,		// 87 - 'W'
	0,		// 88 - 'X'
	0,		// 89 - 'Y'
	0,		// 90 - 'Z'
	0,		// 91 - '['
	0,		// 92 - '\'
	0,		// 93 - ']'
	0,		// 94 - '^'
	0,		// 95 - '_'
	0,		// 96 - '`'
	5, 127, 9, 9, 9, 127,		      //  97 - 'a'
	5, 127, 73, 73, 73, 54,		    //  98 - 'b'
	5, 127, 65, 65, 65, 65,		    //  99 - 'c'
	5, 127, 65, 65, 65, 62,		    // 100 - 'd'
	5, 127, 73, 73, 73, 73,		    // 101 - 'e'
	5, 127, 9, 9, 9, 9, 		      // 102 - 'f'
	0,		// 103 - 'g'
	0,		// 104 - 'h'
	0,		// 105 - 'i'
	0,		// 106 - 'j'
	0,		// 107 - 'k'
	0,		// 108 - 'l'
	0,		// 109 - 'm'
	0,		// 110 - 'n'
	0,		// 111 - 'o'
	0,		// 112 - 'p'
	0,		// 113 - 'q'
	0,		// 114 - 'r'
	0,		// 115 - 's'
	0,		// 116 - 't'
	0,		// 117 - 'u'
	0,		// 118 - 'v'
	0,		// 119 - 'w'
	0,		// 120 - 'x'
	0,		// 121 - 'y'
	0,		// 122 - 'z'
	0,		// 123 - '{'
	1, 127,		// 124 - '|'
	0,		// 125
	0,		// 126
	0,		// 127
	0,		// 128
	0,		// 129
	0,		// 130
	0,		// 131
	0,		// 132
	0,		// 133
	0,		// 134
	0,		// 135
	0,		// 136
	0,		// 137
	0,		// 138
	0,		// 139
	0,		// 140
	0,		// 141
	0,		// 142
	0,		// 143
	0,		// 144
	0,		// 145
	0,		// 146
	0,		// 147
	0,		// 148
	0,		// 149
	0,		// 150
	0,		// 151
	0,		// 152
	0,		// 153
	0,		// 154
	0,		// 155
	0,		// 156
	0,		// 157
	0,		// 158
	0,		// 159
	0,		// 160
	0,		// 161
	0,		// 162
	0,		// 163
	0,		// 164
	0,		// 165
	0,		// 166
	0,		// 167
	0,		// 168
	0,		// 169
	0,		// 170
	0,		// 171
	0,		// 172
	0,		// 173
	0,		// 174
	0,		// 175
	0,		// 176
	0,		// 177
	0,		// 178
	0,		// 179
	0,		// 180
	0,		// 181
	0,		// 182
	0,		// 183
	0,		// 184
	0,		// 185
	0,		// 186
	0,		// 187
	0,		// 188
	0,		// 189
	0,		// 190
	0,		// 191
	0,		// 192
	0,		// 193
	0,		// 194
	0,		// 195
	0,		// 196
	0,		// 197
	0,		// 198
	0,		// 199
	0,		// 200
	0,		// 201
	0,		// 202
	0,		// 203
	0,		// 204
	0,		// 205
	0,		// 206
	0,		// 207
	0,		// 208
	0,		// 209
	0,		// 210
	0,		// 211
	0,		// 212
	0,		// 213
	0,		// 214
	0,		// 215
	0,		// 216
	0,		// 217
	0,		// 218
	0,		// 219
	0,		// 220
	0,		// 221
	0,		// 222
	0,		// 223
	0,		// 224
	0,		// 225
	0,		// 226
	0,		// 227
	0,		// 228
	0,		// 229
	0,		// 230
	0,		// 231
	0,		// 232
	0,		// 233
	0,		// 234
	0,		// 235
	0,		// 236
	0,		// 237
	0,		// 238
	0,		// 239
	0,		// 240
	0,		// 241
	0,		// 242
	0,		// 243
	0,		// 244
	0,		// 245
	0,		// 246
	0,		// 247
	0,		// 248
	0,		// 249
	0,		// 250
	0,		// 251
	0,		// 252
	0,		// 253
	0,		// 254
	0,		// 255
};

#endif

font_data_utf8

C Header File
// Data file for UTF-8 example user defined fonts
#ifndef FONTS_DATA_H
#define FONTS_DATA_H

MD_MAX72XX::fontType_t ExtASCII[] PROGMEM =
{
  0,		// 0 - 'Unused'
  0,		// 1 - 'Unused'
  0,		// 2 - 'Unused'
  0,		// 3 - 'Unused'
  0,		// 4 - 'Unused'
  0,		// 5 - 'Unused'
  0,		// 6 - 'Unused'
  0,		// 7 - 'Unused'
  0,		// 8 - 'Unused'
  0,		// 9 - 'Unused'
  0,		// 10 - 'Unused'
  0,		// 11 - 'Unused'
  0,		// 12 - 'Unused'
  0,		// 13 - 'Unused'
  0,		// 14 - 'Unused'
  0,		// 15 - 'Unused'
  0,		// 16 - 'Unused'
  0,		// 17 - 'Unused'
  0,		// 18 - 'Unused'
  0,		// 19 - 'Unused'
  0,		// 20 - 'Unused'
  0,		// 21 - 'Unused'
  0,		// 22 - 'Unused'
  0,		// 23 - 'Unused'
  0,		// 24 - 'Unused'
  0,		// 25 - 'Unused'
  0,		// 26 - 'Unused'
  0,		// 27 - 'Unused'
  0,		// 28 - 'Unused'
  0,		// 29 - 'Unused'
  0,		// 30 - 'Unused'
  0,		// 31 - 'Unused'
  2, 0, 0,		                          //  32 - 'Space'
  1, 95,		                            //  33 - '!'
  3, 7, 0, 7,		                        //  34 - '"'
  5, 20, 127, 20, 127, 20,		          //  35 - '#'
  5, 36, 42, 127, 42, 18,		            //  36 - '$'
  5, 35, 19, 8, 100, 98,		            //  37 - '%'
  5, 54, 73, 86, 32, 80,		            //  38 - '&'
  2, 4, 3,		                          //  39
  3, 28, 34, 65,		                    //  40 - '('
  3, 65, 34, 28,		                    //  41 - ')'
  5, 42, 28, 127, 28, 42,		            //  42 - '*'
  5, 8, 8, 62, 8, 8,		                //  43 - '+'
  2, 128, 96,		                        //  44 - ','
  5, 8, 8, 8, 8, 8,		                  //  45 - '-'
  2, 96, 96,		                        //  46 - '.'
  5, 32, 16, 8, 4, 2,		                //  47 - '/'
  5, 62, 81, 73, 69, 62,		            //  48 - '0'
  3, 66, 127, 64,		                    //  49 - '1'
  5, 114, 73, 73, 73, 70,		            //  50 - '2'
  5, 33, 65, 73, 77, 51,		            //  51 - '3'
  5, 24, 20, 18, 127, 16,		            //  52 - '4'
  5, 39, 69, 69, 69, 57,		            //  53 - '5'
  5, 60, 74, 73, 73, 49,		            //  54 - '6'
  5, 65, 33, 17, 9, 7,		              //  55 - '7'
  5, 54, 73, 73, 73, 54,		            //  56 - '8'
  5, 70, 73, 73, 41, 30,		            //  57 - '9'
  1, 20,		                            //  58 - ':'
  2, 128, 104,		                      //  59 - ';'
  4, 8, 20, 34, 65,		                  //  60 - '<'
  5, 20, 20, 20, 20, 20,		            //  61 - '='
  4, 65, 34, 20, 8,		                  //  62 - '>'
  5, 2, 1, 89, 9, 6,		                //  63 - '?'
  5, 62, 65, 93, 89, 78,		            //  64 - '@'
  5, 124, 18, 17, 18, 124,		          //  65 - 'A'
  5, 127, 73, 73, 73, 54,		            //  66 - 'B'
  5, 62, 65, 65, 65, 34,		            //  67 - 'C'
  5, 127, 65, 65, 65, 62,		            //  68 - 'D'
  5, 127, 73, 73, 73, 65,		            //  69 - 'E'
  5, 127, 9, 9, 9, 1,		                //  70 - 'F'
  5, 62, 65, 65, 81, 115,		            //  71 - 'G'
  5, 127, 8, 8, 8, 127,		              //  72 - 'H'
  3, 65, 127, 65,		                    //  73 - 'I'
  5, 32, 64, 65, 63, 1,		              //  74 - 'J'
  5, 127, 8, 20, 34, 65,		            //  75 - 'K'
  5, 127, 64, 64, 64, 64,		            //  76 - 'L'
  5, 127, 2, 28, 2, 127,		            //  77 - 'M'
  5, 127, 4, 8, 16, 127,		            //  78 - 'N'
  5, 62, 65, 65, 65, 62,		            //  79 - 'O'
  5, 127, 9, 9, 9, 6,		                //  80 - 'P'
  5, 62, 65, 81, 33, 94,		            //  81 - 'Q'
  5, 127, 9, 25, 41, 70,		            //  82 - 'R'
  5, 38, 73, 73, 73, 50,		            //  83 - 'S'
  5, 3, 1, 127, 1, 3,		                //  84 - 'T'
  5, 63, 64, 64, 64, 63,		            //  85 - 'U'
  5, 31, 32, 64, 32, 31,		            //  86 - 'V'
  5, 63, 64, 56, 64, 63,		            //  87 - 'W'
  5, 99, 20, 8, 20, 99,		              //  88 - 'X'
  5, 3, 4, 120, 4, 3,		                //  89 - 'Y'
  5, 97, 89, 73, 77, 67,		            //  90 - 'Z'
  3, 127, 65, 65,		                    //  91 - '['
  5, 2, 4, 8, 16, 32,		                //  92 - '\'
  3, 65, 65, 127,		                    //  93 - ']'
  5, 4, 2, 1, 2, 4,		                  //  94 - '^'
  5, 64, 64, 64, 64, 64,		            //  95 - '_'
  2, 3, 4,		                          //  96 - '`'
  5, 32, 84, 84, 120, 64,		            //  97 - 'a'
  5, 127, 40, 68, 68, 56,		            //  98 - 'b'
  5, 56, 68, 68, 68, 40,		            //  99 - 'c'
  5, 56, 68, 68, 40, 127,		            // 100 - 'd'
  5, 56, 84, 84, 84, 24,		            // 101 - 'e'
  4, 8, 126, 9, 2,		                  // 102 - 'f'
  5, 24, 164, 164, 156, 120,		        // 103 - 'g'
  5, 127, 8, 4, 4, 120,		              // 104 - 'h'
  3, 68, 125, 64,		                    // 105 - 'i'
  4, 64, 128, 128, 122,		              // 106 - 'j'
  4, 127, 16, 40, 68,		                // 107 - 'k'
  3, 65, 127, 64,		                    // 108 - 'l'
  5, 124, 4, 120, 4, 120,		            // 109 - 'm'
  5, 124, 8, 4, 4, 120,		              // 110 - 'n'
  5, 56, 68, 68, 68, 56,            		// 111 - 'o'
  5, 252, 24, 36, 36, 24,		            // 112 - 'p'
  5, 24, 36, 36, 24, 252,		            // 113 - 'q'
  5, 124, 8, 4, 4, 8,		                // 114 - 'r'
  5, 72, 84, 84, 84, 36,		            // 115 - 's'
  4, 4, 63, 68, 36,		                  // 116 - 't'
  5, 60, 64, 64, 32, 124,		            // 117 - 'u'
  5, 28, 32, 64, 32, 28,		            // 118 - 'v'
  5, 60, 64, 48, 64, 60,		            // 119 - 'w'
  5, 68, 40, 16, 40, 68,		            // 120 - 'x'
  5, 76, 144, 144, 144, 124,		        // 121 - 'y'
  5, 68, 100, 84, 76, 68,		            // 122 - 'z'
  3, 8, 54, 65,		                      // 123 - '{'
  1, 119,		                            // 124 - '|'
  3, 65, 54, 8,		                      // 125 - '}'
  5, 2, 1, 2, 4, 2,		                  // 126 - '~'
  0,		// 127 - 'Unused'
  6, 20, 62, 85, 85, 65, 34,		        // 128 - 
  0,		// 129 - 'Not used'
  2, 128, 96,		                        // 130 - 'Single low 9 quotation mark'
  5, 192, 136, 126, 9, 3,		            // 131 - 'f with hook'
  4, 128, 96, 128, 96,		              // 132 - 'Single low 9 quotation mark'
  8, 96, 96, 0, 96, 96, 0, 96, 96,		  // 133 - 'Horizontal ellipsis'
  3, 4, 126, 4,		                      // 134 - 'Dagger'
  3, 20, 126, 20,		                    // 135 - 'Double dagger'
  4, 2, 1, 1, 2,		                    // 136 - 'Modifier circumflex'
  7, 35, 19, 104, 100, 2, 97, 96,		    // 137 - 'Per mille sign'
  5, 72, 85, 86, 85, 36,		            // 138 - 'S with caron'
  3, 8, 20, 34,		                      // 139 - '< quotation'
  6, 62, 65, 65, 127, 73, 73,		        // 140 - 'OE'
  0,		// 141 - 'Not used'
  5, 68, 101, 86, 77, 68,		            // 142 - 'z with caron'
  0,		// 143 - 'Not used'
  0,		// 144 - 'Not used'
  2, 3, 4,		                          // 145 - 'Left single quote mark'
  2, 4, 3,		                          // 146 - 'Right single quote mark'
  4, 3, 4, 3, 4,		                    // 147 - 'Left double quote marks'
  4, 4, 3, 4, 3,		                    // 148 - 'Right double quote marks'
  4, 0, 24, 60, 24,		                  // 149 - 'Bullet Point'
  3, 8, 8, 8,		                        // 150 - 'En dash'
  5, 8, 8, 8, 8, 8,		                  // 151 - 'Em dash'
  4, 2, 1, 2, 1,		                    // 152 - 'Small ~'
  7, 1, 15, 1, 0, 15, 2, 15,		        // 153 - 'TM'
  5, 72, 85, 86, 85, 36,		            // 154 - 's with caron'
  3, 34, 20, 8,		                      // 155 - '> quotation'
  7, 56, 68, 68, 124, 84, 84, 8,		    // 156 - 'oe'
  0,		// 157 - 'Not used'
  5, 68, 101, 86, 77, 68,		            // 158 - 'z with caron'
  5, 12, 17, 96, 17, 12,		            // 159 - 'Y diaresis'
  2, 0, 0,		                          // 160 - 'Non-breaking space'
  1, 125,		                            // 161 - 'Inverted !'
  5, 60, 36, 126, 36, 36,		            // 162 - 'Cent sign'
  5, 72, 126, 73, 65, 102,		          // 163 - 'Pound sign'
  5, 34, 28, 20, 28, 34,		            // 164 - 'Currency sign'
  5, 43, 47, 252, 47, 43,		            // 165 - 'Yen'
  1, 119,		                            // 166 - '|'
  4, 102, 137, 149, 106,		            // 167 - 'Section sign'
  3, 1, 0, 1,		                        // 168 - 'Spacing diaresis'
  7, 62, 65, 93, 85, 85, 65, 62,		    // 169 - 'Copyright'
  3, 13, 13, 15,		                    // 170 - 'Feminine Ordinal Ind.'
  5, 8, 20, 42, 20, 34,		              // 171 - '<<'
  5, 8, 8, 8, 8, 56,		                // 172 - 'Not sign'
  0,		// 173 - 'Soft Hyphen'
  7, 62, 65, 127, 75, 117, 65, 62,		  // 174 - 'Registered Trademark'
  5, 1, 1, 1, 1, 1,		                  // 175 - 'Spacing Macron Overline'
  3, 2, 5, 2,		                        // 176 - 'Degree'
  5, 68, 68, 95, 68, 68,		            // 177 - '+/-'
  3, 25, 21, 19,		                    // 178 - 'Superscript 2'
  3, 17, 21, 31,		                    // 179 - 'Superscript 3'
  2, 2, 1,		                          // 180 - 'Acute accent'
  4, 252, 64, 64, 60,		                // 181 - 'micro (mu)'
  5, 6, 9, 127, 1, 127,		              // 182 - 'Paragraph Mark'
  2, 24, 24,		                        // 183 - 'Middle Dot'
  3, 128, 128, 96,		                  // 184 - 'Spacing sedilla'
  2, 2, 31,		                          // 185 - 'Superscript 1'
  4, 6, 9, 9, 6,		                    // 186 - 'Masculine Ordinal Ind.'
  5, 34, 20, 42, 20, 8,		              // 187 - '>>'
  6, 64, 47, 16, 40, 52, 250,		        // 188 - '1/4'
  6, 64, 47, 16, 200, 172, 186,		      // 189 - '1/2'
  6, 85, 53, 31, 40, 52, 250,		        // 190 - '3/4'
  5, 48, 72, 77, 64, 32,		            // 191 - 'Inverted ?'
  5, 120, 20, 21, 22, 120,		          // 192 - 'A grave'
  5, 120, 22, 21, 20, 120,		          // 193 - 'A acute'
  5, 122, 21, 21, 21, 122,		          // 194 - 'A circumflex'
  5, 120, 22, 21, 22, 121,		          // 195 - 'A tilde'
  5, 120, 21, 20, 21, 120,		          // 196 - 'A diaresis'
  5, 120, 20, 21, 20, 120,		          // 197 - 'A ring above'
  6, 124, 10, 9, 127, 73, 73,		        // 198 - 'AE'
  5, 30, 161, 161, 97, 18,		          // 199 - 'C sedilla'
  4, 124, 85, 86, 68,		                // 200 - 'E grave'
  4, 124, 86, 85, 68,		                // 201 - 'E acute'
  4, 126, 85, 85, 70,		                // 202 - 'E circumflex'
  4, 124, 85, 84, 69,		                // 203 - 'E diaresis'
  3, 68, 125, 70,		                    // 204 - 'I grave'
  3, 68, 126, 69,		                    // 205 - 'I acute'
  3, 70, 125, 70,		                    // 206 - 'I circumplex'
  3, 69, 124, 69,		                    // 207 - 'I diaresis'
  6, 4, 127, 69, 65, 65, 62,		        // 208 - 'Capital Eth'
  5, 124, 10, 17, 34, 125,		          // 209 - 'N tilde'
  5, 56, 68, 69, 70, 56,		            // 210 - 'O grave'
  5, 56, 70, 69, 68, 56,		            // 211 - 'O acute'
  5, 58, 69, 69, 69, 58,		            // 212 - 'O circumflex'
  5, 56, 70, 69, 70, 57,		            // 213 - 'O tilde'
  5, 56, 69, 68, 69, 56,		            // 214 - 'O diaresis'
  5, 34, 20, 8, 20, 34,		              // 215 - 'Multiplication sign'
  7, 64, 62, 81, 73, 69, 62, 1,		      // 216 - 'O slashed'
  5, 60, 65, 66, 64, 60,		            // 217 - 'U grave'
  5, 60, 64, 66, 65, 60,		            // 218 - 'U acute'
  5, 58, 65, 65, 65, 58,		            // 219 - 'U circumflex'
  5, 60, 65, 64, 65, 60,		            // 220 - 'U diaresis'
  5, 12, 16, 98, 17, 12,		            // 221 - 'Y acute'
  4, 127, 18, 18, 12,		                // 222 - 'Capital thorn'
  4, 254, 37, 37, 26,		                // 223 - 'Small letter sharp S'
  5, 32, 84, 85, 122, 64,		            // 224 - 'a grave'
  5, 32, 84, 86, 121, 64,		            // 225 - 'a acute'
  5, 34, 85, 85, 121, 66,		            // 226 - 'a circumflex'
  5, 32, 86, 85, 122, 65,		            // 227 - 'a tilde'
  5, 32, 85, 84, 121, 64,		            // 228 - 'a diaresis'
  5, 32, 84, 85, 120, 64,		            // 229 - 'a ring above'
  7, 32, 84, 84, 124, 84, 84, 8,		    // 230 - 'ae'
  5, 24, 36, 164, 228, 40,		          // 231 - 'c sedilla'
  5, 56, 84, 85, 86, 88,		            // 232 - 'e grave'
  5, 56, 84, 86, 85, 88,		            // 233 - 'e acute'
  5, 58, 85, 85, 85, 90,		            // 234 - 'e circumflex'
  5, 56, 85, 84, 85, 88,		            // 235 - 'e diaresis'
  3, 68, 125, 66,		                    // 236 - 'i grave'
  3, 68, 126, 65,		                    // 237 - 'i acute'
  3, 70, 125, 66,		                    // 238 - 'i circumflex'
  3, 69, 124, 65,		                    // 239 - 'i diaresis'
  4, 48, 75, 74, 61,		                // 240 - 'Small eth'
  4, 122, 9, 10, 113,		                // 241 - 'n tilde'
  5, 56, 68, 69, 70, 56,		            // 242 - 'o grave'
  5, 56, 70, 69, 68, 56,		            // 243 - 'o acute'
  5, 58, 69, 69, 69, 58,		            // 244 - 'o circumflex'
  5, 56, 70, 69, 70, 57,		            // 245 - 'o tilde'
  5, 56, 69, 68, 69, 56,		            // 246 - 'o diaresis'
  5, 8, 8, 42, 8, 8,		                // 247 - 'Division sign'
  6, 64, 56, 84, 76, 68, 58,		        // 248 - 'o slashed'
  5, 60, 65, 66, 32, 124,		            // 249 - 'u grave'
  5, 60, 64, 66, 33, 124,		            // 250 - 'u acute'
  5, 58, 65, 65, 33, 122,		            // 251 - 'u circumflex'
  5, 60, 65, 64, 33, 124,		            // 252 - 'u diaresis'
  4, 156, 162, 161, 124,		            // 253 - 'y acute'
  4, 252, 72, 72, 48,		                // 254 - 'small thorn'
  4, 157, 160, 160, 125,		            // 255 - 'y diaresis'
};

#endif

Link to library: Adafruit_BME280_Library

Link to library: ArduinoJson

Link to library: Dusk2Dawn

Link to library: MD_MAX72XX

Link to library: MD_Parola

Link to library: RotaryEncoder

Link to library: Time-master

Credits

ericBcreator

ericBcreator

6 projects • 109 followers

Comments