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
USB Data Cable
Dupont Wire Set
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.
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.
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.
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 display | ESP32 lite |
---|---|
CS/SS | 5 |
SCL/SCK | 18 |
SDA/DIN/MOSI | 23 |
BUSY | 15 |
RES/RST | 2 |
DC | 0 |
VCC | 3.3V |
GND | G |
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.
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.
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:
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:
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:
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:
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.
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 ; )
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.