Hardware components | ||||||
| × | 1 | ||||
| × | 16 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
Software apps and online services | ||||||
|
After my previous project (Arduino 32x8 LED Matrix Info Display displaying date, time, temperature, humidity) I thought it would be interesting to build a more dynamic display which connects to the internet and can retrieve data from various websites.
Update 20181111: I added a web interface:
The message board is built with an ESP32 microcontroller, 16 8x8 LED matrices, a temperature/humidity sensor and a rotary encoder to control what is displayed. The LED matrices are devided into 3 zones: time, alternating date and barometer info and the main message display. The Parola library is used for displaying the messages.
After powering up, the ESP32 connects to the local wifi network and a timeserver to get the local date and time and determines if Daylight Saving Time is active or not.
Then it loops through the main carousel, displaying full date, sunrise/sunset times for a city, current weather and forecast for several cities from Openweathermap.org, news headline feeds from Newsapi.org and stock data from www.alphavantage.co. The rotary controller lets you select several menu items for chosing what to display: the main carousel, several news categories or stock data. It is pretty basic, I intend to extend it in the future with submenus, etc.
The script can be localized and adjusted to display information from other cities, other websites or other sources.
The setupComponents:
- ESP32
- 16 MAX7219 8x8 LED matrices (I used 4 modules with 4 matrices)
- BME280 sensor
- rotary encoder
- buzzer
The LED matrices are daisy chained (soldered 2 by 2) and connected to the ESP32. I used tape and pieces of cardboard on the back to keep the modules together.
Because of the large number (1024) of LEDs, the projects needs to be powered by an external power supply, preferably 2A or higher. I do not recommend powering it by USB through the ESP32, the power the LEDs will draw is (much) higher than the ESP32 is designed to handle.
Tip: I would recommend not daisy chaining the power connections of the MAX7219 4 LED modules but connecting each one directly because there is a slight power drop after each module. It is noticeable when you power all 1024 LEDs at once, not so much with normal use.
Then the BME280 and rotary encoder are connected to the ESP32. Here is my test setup, check the schematic for the details:
I bought a cheap (€ 2) wooden wine bottle box to mount the display and other parts:
I tried some pieces of paper and film to improve the contrast of the display:
I used a transparant film sticker and printed it with 75% black and stuck it onto the display:
After testing, I later decided to add the rotary encoder, here is a test setup:
A look at the inside. Still with the breadboard, it will be soldered later.
The code consists of the main code file, 2 font data files (from Parola library examples) and 7 libraries. The code files and links are in the Code section. The libraries have to be installed in the Arduino IDE (I used version 1.8.7) and the 2 font files have to be copied to the same folder as the main code file.
To adjust the code for your setup: search for the @EB-setup comments. Search for the @EB-share comments to change default settings to your preferences.
Note: click the 'Download' button and not the 'Copy' or the code may be truncated and not work correctly!
Update 20181110- Added support for conversion from UTF-8 characters to extended ASCII codes (according to ISO-Latin-1).
- Added a web interface for displaying custom messages and with several controls.
I added a buzzer to get an audible notification when a new message is received from the web interface.
Update 20190411On a suggestion of user PT I added light sensor support, it will set the brightness of the leds (the Parola library) according to the light hitting the sensor.
Note: using Wifi on the ESP32 causes problems with analog reading on some GPIO pins, I used pin 36.
I also added a Youtube counter, clickable links for Newsapi and Wikipedia articles (for the web server), a delay corresponding to the Newsapi maximum requests of 1000 per day (for the free plan) and some other enhancements.
LicenseFeel free to use the code for your personal use, not for commercial purposes.
You can contact me at ericBcreator@gmail.com.
/*
***************************************************************************
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.
/*
***************************************************************************
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.
// 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
// 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
Comments