Hardware
An Hardware benötigen wir:
ESP8266 Mikrocontroller NodeMCU oder D1 Mini
Optional:
3D gedrucktes Gehäuse
Schrauben & Schmelzgewinde
Als erstes muss das Display an den Mikrocontroller angeschlossen werden. Dieses kann entweder über eine direkt gelötete Kabelverbindung verbunden werden, oder über ein selbstgekrimptes Kabel, welches an die Stiftleiste des Displays gesteckt wird.
ESP8266 Node MCU | 2.9“ epaper Display | |
---|---|---|
3V3 | VCC | |
GND | GND | |
D7 | SDI | |
D5 | SCLK | |
D4 | CS | |
D2 | D/C | |
D1 | Reset | |
D3 | Busy |
Verdrahtung mit Buchsenleiste
Buchsenleiste wie gezeigt anlöten
Software
Die benötigte Software besteht aus zwei Teilen:
1 Google Script
Gehen Sie als erstes auf die Google Calendar Website, melden Sie sich mit Ihrem Account an und erstellen Sie links bei „Weitere Kalender“ einen neuen Kalender. Unter diesem Kalender speichern Sie zukünftig alle Termine, welche auf dem Display angezeigt werden sollen.
Öffnen Sie nun in Ihrem Browser die Google Script Website, melden Sie sich hier mit dem gleichen Google Konto wie oben an. Klicken Sie als Nächstes links oben auf den Button „Neues Projekt“,
es öffnet sich nun ein Fenster in welches Sie folgenden Code kopieren:
// Copyright (c) 2024 Bastian Brumbi
function doGet(e) {
var str = '';
//Berechne den Start und Endzeitpunkt der Kalenderabfrage
var start = new Date();
start.setHours(0, 0, 0);
const oneday = 24*3600000;
const stop = new Date(start.getTime() + 14 * oneday); // 14 Tage
//lese Daten aus Kalender aus (Bei mehreren Kalendern kopieren)
var calendar = CalendarApp.getCalendarsByName('test')[0]; //Hier den Namen des Kalenders eintragen
if (calendar == undefined) {
Logger.log("Kein Zugriff");
return ContentService.createTextOutput("");
}
//Termine abrufen
var events = calendar.getEvents(start, stop);
//Bilde einen String aus den Daten, welcher am ende über den API-Link zurückgegeben wird
for (var ii = 0; ii < events.length; ii++) {
var event=events[ii];
str += event.getStartTime() + ';' +
event.getTitle() +';' +
event.getEndTime() + ';'
;
}
return ContentService.createTextOutput(str);
}
Zum Schluss drücken Sie auf das blaue Feld „Bereitstellen“ und wählen Sie dort „Neue Bereitstellung“ aus. Im neu geöffneten Fenster wählen Sie unter dem Zahnrad die Bereitstellungsoption „Web-App“.
Tragen Sie nun eine Beschreibung ein und ändern Sie das Zugriffsrecht auf „Jeder“. Im nächsten Schritt wird Ihnen eine „Bereitstellungs-ID“ angezeigt. Dabei handelt es sich um den API-key, über welchen der ESP später auf die Daten zugreifen kann. Dieser muss dann in den Quellcode eingefügt werden. Um das Script zu testen, können Sie den darunter stehenden Link öffnen. Daraufhin sollten die Termine der nächsten 14 Tage mit Start- und Enddatum mit einem Semikolon getrennt angezeigt werden.
Dieses Script liest die Termine nur aus einem Kalender aus. Wenn Sie mehrere Kalender anzeigen lassen wollen, müssen Sie den Block kopieren und mit geändertem Kalendernamen darunter einfügen. (Auf dem Display werden aber nur die ersten 4 Termine der Kette angezeigt).
Falls Sie weitere Daten aus Ihrem Kalender lesen wollen, finden Sie die nötigen Informationen in den Google Developer Docs.
2 Arduino IDE/Platform IO
Arduino IDE
Zuerst müssen Sie noch die zugehörigen Libraries installieren:
Falls Sie zum ersten Mal einen Mikrocontroller mit ESP8266 Prozessor programmieren, müssen Sie noch die folgende Zeile unter Preferences in das Boardverwalter-Url Feld kopieren: http://arduino.esp8266.com/stable/package_esp8266com_index.json. Installieren Sie im Anschluss über den Boardmanager das ESP8266 Paket.
Platform IO
Kopieren Sie folgende Zeilen in die platformio.ini Datei ihres Projekts:
[env:esp8266]
platform = espressif8266
board = d1_mini
framework = arduino
monitor_speed = 115200
lib_deps =
bblanchon/ArduinoJson@^7.1.0
https://github.com/electronicsguy/HTTPSRedirect.git
wifi
zinggjm/GxEPD2@^1.4.8
adafruit/Adafruit GFX Library@^1.11.10
Der Sketch
Als erstes werden die benötigten Bibliotheken und die mit der Display Library installierten Fonts (Schriftarten) eingebunden.
xxxxxxxxxx
// Copyright (c) 2024 Bastian Brumbi
// Google Calendar epaper
// https://github.com/bblanchon/ArduinoJson
// https://github.com/electronicsguy/ESP8266/tree/master/HTTPSRedirect
Im Anschluss werden structs für die einzelnen Termine und deren Zeit erstellt. Diese werden dann in Form einer Liste mit vier Elementen gespeichert.
const int MAX_ENTRIES = 4;
struct Time { //Zeit
String day;
int date;
int hour;
int min;
};
struct Entry { //Kalenderevents
String title;
Time start;
String startDate;
Time end;
String endDate;
};
Entry entries[MAX_ENTRIES]; //Liste der Events
Danach werden Variablen für den Zeilenumbruch bei längeren Namen und für das Netzwerk initialisiert.
int offset = 0; //Verschiebung bei Zeilenumbruch
//Hier API-key eintragen
char const * const dstHost = "script.google.com";
char const * const dstPath = "/macros/s/ key /exec";
int const dstPort = 443;
//Hier Wlan Zugangsdaten eintragen
String SSID = " ";
String Password = " ";
Nun erstellen wir Objekte für den WiFiClient, welcher später für das Lesen der Daten vom Server verwendet wird und ein Objekt für das Display. Sollte ihr Display nicht funktionieren (ältere Version), wählen Sie einen anderen Treiber aus der Liste, indem Sie die Kommentarzeichen der Zeile entfernen.
WiFiClient client;
GxEPD2_3C<GxEPD2_290c, GxEPD2_290c::HEIGHT> display(GxEPD2_290c( 2, 4, 5, 0));
// GDEW029Z10 128x296, UC8151 (IL0373)
// GxEPD2_3C<GxEPD2_290_C90c, MAX_HEIGHT(GxEPD2_290_C90c)> display(GxEPD2_290_C90c( 2, 4, 5, 0));
// GxEPD2_3C<GxEPD2_290_Z13c, GxEPD2_290_Z13c::HEIGHT> display(GxEPD2_290_Z13c( 2, 4, 5, 0));
// GDEH029Z13 128x296, UC8151D
// GxEPD2_3C<GxEPD2_290_C90c, GxEPD2_290_C90c::HEIGHT> display(GxEPD2_290_C90c( 2, 4, 5, 0));
// GDEM029C90 128x296, SSD1680
Im Folgenden können Sie die Farben des Titels, der Terminnamen und der Terminzeit, sowie den Titel festlegen.
String title = "Anstehende Termine";
void extractData(const String& str);
String getValue(String data, char separator, int index);
void readCalendar();
void displayEvent(int i, int y);
void convTime();
void convDayName();
void errorCode();
Im setup() wird lediglich das Display und die Status LED initialisiert.
void setup() {
pinMode(15, OUTPUT); //Status LED
display.init(115200);
display.setRotation(3);
display.setFullWindow();
}
Im loop() verbindet sich der ESP zuerst mit dem WLAN, während dessen blinkt die Status LED. Ist nach 20 Sekunden immer noch keine Verbindung aufgebaut, wird der ESP neugestartet. Nach erfolgreicher Verbindung wird das Display unter Verwendung der Methode displayEvent() beschrieben.
void loop() {
pinMode(15, HIGH);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, Password);
long t_start = millis();
while (WiFi.status() != WL_CONNECTED) {
digitalWrite(15, LOW);
delay(500);
digitalWrite(15, HIGH);
delay(250);
if(millis() - t_start > 20000) ESP.restart(); //nach 20 sek keine Verbingung - Neustart
}
display.firstPage();
do
{
readCalendar();
convTime();
convDayName();
display.fillScreen(GxEPD_WHITE);
display.setFont(&FreeMonoBold12pt7b);
display.setCursor(20, 15);
display.setTextColor(TITELCOL);
display.print(title);
display.drawLine(0, 20, display.width(), 20, GxEPD_BLACK);
displayEvent(0, 40);
displayEvent(1, 60);
displayEvent(2, 80);
displayEvent(3, 100);
}
while (display.nextPage());
digitalWrite(15, LOW);
display.powerOff();
WiFi.disconnect();
delay(1000 * 60 * 60 * 1);
}
Folgende Methode baut eine Verbindung zum Server auf, liest die Daten in Form eines String und extrahiert durch den Aufruf von extractData() die einzelnen Daten (Startzeit, Name, Endzeit).
void readCalendar() {
HTTPSRedirect* client = nullptr;
//Verbindung mit Server aufbauen
client = new HTTPSRedirect(dstPort);
client->setInsecure();
client->setPrintResponseBody(false);
client->setContentTypeHeader("application/json");
bool flag = false;
for (int i = 0; i < 5; i++) {
int retval = client->connect(dstHost, dstPort);
if (retval == 1) {
flag = true;
break;
}
else errorCode();
}
if (!flag) {
errorCode();
delete client;
client = nullptr;
return;
}
//Antwort des Servers speichern
client->GET(dstPath, dstHost);
String googleCalData = client->getResponseBody();
//Daten aus String Extrahieren
extractData(googleCalData);
delete client;
client = nullptr;
}
In der nächten Funktion wird nach den Semikolons gesucht, welche die einzelnen Daten trennen und diese Daten anschließend als String in der Liste, in den zugehörigen Elementen, gespeichert.
void extractData(const String& str) {
String temp = str;
int index = 0;
int entryCount = 0;
while (temp.length() > 0 && entryCount < MAX_ENTRIES) {
//Position des ersten Semikolons
int pos = temp.indexOf(';');
if (pos == -1) break;
//gefundene Daten zwischenspeichern und aus Kette löschen
String token = temp.substring(0, pos);
temp = temp.substring(pos + 1);
//Über Index identifizierung welches Element vorliegt
//dann in Liste speichern
if (index % 3 == 0) { // Startdatum
entries[entryCount].startDate = token;
} else if (index % 3 == 1) { // Titel
entries[entryCount].title = token;
} else { // Enddatum
entries[entryCount].endDate = token;
entryCount++;
}
index++;
}
}
Die folgende Methode liest aus dem String, welcher die Zeit speichert, die Minute, Stunde, den Tag und den Wochentag heraus und speichert diese im struct Time der einzelnen Elemente.
void convTime() {
for(int i = 0; i<MAX_ENTRIES; i++) {
String datetimeStr = entries[i].startDate;
//Wochentag
int idx = datetimeStr.indexOf(' ');
entries[i].start.day = datetimeStr.substring(0, idx);
datetimeStr = datetimeStr.substring(idx + 1);
//Monat
idx = datetimeStr.indexOf(' ');
datetimeStr.substring(0, idx);
datetimeStr = datetimeStr.substring(idx + 1);
//Tag
idx = datetimeStr.indexOf(' ');
entries[i].start.date = datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Jahr
idx = datetimeStr.indexOf(' ');
datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Stunde
idx = datetimeStr.indexOf(':');
entries[i].start.hour = datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Minute
idx = datetimeStr.indexOf(':');
entries[i].start.min = datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Sekunde
idx = datetimeStr.indexOf(' ');
datetimeStr.substring(0, idx).toInt();
}
for(int i = 0; i<MAX_ENTRIES; i++) {
String datetimeStr = entries[i].endDate;
//Wochentag
int idx = datetimeStr.indexOf(' ');
entries[i].end.day = datetimeStr.substring(0, idx);
datetimeStr = datetimeStr.substring(idx + 1);
//Monat
idx = datetimeStr.indexOf(' ');
datetimeStr.substring(0, idx);
datetimeStr = datetimeStr.substring(idx + 1);
//Tag
idx = datetimeStr.indexOf(' ');
entries[i].end.date = datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Jahr
idx = datetimeStr.indexOf(' ');
datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Stunde
idx = datetimeStr.indexOf(':');
entries[i].end.hour = datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Minute
idx = datetimeStr.indexOf(':');
entries[i].end.min = datetimeStr.substring(0, idx).toInt();
datetimeStr = datetimeStr.substring(idx + 1);
//Sekunde
idx = datetimeStr.indexOf(' ');
datetimeStr.substring(0, idx).toInt();
}
}
Die Methode convDayName() wandelt den englischen Wochentagsnamen in den deutschen um.
void convDayName() {
for(int i = 0; i<MAX_ENTRIES; i++) {
if(entries[i].start.day == "Mon") entries[i].start.day = "Mo";
else if(entries[i].start.day == "Tue") entries[i].start.day = "Di";
else if(entries[i].start.day == "Wed") entries[i].start.day = "Mi";
else if(entries[i].start.day == "Thu") entries[i].start.day = "Do";
else if(entries[i].start.day == "Fri") entries[i].start.day = "Fr";
else if(entries[i].start.day == "Sat") entries[i].start.day = "Sa";
else if(entries[i].start.day == "Sun") entries[i].start.day = "So";
if(entries[i].end.day == "Mon") entries[i].end.day = "Mo";
else if(entries[i].end.day == "Tue") entries[i].end.day = "Di";
else if(entries[i].end.day == "Wed") entries[i].end.day = "Mi";
else if(entries[i].end.day == "Thu") entries[i].end.day = "Do";
else if(entries[i].end.day == "Fri") entries[i].end.day = "Fr";
else if(entries[i].end.day == "Sat") entries[i].end.day = "Sa";
else if(entries[i].end.day == "Sun") entries[i].end.day = "So";
}
}
displayEvent() wird in der loop() Funktion aufgerufen, um das Event mit dem übergebenen Index auf dem Display anzuzeigen.
void displayEvent(int i, int y) {
display.setFont(&FreeMonoBold9pt7b);display.setTextColor(NAMECOL);
display.setCursor(5, y + offset);
display.print(entries[i].title.substring(0, 12));
if(entries[i].title.substring(12).length() > 0 && (offset == 0 || (offset == 20 && entries[3].start.day.length() == 0) || (offset == 40 && entries[2].start.day.length() == 0))) {
offset += 20;
display.setCursor(5, y + offset - 5);
display.print(entries[i].title.substring(12, 24));
display.setTextColor(TIMECOL);display.setFont();
display.setCursor(145, y + offset - 15);
display.printf("%s;%d %d:%d - %s;%d %d:%d", entries[i].start.day.c_str(), entries[i].start.date, entries[i].start.hour, entries[i].start.min, entries[i].end.day.c_str(), entries[i].end.date, entries[i].end.hour, entries[i].end.min);
}
else if(entries[i].start.day.length() > 0) {
display.setTextColor(TIMECOL);display.setFont();
display.setCursor(145, y-8 + offset);
display.printf("%s;%d %d:%d - %s;%d %d:%d", entries[i].start.day.c_str(), entries[i].start.date, entries[i].start.hour, entries[i].start.min, entries[i].end.day.c_str(), entries[i].end.date, entries[i].end.hour, entries[i].end.min);
}
}
Die folgende Methode wird aufgerufen, um eine Störung über die Status LED anzuzeigen:
xxxxxxxxxx
void errorCode() {
for(int i = 0; i<3; i++) {
digitalWrite(15, LOW);
delay(300);
digitalWrite(15, HIGH);
delay(150);
}
}
Zusammenbau und Bedienung
Drucken Sie das Gehäuse mit einem 3D-Drucker aus und pressen Sie mit einem heißen Lötkolben die Schmelzgewinde (M3) in die vorgesehenen Löcher. Zuletzt kleben Sie das Board mit der USB-Buchse in der Aussparung an und schrauben das Display zuletzt mit den passenden Schrauben von außen fest.
Nachdem Sie eine Stromversorgung angeschlossen haben, werden auf dem Display nun automatisch jede Stunde die nächsten vier Termine angezeigt. Nach Betätigung des Tasters werden die Termine sofort aktualisiert. Das Display wird während des Beschreibens mehrfach aufblinken. Dies ist normal, dabei werden die Farbkapseln im Display ausgerichtet.
Viel Spaß beim Nachbauen :)