Dans ce tutoriel, vous apprendrez à implémenter un calendrier mensuel sur un écran e-Paper avec un ESP32. L’écran affichera également l’heure actuelle et synchronisera l’heure avec un fournisseur de temps internet (serveur SNTP), afin que l’heure et le calendrier soient toujours précis. Plus besoin de régler manuellement l’heure et la date.
Pièces requises
J’utilise l’ESP32 lite comme microprocesseur, car il est peu coûteux et dispose d’une interface de charge de batterie, ce qui permet de faire fonctionner le calendrier sur une batterie LiPo. Tout autre ESP32 ou ESP8266 avec une mémoire suffisante fonctionnera aussi, mais il est préférable d’en choisir un avec une interface de charge de batterie. Vous devriez aussi pouvoir utiliser un Arduino, mais je ne l’ai pas essayé.

Écran e-Paper 4,2″

ESP32 lite

Câble USB de données

Jeu de fils Dupont

Plaque d’essai (breadboard)
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Écran e-Paper
L’écran e-Paper utilisé dans ce projet est un module de 4,2″ avec une résolution de 400×300 pixels, 4 niveaux de gris, un temps de rafraîchissement partiel de 0,4 seconde, et un contrôleur intégré avec interface SPI.

Notez que le module possède un petit cavalier/interrupteur à l’arrière pour passer du SPI 4 fils au SPI 3 fils. Nous allons utiliser ici le SPI 4 fils par défaut. Vous ne devriez donc rien avoir à changer.

Le module d’écran fonctionne en 3,3V ou 5V, a un courant de veille très faible de 0,01µA et consomme environ 26,4mW lors du rafraîchissement. Pour plus d’informations sur les écrans e-Paper en général, consultez les deux tutoriels suivants : Interfacing Arduino To An E-ink Display et Weather Station on e-Paper Display .
Connexion et test de l’e-Paper
Commençons par connecter et tester le fonctionnement de l’e-Paper. L’image suivante montre le câblage complet pour l’alimentation et le SPI.

Voici un tableau récapitulatif de toutes les connexions pour plus de commodité. Notez que vous pouvez alimenter l’écran en 3,3V ou 5V, mais l’ESP32-lite ne fournit que du 3,3V et les lignes de données SPI doivent être en 3,3V !
| Écran e-Paper | ESP32 lite |
|---|---|
| CS/SS | 5 |
| SCL/SCK | 18 |
| SDA/DIN/MOSI | 23 |
| BUSY | 15 |
| RES/RST | 2 |
| DC | 0 |
| VCC | 3,3V |
| GND | G |
Vous pouvez utiliser une breadboard pour tout câbler. Mais j’ai en fait connecté l’écran directement à l’ESP32 avec des fils Dupont. La photo ci-dessous montre à quoi ressemblait mon montage.

Installer la bibliothèque GxEPD2
Avant de pouvoir dessiner ou écrire sur l’écran e-Paper, nous devons installer deux bibliothèques. La Adafruit_GFX bibliothèque graphique, qui fournit un ensemble commun de primitives graphiques (texte, points, lignes, cercles, etc.). Et la GxEPD2 bibliothèque, qui fournit le pilote graphique pour l’écran e-Paper.
Installez simplement les bibliothèques de la manière habituelle. Après l’installation, elles devraient apparaître dans le Library Manager comme suit.

Code de test
Une fois la bibliothèque installée, lancez le code de test suivant pour vous assurer que tout fonctionne.
#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() {}
Le code affiche le texte « Makerguides » et après un clignotement de l’écran (rafraîchissement complet), vous devriez le voir sur votre écran comme montré ci-dessous :

Sinon, quelque chose ne va pas. Le plus probable est que l’écran n’est pas correctement câblé ou que le mauvais pilote d’écran est sélectionné. La ligne critique du code est celle-ci, où nous spécifions le pilote pour l’écran 4,2″ :
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
La Readme GxEPD2 bibliothèque liste tous les écrans supportés et vous pouvez trouver les détails dans les fichiers d’en-tête, par exemple GxEPD2.h . Trouvez le pilote spécifique à votre écran. Cela peut demander quelques essais.
Code pour un calendrier sur e-Paper
Dans cette section, nous allons écrire le code pour notre calendrier. La capture d’écran ci-dessous montre à quoi il ressemblera :

Le jour et l’heure actuels sont affichés en haut. En dessous, l’écran montre le mois en cours, l’année et les jours du mois avec le jour actuel marqué.
Voici le code de ce calendrier. Jetez un coup d’œil rapide pour avoir une vue d’ensemble, puis nous entrerons dans les détails :
#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 days[month-1];
}
// 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 = 100;
// 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, 80);
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
}
Bibliothèques
Le code commence par inclure les bibliothèques nécessaires pour la connectivité WiFi, la gestion du temps et la fonctionnalité de l’écran e-Paper.
#include <WiFi.h> #include <time.h> #include <GxEPD2_BW.h>
Polices
Ensuite, nous incluons les polices utilisées. Vous pouvez en choisir d’autres et tant qu’elles ont la même taille (9pt, 12pt, 24pt), la mise en page du calendrier devrait fonctionner. Pour la liste des polices disponibles, consultez here .
#include <Fonts/FreeSans9pt7b.h> #include <Fonts/FreeSansBold9pt7b.h> #include <Fonts/FreeSansBold12pt7b.h> #include <Fonts/FreeSansBold24pt7b.h>
Constantes
Ensuite, nous définissons quelques constantes pour les identifiants WiFi, le serveur NTP, le fuseau horaire, les tableaux des noms des jours et des mois, ainsi que les corrections de mise en page.
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
};
Vous devrez remplacer les constantes SSID et PASSWORD par vos identifiants WiFi, et probablement aussi la constante TIMEZONE correspondant à votre fuseau horaire.
La spécification du fuseau horaire » AEST-10AEDT,M10.1.0,M4.1.0/3 » correspond à l’Australie, soit l’heure normale de l’Est australien (AEST) avec les ajustements pour l’heure d’été. Pour d’autres définitions de fuseaux horaires, consultez le Posix Timezones Database . Copiez-collez simplement la chaîne que vous y trouverez et modifiez la constante TIMEZONE en conséquence.
Décalages
Les constantes pour les noms des jours et des mois sont évidentes, mais la constante shifts nécessite une explication. Les noms des jours et les jours du mois dans la vue calendrier sont alignés à droite. J’utilise la fonction getTextBounds() pour cela. Cependant, apparemment, les limites de texte calculées par cette fonction ne sont pas tout à fait précises, et l’alignement à droite non plus. Voir ci-dessous :

La constante shifts décale l’alignement à droite du nombre de pixels spécifié pour un jour donné, par exemple shifts[0] décale le jour 1 de trois pixels vers la droite. L’image ci-dessus compare l’alignement des colonnes de jours sans (gauche) et avec (droite) la correction de décalage. Vous pouvez voir que les numéros de jours à gauche ne sont pas parfaitement alignés à droite, ce qui est gênant quand on regarde le calendrier complet. Les constantes de décalage corrigent cela.
Configuration de l’écran
La ligne de code suivante crée l’objet écran et le relie aux broches CS, SCL, SDA, BUSY, RES et 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));
Si vous utilisez un autre écran e-Paper, vous devrez peut-être choisir un autre pilote. La bibliothèque Readme GxEPD2 liste tous les écrans supportés et vous pouvez trouver les détails dans les fichiers d’en-tête, par exemple GxEPD2.h et GxEPD2_display_selection_new_style.h .
Fonctions de date
Ensuite, nous avons quelques fonctions de date. La fonction isLeapYear() renvoie true si l’année donnée year est bissextile.
bool isLeapYear(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
La fonction daysInMonth() renvoie le nombre de jours dans un mois donné, en tenant compte des années bissextiles pour février.
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];
}
Et la fonction dayOfWeek() calcule l’indice du jour de la semaine pour une date donnée. Elle renvoie des valeurs de 0 (dimanche) à 6 (samedi).
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;
}
Fonctions de dessin
Fonction drawAt
La fonction drawAt() est responsable de dessiner du texte sur l’écran e-Paper à des coordonnées spécifiées, avec un marquage optionnel.
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);
}
Le marquage sert à mettre en évidence le jour actuel en le soulignant avec un fond noir et en écrivant le texte en blanc. Voici un exemple pour le jour 6 :

Notez le paramètre shift , qui sert à corriger l’alignement à droite des jours comme expliqué plus haut. Nous obtenons les limites du texte en appelant getTextBounds() , puis décalons le texte vers la droite en soustrayant la largeur wb du texte de la position de dessin x et en soustrayant aussi la correction shift .
Fonction drawCalendar
La fonction drawCalendar() dessine le calendrier complet pour un jour, un mois et une année spécifiés.
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;
}
}
}
La vue calendrier est essentiellement divisée en quatre parties. La première partie (1) en haut affiche le nom du jour actuel et l’heure actuelle. La deuxième partie (2) affiche le mois et l’année en cours. La partie (3) affiche les noms des jours. Et la partie (4) affiche les jours du mois.

Fonction updateTime
La fonction drawCalendar() ci-dessus dessine les parties 2 à 4, tandis que la fonction updateTime() ci-dessous dessine la partie 1. Elle récupère l’heure locale actuelle et l’affiche.
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);
}
Fonction updateCalendar
De même, la fonction updateCalendar() récupère la date actuelle puis appelle drawCalendar() pour mettre à jour le calendrier affiché.
void updateCalendar(const void* pv) {
struct tm t;
getLocalTime(&t);
drawCalendar(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
}
Notez que les deux fonctions de mise à jour, updateTime() et updateCalendar() effectuent un rafraîchissement partiel, qui est plus rapide et évite le clignotement de l’écran. Si vous voulez en savoir plus sur le rafraîchissement partiel, consultez le tutoriel Partial Refresh of e-Paper Display .
Synchronisation de l’heure
La fonction syncTime() crée une connexion WiFi puis synchronise l’horloge interne de l’ESP32 avec le serveur SNTP » pool.ntp.org » en appelant 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);
}
Vous pouvez spécifier d’autres serveurs SNTP, voire plusieurs, pour la connexion. Consultez le tutoriel How to synchronize ESP32 clock with SNTP server pour plus d’informations.
Notez que la fonction syncTime() tente seulement 10 fois de se connecter au WiFi avant d’abandonner. Cela a l’avantage que le calendrier continue de fonctionner même si aucun WiFi n’est disponible. Sinon, le code bloquerait ici jusqu’à ce que le WiFi soit disponible.
Fonction setup
Dans la fonction setup() , nous initialisons la communication série, synchronisons l’heure, initialisons l’écran e-Paper, effaçons l’écran et dessinons le calendrier.
void setup() {
Serial.begin(115200);
syncTime();
initEPD();
clearScreen();
epd.drawPaged(updateCalendar, 0);
epd.hibernate();
}
Cela signifie qu’à chaque redémarrage de l’ESP32, l’heure est synchronisée et vous n’avez pas à vous soucier de régler manuellement l’heure et la date.
Fonction loop
La fonction loop() vérifie si un nouveau jour a commencé, synchronise l’heure si c’est le cas, efface l’écran et met à jour le calendrier et l’heure affichés.
void loop() {
if (isNewDay()) {
syncTime();
clearScreen();
epd.drawPaged(updateCalendar, 0);
}
epd.drawPaged(updateTime, 0);
epd.hibernate();
delay(60 * 1000); // 60 seconds
}
Le calendrier est redessiné/mis à jour une seule fois par jour, tandis que l’affichage de l’heure est mis à jour toutes les 60 secondes. Toutes ces mises à jour sont des rafraîchissements partiels, rapides et sans clignotement.
Cependant, la fonction clearScreen() appelée une fois par jour, lors de la mise à jour du calendrier, effectue un rafraîchissement complet. Cela évite un « burn-in » des images résiduelles du rafraîchissement partiel. Le tutoriel Waveshare Manual donne plus d’informations à ce sujet.
Notez que la synchronisation quotidienne de l’heure prend en compte les changements d’heure d’été, mais il y aura quelques heures où l’horloge sera décalée. Vous pouvez éviter cela en synchronisant toutes les 30 minutes environ. Consultez le tutoriel Digital Clock on e-Paper Display pour un exemple de code.
Conclusions
Dans ce tutoriel, vous avez appris à implémenter un calendrier mensuel sur un écran e-Paper avec un ESP32.
Les écrans e-Paper consomment très peu d’énergie et sont donc parfaits pour les projets alimentés par batterie. Vous pourriez faire fonctionner l’ESP32 avec le calendrier sur une batterie LiPo, mais dans ce cas, il serait préférable de mettre l’ESP32 en mode deep-sleep entre les mises à jour de l’écran. Je ne vous ai pas montré comment faire cela dans ce tutoriel, mais le tutoriel Digital Clock on e-Paper Display contient un exemple de code pour cela.
De plus, pour un calendrier alimenté par batterie, il serait intéressant de surveiller le niveau de charge de la batterie LiPo et d’afficher un petit symbole de batterie. Consultez le tutoriel Monitor Battery Levels with a MAX1704X si vous souhaitez en savoir plus sur la surveillance de batterie.
En plus de la charge de la batterie, vous pourriez aussi afficher la température ambiante ou les données météo. Consultez le tutoriel Weather Station on e-Paper Display pour plus d’informations à ce sujet.
Si vous avez des commentaires, n’hésitez pas à les laisser dans la section commentaires.
Bon bricolage ; )

