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 :)
12 commenti
Bastian Brumbi
@sven
Bitte überprüfen Sie, ob der Inhalt in der Antwort des Google Servers im Richtigen Format vorliegt (tStart; Titel; tEnd). Da die extract Funktion mit dem Modulo Operator arbeitet ist dies für die Reihenfolge von Bedeutung.
@tom
Wenn Sie über den Link die Antwort des Servers erhalten, sollte die URL stimmen. Überprüfen Sie, dass der Host und Path in den separaten Variablen getrennt sind. Leerzeichen am Anfang oder Ende könnten auch zu einem Fehler führen.
Tom
Hallo,
ich brauche auch ein wenig Hilfe:
Verbindungsaufbau klappt, aber das “client→Get” erzeugt den (HTML-)Fehler 404.
Das Script konnte ich mit der URL, die AppsScript zur Verfügung stellt, testen. Soweit scheint das in Ordnung zu sein. Ich habe die ID mehrfach überprüft.
Hat noch jemand einen Tipp für mich?
Danke
Sven Lorenzen
Hi @all,
leider funktioniert es bei nicht richtig, es wird mir nicht der Terminname (event) angezeigt, der Tag und Zeit ist auch nicht ganz richtig. Bei mir steht Fr; 20:15 – ;0 0:0 die Endzeit wird nicht angezeigt und nur 1 Termin obwohl mehr vorhanden sind.
Wenn ich den Link von WebApp aufrufe werden mir im Browser alle Termine angezeigt mit Terminname und der Startzeit/Endzeit.
Ausführen als
Ich
Zugriffsberechtigte
Jeder
Könnt ihr mir helfen ?
Danke im vorraus
Gruß Sven
Eckmar Schmitz
Ich habe ein Problem mit der Google Schnittstelle.
Es folgen nach dem ersten GET mit den bereitgestellten Daten von Google weitere Redirection und zwar von:
Host: script.google.com
GET /macros/s/key/exec nach
redirHost: accounts.google.com
GET /ServiceLogin?passive=xxxxx&continue=….. nächstes redir
GET /InteractiveLogin?continue=…….. nächstes redir
GET /v3/signin/identifier?continue=….. danach Status code: 200
Dann wird ein Cookie von 335324 Bytes geschickt. Dann wird die HTTPSRedirect verlassen aber es werden in der readCalendar Funktion mit getResponseBody keine Daten empfangen.
Ich habe das Ganze mal kurz als Windows Form App in .Net geschrieben. Dort läuft es ohne Probleme.
Reimund
Hallo wie druckt man denn das Gehäuse ? Mit Support?
Henry
Korrektur, man muss natürlich etwas in den Kalender eintragen, dann kommt etwas.
Die Berechtigung muss aber erteilt werden.
Jetzt muss nur noch der “richtige” Kalender laufen.
cu
Henry
Hallo,
auch bei einem neu angelegtem Kalender erfolgt diese Abfrage, da dies ja noch nichts mit dem Kalendernamen zu tun hat.
Zugriffsberechtigte “jeder” kann nur bei ausführen als “ich” ausgewählt werden.
Nach → weiter kommt die Abfrage.
Auch nach zulassen des Zugriffes der Webapp auf den Kalender kommen mit der URL keine Daten.
cu
Bastian Brumbi
Sehr geehrter Leser,
erstellen Sie einfach wie unter Punkt 1 beschrieben einen neuen Kalender auf der Google Calendar Website. Hier müssen Sie einen Namen für den Kalender eintragen, diesen Namen tragen Sie in die Google Script Datei anstatt Test ein. Eine weitere Autorisierung ist hier nicht notwendig.
Ich hoffe ich konnte ihnen Weiterhelfen.
Grüße,
Bastian Brumbi
Henry
Ganz so einfach wie beschrieben ist es nicht.
Wo findet man denn den korrekten Kalendernamen von Google, der statt (test) eingetragen werden muss?
Und “jeder” funktioniert auch nicht wie beschrieben, hier muss anschließend autorisiert werden, nur wie?
cu
Andreas Wolter
If you use the download, it should work. The original source code on the homepage may have been changed during translation.
Best regards,
Andreas Wolter
AZ-Delivery Blog
laatste
use the code from the german language page because the the english translation makes a mess of the original code.
Rob Versteden
The code to copy has several flaws. Use the printed one to correct it before implementing. I guess you have used an overactive spelling corrector.
But it’s a nice project!