Introduction: A Scrolling Digital Clock and Weather Station

About: Retired high school physics teacher.

Over the years I have built a large number of digital clocks using 7 segment LED displays as well as Nextion displays. I redesigned the software for a scrolling LED matrix display building on the software written for many clocks using an ESP32 for its Internet connectivity. The best feature of a scrolling display is that a larger amount of information can be viewed compared to a static display. LED matrix displays are a convenient way to produce a scrolling display and a variety of different ones can be found on Amazon and eBay. The specific ones used in this project come in either red, green or blue colors and are 32 LEDs wide by 8 high. There are actually four 8x8 LED modules mounted on these boards, each with a MAX7219 LED driver and chained together. These boards can also be connected together giving a wider array. For this project two of these boards were connected making a 64x8 array. Makers should be aware there are slight differences on these boards in terms of size and mounting hole spacing depending upon the brand. With the boards I bought I also found the green boards to be a yellow-green and not nearly as bright as the red and blue boards.

See the video below for an example of the display.

Supplies

Step 1: Connecting the Displays

Connecting two of these boards together can be a challenge. Each individual board has a 5 pin header on the input end and 5 empty pads on the output end. The difficulty is that the header pins on the input end are not mounted to mate directly with the output pads at the other end. Here are some possibilities. The second board needs to have the header pins carefully removed, so as to not damage the pads, and then a new 5 pin header mounted which points outward from the back of the board. The first board gets another 5 pin header soldered into the empty 5 pads at its output end. The two boards can then be electrically joined by mounting 5 jumper straps between these pins.  Simple wire jumpers soldered between the 5 sets of pads could also be used. Another possibility is to bend downward, in a U shape, the pins on the second board and then fit them into the solder pads on the first board. The two boards are then assembled on a 3D printed plastic mount with #4 machine screws.

Step 2: The Hardware

The LED boards have an SPI interface and the LedControl library is the driver used. LedControl is an Arduino library for the MAX7219 and MAX7221 LED display drivers. The code also works nicely with an ESP32 microcontroller. Documentation for the library is on the Github Project Pages http://wayoda.github.io/LedControl/ The latest binary version of the library is always available from the LedControl Release Page (https://github.com/wayoda/LedControl/releases). The library can be installed using the standard Arduino library install procedure found at http://arduino.cc/en/Guide/Libraries#.UwxndHX5PtY/.

The ESP32 microcontroller was chosen for its Internet connectivity and SPI interface. I selected a 38 pin version of the ESP32 and designed and produced a general purpose printed circuit board (PCB) using the free ExpressPCB software. This PCB will accommodate a 38 pin ESP32 with either 0.9 or 1.0 inch wide pin spacing. The PCB mounts the ESP32, and connections for a DS3231 Real Time Clock (RTC) in two different pin formats, a BME280 Temperature and Humidity module, an optional PAM8403 Audio Amplifier module and an optional LCD display. A word of caution on the BME280 Temperature and Humidity module is in order. Many of the BME280 units available on both Amazon and eBay do not have the humidity interface functioning, only the temperature. I suspect these are production culls, so be aware if you need the local humidity reading. I placed a trap in my code to catch this if the local humidity value returns a zero value. Additional pads running to the ESP32 have been added to the PCB. This allows the addition of other components, for example a RCWL-0516 microwave sensor used as a motion detector.

An image is shown with the populated PCB mounted in a 3D printed project box. The battery coin cell was not needed on the blue DS3231 RTC board as the time is read from the Internet in the main loop. The gray 3D printed mount board supports the two LED matrix displays underneath. Header jumpers connect the input pins of the first LED matrix display on the left to an SPI connector on the second LED matrix display on the right. The header pins for the ESP32 SPI connection are on the bottom of the PCB, and underneath the ESP32, and need to be the right angle type for the PCB to fit above the displays. The five blue header jumpers connecting the two LED matrix displays are visible in the center. The red/black wires at the bottom left connect to a 5 volt power input jack and those at the bottom right connect to the BME280 breakout board. The connection to the BME280 breakout board is brought to the outside of the project box in the back by a 4 pin header jumper 20 cm. long to avoid heating by the internal components. The small board in the upper right of the project box is a RCWL-0516 Microwave Human Body Sensor. This optional board turns the display off after three minutes if no human presence is detected.

Step 3: The Software

A simplified flow chart for the software is shown above. The main loop of the program first obtains the time and weather data from the Internet. These values are then used to produce a long string of characters that will be scrolled across the LED display. The time appears three times in the string along with the city name, its current weather, temperature, humidity and wind. Sunrise and sunset times are also included as well as the date. The string is then used to produce an array of columns for the LED matrix display called colArray[], which is built by using the byte newFont[128][5] array. Each of the five byte values of newFont[128][5] represent the five columns of an 8x5 character. Each character in the string will add five bytes to colArray[] for the character and another byte as a blank column to separate the characters. A loop then sends bytes 0 through 63 from the colArray[] to the display using the .setColumn() method of the LedControl object. Next bytes 1 through 64 are sent then 2 through 65, etc., until the entire colArray[] has been sent. The movement of the selected 64 columns produces the scrolling effect.

The following are some further comments on various parts of the program.

NTP Server

The Network Time Protocol (NTP) server is used to obtain the current time from the Internet. Here are the variables and a code fragment example. See the complete program downloadable below for details on use. The ntp.org website has a wealth of information on this service.


#include "time.h"

const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec_east      = -3600 * 5;  // Eastern Standard Time
const long gmtOffset_sec_central   = -3600 * 6;  // Central Standard Time
const long gmtOffset_sec_mount     = -3600 * 7;  // Mountain Standard Time
const long gmtOffset_sec_w_europe  = 3600;       // France, Netherlands
const long gmtOffset_sec_pacific_is = -3600 * 10; // Honolulu
const int  daylightOffset_sec      = 3600;

struct tm timeinfo;
// Variables in structure
// int tm_sec;
// int tm_min;
// int tm_hour;
// int tm_mday;
// int tm_mon;
// int tm_year;
// int tm_wday;
// int tm_yday;
// int tm_isdst;

configTime(gmtOffset, daylightOffset_sec, ntpServer);
getLocalTime(&timeinfo);
iyear = timeinfo.tm_year;
iyear = iyear % 100;
iyear = iyear + 2000;
bmonth = timeinfo.tm_mon;
bdayOfMonth = timeinfo.tm_mday;
bhour = timeinfo.tm_hour;
bminute = timeinfo.tm_min;
bsecond = timeinfo.tm_sec;
rtc.adjust(DateTime(iyear, bmonth, bdayOfMonth, bhour, bminute, bsecond));

RTClib

The real time clock (RTC) library used is Adafruit's RTClib and it can be found at https://github.com/adafruit/RTClib. Once the time is obtained from the Internet it is read into the RTC. While the Internet time is only read once for each long string displayed, the RTC is called many times as the display scrolls.


now_time = rtc.now();
last_sec = now_time.second();
bminute = now_time.minute();
bhour = now_time.hour();
bdayOfMonth = now_time.day();
bmonth = now_time.month();
iyear = now_time.year();
bdayOfWeek = (byte)weekday(iyear, (int)bmonth, (int)bdayOfMonth);
bhour24 = bhour;
ampm = 0;
// Check if it's 12 PM
if (bhour == 12) ampm = 1;

// Check if 12 AM
if (bhour == 0) bhour = 12;

// Check if it's PM
if (bhour > 12)
{
  bhour -= 12;
  ampm = 1;
}


OpenWeather

The openweathermap.org service is used to obtain the local weather. The service is free but you do need to obtain an account to access the weather data. Their web site contains a large text file containing the city numbers needed in a call to their server. This text file is in JSON format but can be simply searched for a city name with a program like Windows Notepad. After the call to api.openweathermap.org, a JSON format string is returned with the weather information and a simple parse function retrieves the desired data.


String name_location[12] = {"Tierra Verde, FL", "Norton Shores, MI", "Chicago, IL", "Sparta, MI", "Bluffton, SC", "Green Valley, AZ", "Rodeo, NM", "Silver City, NM", "Brigham City, UT", "Naperville, IL", "Paris, France", "Leens, Nederland"};

String num_location[12] = {"4175331", "5004005", "4887398", "5010690", "4571722", "5296802", "5488280", "5491487", "5771960", "4903279", "6455259", "2751813"};

const String endpoint = "http://api.openweathermap.org/data/2.5/weather?id=";
const String endpoint2 = "&units=imperial&APPID=";
// You need to obtain a key from api.openweathermap.org
const String key = "";
String location = "4175331";       // This is Tierra Verde, FL

http.begin(endpoint + location + endpoint2 + key); //Specify the URL
int httpCode = http.GET(); //Make the request

if (httpCode > 0)
{
   // Check for the returning code
   payload = http.getString();
   if (debug)
   {
       Serial.println(payload);
       Serial.print(payload.length());
       Serial.println(" bytes");
   }
   weather = parse_it(payload, isitHome);
 }
 else
 {
     if (debug) Serial.println("Error on HTTP request");
 }

 http.end(); //Free the resources
 return httpCode;

Number of cities

The clock could have just the time and weather for only one city but I usually have at least two that alternate. The colArray[] will use 6 times the number of characters in the long string that is built for each city. As each long string is built the city needs to have its time zone set as well as its Daylight Savings Time. Here is an example of assigning these values using 12 cities. It uses East, Central and Mountain Time in the U.S., as well as Western Europe. Only Green Valley, AZ does not use Daylight Savings Time. The variable myHome is incremented before the next long string is built and then reset to 0 when myHome reaches 12.


switch(myHome)                          
     {
         case 0: // Tierra Verde,FL
                 gmtOffset = gmtOffset_sec_east;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 1: // Norton Shores, MI
                 gmtOffset = gmtOffset_sec_east;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 2: // Chicago, IL
                 gmtOffset = gmtOffset_sec_central;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 3: // Sparta, MI
                 gmtOffset = gmtOffset_sec_east;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 4: // Bluffton, SC
                 gmtOffset = gmtOffset_sec_east;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 5: // Green Valley, AZ
                 gmtOffset = gmtOffset_sec_mount;
                 daylight_saving = no_DST;
                 break;
         case 6: // Rodeo, NM
                 gmtOffset = gmtOffset_sec_mount;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 7: // Silver City, NM
                 gmtOffset = gmtOffset_sec_mount;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 8: // Brigham City, UT
                 gmtOffset = gmtOffset_sec_mount;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 9: // Naperville, IL
                 gmtOffset = gmtOffset_sec_central;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 10: // Paris, France
                 gmtOffset = gmtOffset_sec_w_europe;
                 daylight_saving = daylightOffset_sec;
                 break;
         case 11: // Leens, Netherlands
                 gmtOffset = gmtOffset_sec_w_europe;
                 daylight_saving = daylightOffset_sec;
                 break;                
     }


Values for sunrise and sunset are returned after a call to the openweathermap.org service. Unfortunately, these values are returned in Unix timestamp format, ie. the number of seconds since January 1, 1970. The function timeStamp(long ts, bool full, long offset, int dst) returns the Unix timestamp as a string. The variable ts is the Unix timestamp, full determines whether the entire date and time or just the time is returned, offset is the number of seconds to offset the time zone from Universal Standard Time (UST) and dst is the number of seconds for Daylight Savings Time (DST). Here is the fragment to place sunrise and sunset into the long string built for the city.

     if (daylight_saving == no_DST) daylight_saving = daylightOffset_sec;
     s.concat("Sunrise ");
srss = timeStamp(Sunrise.toInt(), false, Timezone.toInt(), daylight_saving);
     s.concat(srss);
     s.concat(", Sunset ");
srss = timeStamp(Sunset.toInt(), false, Timezone.toInt(), daylight_saving);
     s.concat(srss);
     s.concat(", ");

BME280

The BME280 temperature and humidity sensor on its small breakout board is simple to use. A number of settings and calibrations are done in setup(). I used the SparkFun library for my driver which is available at https://github.com/sparkfun/SparkFun_BME280_Arduino_Library. The library provides functions to obtain the temperature and humidity.

#include "SparkFunBME280.h"

BME280 mySensor;

mySensor.settings.commInterface = I2C_MODE;
mySensor.settings.I2CAddress = 0x76;
mySensor.settings.runMode = 3; //Normal mode
mySensor.settings.tStandby = 0;
mySensor.settings.filter = 0;
mySensor.settings.tempOverSample = 1;
mySensor.settings.pressOverSample = 1;
mySensor.settings.humidOverSample = 1;
mySensor.begin();
mySensor.calibration.dig_T1;
mySensor.calibration.dig_T2;
mySensor.calibration.dig_T3;
mySensor.calibration.dig_P1;
mySensor.calibration.dig_P2;
mySensor.calibration.dig_P3;
mySensor.calibration.dig_P4;
mySensor.calibration.dig_P5;
mySensor.calibration.dig_P6;
mySensor.calibration.dig_P7;
mySensor.calibration.dig_P8;
mySensor.calibration.dig_P9;
mySensor.calibration.dig_H1;
mySensor.calibration.dig_H2;
mySensor.calibration.dig_H3;
mySensor.calibration.dig_H4;
mySensor.calibration.dig_H5;
mySensor.calibration.dig_H6;

currenttemp = mySensor.readTempF();
humidity = mySensor.readFloatHumidity();

buildCols()

The buildCols() function uses the newFont[] array of 5 x 8 characters to produce the colArray[] array with the desired text. The colArray[] array is then simply output to the display using the displayCols() function.

// First flush the columns
for (int i = 0; i < ((str.length() * 6) + 64); i++) colArray[i] = 0x00;
int c = 0;
for (int i = 0; i < str.length(); i++)
{
   int ch = str[i];   // ASCII value
   if (ch > 0x7F) continue;
   for (int j = 0; j < 5; j++)
   {
       colArray[c++] = newFont[ch - 0x20][j];
   }
   colArray[c++] = 0x00;
}
return str.length();

displayCols()

The displayCols() function simply uses the LedDisplay .setColumn() method to write the columns to the LED display.

for (int add = 7; add >= 0; add--)
{
   for (int column = 0; column < 8; column++)
   {
       lc.setColumn(add, column, colArray[num++]);
   }
}

writeTime

The writeTime function writes the current time into the scrolling display string at the three places the time is displayed. Because this function is called repeatedly as we loop through the display string's columns, the seconds will be immediately updated and show increments as the display scrolls.

   if (bhour > 9) s.setCharAt(point[0], Nums[bhour / 10]);
   s.setCharAt(point[0]+1, Nums[bhour % 10]);
   s.setCharAt(point[0]+3, Nums[bminute / 10]);
   s.setCharAt(point[0]+4, Nums[bminute % 10]);
   s.setCharAt(point[0]+6, Nums[last_sec / 10]);
   s.setCharAt(point[0]+7, Nums[last_sec % 10]);

   if (bhour > 9) s.setCharAt(point[1], Nums[bhour / 10]);
   s.setCharAt(point[1]+1, Nums[bhour % 10]);
   s.setCharAt(point[1]+3, Nums[bminute / 10]);
   s.setCharAt(point[1]+4, Nums[bminute % 10]);
   s.setCharAt(point[1]+6, Nums[last_sec / 10]);
   s.setCharAt(point[1]+7, Nums[last_sec % 10]);

   if (bhour > 9) s.setCharAt(point[2], Nums[bhour / 10]);
   s.setCharAt(point[2]+1, Nums[bhour % 10]);
   s.setCharAt(point[2]+3, Nums[bminute / 10]);
   s.setCharAt(point[2]+4, Nums[bminute % 10]);
   s.setCharAt(point[2]+6, Nums[last_sec / 10]);
   s.setCharAt(point[2]+7, Nums[last_sec % 10]);

Talkie (Optional)

Talkie is speech library for Arduino. It generates speech from a fixed vocabulary encoded with Linear Predictive Coding (LPC). Talkie comes with over 1000 words of speech data that can be included in your projects. It is a software implementation of the Texas Instruments speech synthesis architecture (LPC) from the late 1970s / early 1980s. The voice is familiar from Texas Instruments Speak & Spell family of educational products. Details can be found at https://github.com/ArminJo/Talkie.

#include <Talkie.h>
const uint8_t spZERO[] PROGMEM = {0x69,0xFB,0x59,0xDD,0x51,0xD5,0xD7,0xB5,0x6F,0x0A,0x78,0xC0,0x52,0x01,0x0F,0x50,0xAC,0xF6,0xA8,0x16,0x15,0xF2,0x7B,0xEA,0x19,0x47,0xD0,0x64,0xEB,0xAD,0x76,0xB5,0xEB,0xD1,0x96,0x24,0x6E,0x62,0x6D,0x5B,0x1F,0x0A,0xA7,0xB9,0xC5,0xAB,0xFD,0x1A,0x62,0xF0,0xF0,0xE2,0x6C,0x73,0x1C,0x73,0x52,0x1D,0x19,0x94,0x6F,0xCE,0x7D,0xED,0x6B,0xD9,0x82,0xDC,0x48,0xC7,0x2E,0x71,0x8B,0xBB,0xDF,0xFF,0x1F};
const uint8_t spONE[] PROGMEM = {0x66,0x4E,0xA8,0x7A,0x8D,0xED,0xC4,0xB5,0xCD,0x89,0xD4,0xBC,0xA2,0xDB,0xD1,0x27,0xBE,0x33,0x4C,0xD9,0x4F,0x9B,0x4D,0x57,0x8A,0x76,0xBE,0xF5,0xA9,0xAA,0x2E,0x4F,0xD5,0xCD,0xB7,0xD9,0x43,0x5B,0x87,0x13,0x4C,0x0D,0xA7,0x75,0xAB,0x7B,0x3E,0xE3,0x19,0x6F,0x7F,0xA7,0xA7,0xF9,0xD0,0x30,0x5B,0x1D,0x9E,0x9A,0x34,0x44,0xBC,0xB6,0x7D,0xFE,0x1F};


Although I did not implement this in this version of my Scrolling Digital Clock, all the software is present in the program. Only enough words are available for the numbers needed to speak the time. The talk_time() function will output the current time. The audio output is available on GPIO pin 25 on the ESP32 and this signal must be fed into a PAM8403 Audio Amplifier module, and pin pads for this module area available on the PCB. A small 8 ohm speaker will also be needed.

Below are SketchUp files for the clock case and the complete program for the ESP32.

Step 4: Additional Files

There are file types not supported for downloads by Instructables that are useful to a builder. This includes the ExpressPCB .pcb file for a printed circuit board, the city list and their numbers for use when calling openweathermap.org and SketchUp .obj files for use with a slicer such as Cura. These files can be obtained at no cost from the author at theronsarticles@gmail.com.