Skip to Content

Monthly Calendar on E-Paper Display

Monthly Calendar on E-Paper Display

In this tutorial you will learn how implement a Monthly Calendar on an E-Paper Display using an ESP32. The display will also show the current time and will synchronize the time with an internet time provider (SNTP server), so that time and calendar are always accurate. No more manual setting of time and date.

Required Parts

I am using the ES32 lite as a microprocessor, since it is cheap and has a battery charging interface, which allows you to run the calendar on a LiPo battery. Any other ESP32 or ESP8266 with sufficient memory will work as well but preferably get one with a battery charging interface. You should be able to use an Arduino too but I didn’t try it.

4.2″ e-Paper Display

ESP32 lite Lolin32

ESP32 lite

USB data cable

USB Data Cable

Dupont wire set

Dupont Wire Set

Half_breadboard56a

Breadboard

Makerguides.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to products on Amazon.com. As an Amazon Associate we earn from qualifying purchases.

E-paper Display

The e-Paper display used in this project is a 4.2″ display module, with 400×300 pixels resolution, 4 grey levels, a partial refresh time of 0.4 seconds, and an embedded controller with an SPI interface.

Front and Back of 4.2″ e-Paper display module

Note that the module has a little jumper pad/switch at the back to switch from 4-wire SPI to 3-wire SPI. We are going to use the default 4-wire SPI here. So you should not have to change anything.

Back of display module with SPI-interface and controller

The display module runs on 3.3V or 5V, has a very low sleep current of 0.01µA and consumes only about 26.4mW while refreshing. For more information on e-Paper displays, in general, have a look at the following two tutorials: Interfacing Arduino To An E-ink Display and Weather Station on e-Paper Display.

Connecting and Testing the e-Paper

First, let us connect and test the function of the e-Paper. The following picture shows the complete wiring for power and SPI.

Connecting 4.2" e-Paper to ESP32 via SPI
Connecting 4.2″ e-Paper to ESP32 via SPI

Below a table with all the connections for convenience. Note that you can power the display with 3.3V or 5V but the ESP32-lite has only a 3.3V output and the SPI data lines must be 3.3V!

e-Paper displayESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

You can use a breadboard to wire everything up. But I actually connected the display directly to the ESP32 with Dupont wires. The picture below shows how my setup looked like.

ESP32 wired with e-Paper

Install GxEPD2 library

Before we can draw or write on the e-Paper display we need to install two libraries. The Adafruit_GFX graphics library, which provides a common set of graphics primitives (text, points, lines, circles, etc.). And the GxEPD2 library, which provides the graphics driver software for the E-Paper Display.

Just install the libraries the usual way. After the installation they should appear in the Library Manager as follows.

Adafruit_GFX and GxEPD2 libraries in Library Manager
Adafruit_GFX and GxEPD2 libraries in Library Manager

Test code

Once you have installed the library run the following test code to ensure that everything works.

#include "GxEPD2_BW.h"

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(90, 190);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

void loop() {}

The text code prints the text “Makerguides” and after some flickering of the display (full refresh) you should see it on your display as shown below:

Test output on 4.2" e-Paper display
Test output on 4.2″ e-Paper display

If not, then something is wrong. Most likely either the display is not correctly wired or the wrong display driver is chosen. The critical line of code is this one, where we specify the 4.2″ display driver:

GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

The Readme for GxEPD2 library lists all the supported displays and you can find the specifics in the header files, e.g. GxEPD2.h. Find the display driver specific for your display. This may take some trial and error.

Code for a Calendar on e-Paper

In this section we are going to write the code for our Calendar. The screenshot below shows how it will look like:

Calendar view
Calendar view

The current day and time are shown at the top. Below the display shows the current month, year and the days of the month with the current day marked.

Below is the code for this calendar. Have a quick look first to get an overview and then we’ll dive into the details:

#include <WiFi.h>
#include <time.h>
#include <GxEPD2_BW.h>

#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>

const char* ssid = "SSID";
const char* password = "PWD";
const char* ntpServer = "pool.ntp.org";
const char* timezone = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const char* dayNames[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
char* dayLongNames[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* monthNames[] = {
  "January", "February", "March", "April", "May",
  "June", "July", "August", "September", "October", "November", "December"
};
const int shifts[] = {
  3, 0, 0, 1, 0, 0, 0, 0, 0, 2,
  2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  1
};

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

bool isLeapYear(int year) {
  return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

int daysInMonth(int year, int month) {
  int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  if (month == 2 && isLeapYear(year)) {
    return 29;
  }
}

// Return day of the week (0=Sunday, 1=Monday, ..., 6=Saturday)
int dayOfWeek(int year, int month, int day) {
  tm timeinfo = {};
  timeinfo.tm_year = year - 1900;
  timeinfo.tm_mon = month - 1;
  timeinfo.tm_mday = day;
  mktime(&timeinfo);
  return timeinfo.tm_wday;
}

void drawAt(int x, int y, const char* text, int shift, bool mark) {
  int16_t xb, yb;
  uint16_t wb, hb;
  epd.getTextBounds(text, 0, 0, &xb, &yb, &wb, &hb);
  epd.setCursor(x - wb - shift, y);
  epd.setTextColor(mark ? GxEPD_WHITE : GxEPD_BLACK);
  if (mark) {
    epd.fillRect(x - wb - shift - 3, y - hb - 3, wb + 8, hb + 8, GxEPD_BLACK);
  }
  epd.print(text);
}

void drawCalendar(int year, int month, int day) {
  char buf[6];
  int dy = 25;
  int dx = 56;
  int xoff = 10;
  int yoff = 120;

  // Print name of month and the year
  epd.setPartialWindow(0, yoff - 20, 400, 300 - (yoff - 20));
  epd.fillRect(6, yoff - 20, 400 - 12, 28, GxEPD_BLACK);
  epd.setFont(&FreeSansBold12pt7b);
  epd.setTextColor(GxEPD_WHITE);
  epd.setCursor(xoff, yoff);
  epd.printf("%s %d", monthNames[month - 1], year);

  // print names of week days
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold9pt7b);
  xoff += 30;
  for (int i = 0; i < 7; i++) {
    int x = xoff + i * dx;
    int y = yoff + 35;
    drawAt(x, y, dayNames[i], 0, false);
  }

  // Print days of the month
  int days = daysInMonth(year, month);
  int firstDay = dayOfWeek(year, month, 1);
  int x = xoff;
  int y = yoff + 65;

  for (int i = 0; i < firstDay; i++) {
    x += dx;  // shift pos of first day
  }
  epd.setFont(&FreeSans9pt7b);
  for (int d = 1; d <= days; d++) {
    sprintf(buf, "%d", d);
    drawAt(x, y, buf, shifts[d - 1], d == day);
    x += dx;
    if ((firstDay + d) % 7 == 0) {
      y += dy;
      x = xoff;
    }
  }
}

void updateTime(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(10, 60);
  epd.setPartialWindow(0, 0, 400, 100);
  epd.printf("%s %02d:%02d", dayLongNames[t.tm_wday], t.tm_hour, t.tm_min);
}

void updateCalendar(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  drawCalendar(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
}

bool isNewDay() {
  struct tm t;
  getLocalTime(&t);
  return t.tm_hour == 0 && t.tm_min == 0;
}

void syncTime() {
  WiFi.begin(ssid, password);
  for (int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
    delay(100);
  }
  if (WiFi.status() == WL_CONNECTED)
    configTzTime(timezone, ntpServer);
}

void clearScreen() {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_WHITE);
  epd.display();
}

void initEPD() {
  epd.init(115200, true, 50, false);
  epd.setRotation(0); 
}

void setup() {
  Serial.begin(115200);
  syncTime();
  initEPD();
  clearScreen();
  epd.drawPaged(updateCalendar, 0);
  epd.hibernate();
}

void loop() {
  if (isNewDay()) {
    syncTime();
    clearScreen();    
    epd.drawPaged(updateCalendar, 0);
  }
  epd.drawPaged(updateTime, 0);
  epd.hibernate();
  delay(60 * 1000);  // 60 seconds
}

Libraries

The code begins by including necessary libraries for WiFi connectivity, time management, and E-Paper display functionality.

#include <WiFi.h>
#include <time.h>
#include <GxEPD2_BW.h>

Fonts

After that we include the fonts used. You can pick different ones and as long as they are of the same size (9pt, 12pt, 24pt) the calendar layout should work. For list of available fonts look here.

#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>

Constants

Next, we define some constants for WiFi credentials, NTP server, timezone, arrays for day and month names and layout corrections.

const char* ssid = "SSID";
const char* password = "PWD";
const char* ntpServer = "pool.ntp.org";
const char* timezone = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const char* dayNames[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
char* dayLongNames[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* monthNames[] = {
  "January", "February", "March", "April", "May",
  "June", "July", "August", "September", "October", "November", "December"
};

const int shifts[] = {
  3, 0, 0, 1, 0, 0, 0, 0, 0, 2,
  2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  1
};

You will have to replace the SSID and PASSWORD constants for your WiFi, and probably the TIMEZONE you are living in as well.

The time zone specification “AEST-10AEDT,M10.1.0,M4.1.0/3” is for Australia, which corresponds to the Australian Eastern Standard Time (AEST) with daylight saving time adjustments. For other time zone definitions have a look at the Posix Timezones Database. Just copy and paste the string you find there and change the TIMEZONE constant accordingly.

Shifts

The constants for the names of days and months are obvious but the shifts constant requires some explanation. The day names and the days of the month in the calendar view are drawn aligned to the right. I use the function getTextBounds() to achieve this. However, apparently the text bounds computed by this function are not quite accurate and the right alignment is therefore neither. See below:


Right alignment without (left) and with (right) shift correction
Right alignment without (left) and with (right) shift correction

The shifts constants shifts the right alignment by the specified number of pixels for a specific day, e.g. shifts[0] shifts day 1 three pixels to the right. The picture above compares the alignment of the day columns without (left) and with (right) the shift correction. You can see that day numbers on the left are not perfectly right aligned, which is jarring when you look at the full calendar. The shift constants fix this.

Display Configuration

The following line of code creates the display object and links it to the pin connections for CS, SCL, SDA, BUSY, RES, and DC.

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

If you use a different E-paper display you may have to pick a different driver there. The Readme for GxEPD2 library lists all the supported displays and you can find the specifics in the header files, e.g. GxEPD2.h and GxEPD2_display_selection_new_style.h.

Date Functions

Next we have some date functions. The isLeapYear() returns true if the given year is a leap year.

bool isLeapYear(int year) {
  return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

The daysInMonth() function returns the number of days in a given month, accounting for leap years in February.

int daysInMonth(int year, int month) {
  int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  if (month == 2 && isLeapYear(year)) {
    return 29;
  }
  return days[month - 1];
}

And the dayOfWeek() function calculates the day of the week index for a given date. It returns values from 0 (Sunday) to 6 (Saturday).

int dayOfWeek(int year, int month, int day) {
  tm timeinfo = {};
  timeinfo.tm_year = year - 1900;
  timeinfo.tm_mon = month - 1;
  timeinfo.tm_mday = day;
  mktime(&timeinfo);
  return timeinfo.tm_wday;
}

Drawing Functions

drawAt Function

The drawAt() function is responsible for drawing text on the E-Paper display at specified coordinates, with optional marking.

void drawAt(int x, int y, const char* text, int shift, bool mark) {
  int16_t xb, yb;
  uint16_t wb, hb;
  epd.getTextBounds(text, 0, 0, &xb, &yb, &wb, &hb);
  epd.setCursor(x - wb - shift, y);
  epd.setTextColor(mark ? GxEPD_WHITE : GxEPD_BLACK);
  if (mark) {
    epd.fillRect(x - wb - shift - 3, y - hb - 3, wb + 8, hb + 8, GxEPD_BLACK);
  }
  epd.print(text);
}

The marking is use to highlight the current day by underlying it with a black background and writing the text in white. Below an example how this looks like for day 6:

Highlighting of the current day
Highlighting of the current day

Note the shift parameter, which is used to correct the right alignment of the days as described above. We get the text bounds by calling getTextBounds(), then shift the text to the right by subtracting the with wb of the text from the drawing position x and also subtracting the shift correction.

drawCalendar Function

The drawCalendar() function draws the entire calendar for a specified day, month and year.

void drawCalendar(int year, int month, int day) {
  ...
  // Print name of month and the year
  epd.setPartialWindow(0, yoff - 20, 400, 300 - (yoff - 20));
  epd.fillRect(6, yoff - 20, 400 - 12, 28, GxEPD_BLACK);
  epd.setFont(&FreeSansBold12pt7b);
  epd.setTextColor(GxEPD_WHITE);
  epd.setCursor(xoff, yoff);
  epd.printf("%s %d", monthNames[month - 1], year);

  // Print names of week days
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold9pt7b);
  xoff += 30;
  for (int i = 0; i < 7; i++) {
    int x = xoff + i * dx;
    int y = yoff + 35;
    drawAt(x, y, dayNames[i], 0, false);
  }

  // Print days of the month
  int days = daysInMonth(year, month);
  int firstDay = dayOfWeek(year, month, 1);
  int x = xoff;
  int y = yoff + 65;

  for (int i = 0; i < firstDay; i++) {
    x += dx;  // shift pos of first day
  }
  epd.setFont(&FreeSans9pt7b);
  for (int d = 1; d <= days; d++) {
    sprintf(buf, "%d", d);
    drawAt(x, y, buf, shifts[d - 1], d == day);
    x += dx;
    if ((firstDay + d) % 7 == 0) {
      y += dy;
      x = xoff;
    }
  }
}

The calendar view is essentially divided into four parts. The first part (1) at the top shows the name of the current day and the current time. The second part (2) shows the current month and year. Part (3) shows the names of the days. And part (4) shows days of the months.

Components of calendar view
Parts of calendar view

updateTime Function

The drawCalendar() function shown above draws parts 2 to 4, while the updateTime() function below draws part 1. It gets the current local time and displays it.

void updateTime(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(10, 60);
  epd.setPartialWindow(0, 0, 400, 100);
  epd.printf("%s %02d:%02d", dayLongNames[t.tm_wday], t.tm_hour, t.tm_min);
}

updateCalendar Function

Similarly, the updateCalendar() function gets the current date and then calls drawCalendar() to update the shown calendar.

void updateCalendar(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  drawCalendar(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
}

Note that both update functions, updateTime() and updateCalendar() perform a partial refresh, which is faster and avoids display flickering. If you want to learn more about the partial refresh operation, have a look at the Partial Refresh of e-Paper Display tutorial.

Time Synchronization

The syncTime() function creates a WiFi connection and then synchronizes the internal clock of the ESP32 with the SNTP server “pool.ntp.org” by calling configTzTime().

void syncTime() {
  WiFi.begin(ssid, password);
  for (int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
    delay(100);
  }
  if (WiFi.status() == WL_CONNECTED)
    configTzTime(timezone, ntpServer);
}

You can specify other, or even multiple SNTP servers to connect to. Have a look at the How to synchronize ESP32 clock with SNTP server tutorial for more information.

Note that the syncTime() function tries only 10 times to connect to the WiFi and then gives up. This has the advantage that the calendar still runs, even if no WiFi is available. Otherwise, the code would block here until WiFi becomes available.

Setup Function

In the setup() function, we initialize the serial communication, synchronize time, initialize the E-Paper display, clear the screen, and draw the calendar.

void setup() {
  Serial.begin(115200);
  syncTime();
  initEPD();
  clearScreen();
  epd.drawPaged(updateCalendar, 0);
  epd.hibernate();
}

That means, every time the ESP32 is reset the time is synchronized and you don’t have to worry about setting time and date manually.

Loop Function

The loop() function checks if a new day has started, synchronizes the time if it has, clears the screen, and updates the displayed calendar and time.

void loop() {
  if (isNewDay()) {
    syncTime();
    clearScreen();    
    epd.drawPaged(updateCalendar, 0);
  }
  epd.drawPaged(updateTime, 0);
  epd.hibernate();
  delay(60 * 1000);  // 60 seconds
}

The calendar is redrawn/updated only once every new day, while the time display is updated every 60 seconds. All these updates are partial refreshes, which are fast and without flickering.

However, the clearScreen() function that is called once a day, when the calendar is refreshed, performs a full refresh. This is to avoid a “burn in” of the after-images from the partial refresh. The Waveshare Manual has more information on this.

Note that the daily time synchronization takes care of changes in daylight savings time but there will be a few hours, when the clock will be off. You can avoid this by synchronizing every 30 minutes or so. Have a look at the Digital Clock on e-Paper Display tutorial for example code.

Conclusions

In this tutorial you learned how implement a Monthly Calendar on an E-Paper Display using an ESP32.

E-Paper displays consume very little power and are therefore great for battery powered projects. You could run the ESP32 with the calendar on a LiPo battery but in this case it would be better to put the ESP32 into deep-sleep mode between display updates. I didn’t show you how this can be done in this tutorial but the Digital Clock on e-Paper Display has example code for this.

Also, for a battery powered calendar it would be nice to monitor the charge level of the LiPo battery and display a little battery symbol. Have a look at the Monitor Battery Levels with a MAX1704X tutorial, if you want to learn more about battery monitoring.

Apart from the battery charge you could also display ambient temperature or weather data. Have a look at the Weather Station on e-Paper Display tutorial for more on that.

If you have any comments, feel free to leave them in the comment section.

Happy Tinkering ; )