Introduction: E-Ink Family Calendar Using ESP32
For many years I have been playing with the idea of breaking the barrier between physical and digital calendars - more specifically creating a nice looking e-ink calendar that can hang in our living room/kitchen. Now the idea has materialized in a very satisfying way, and I would love to share how I made it come true.
The calendar displays the first 9 events for all selected google calendars for a specific user. In my case I have selected that of my wife, myself and our shared family calendar. Besides the calendar, I have created a mini-weather display in the corner, showing an icon from OpenWeather Maps, as well as temperature and windspeed.
The project combine a 7.5 Waveshare e-ink screen, with an ESP32 microcontroller and a LIPO battery. It is packaged in 13x18 IKEA Ribba frame. Besides Arduino code for the microcontroller, I also had to create a google script to extract the calendar entries from google.
Credits to the ESP32 E-Ink Weather Station project on Git, from which I have learned a lot when coding the project.
Find the code for the project here: https://github.com/kristiantm/eink-family-calendar...
Thanks to Ibsendk for sparring and sanity checking of instructable and code.
Supplies
- Waveshare 7.5 inch E-ink Screen (800x600 version) ~76 EUR on Amazon.de (linked to the yellow (C) version, as I could not find the red (B) version anymore)
- LOLIN32 ESP32 ~8-9 USD on Aliexpress (I used an older version, but get the D32 or D32 Pro as it has integrated battery gauge on pin 35)
- LIPO battery with 1800 mAH ~13 EUR on Amazon.de
- IKEA Ribba 13x18 frame ~3 EUR in IKEA
Total cost: ~100 EUR
As an alternative, Waveshare is offering a custom ESP32 unit with an E-Ink port (so you avoid wiring) - however this do not come with the battery plug of LOLIN32, so you have to power it via a 5V powerbank or wire your own battery directly to the 3V and GND connectors.
Step 1: Connect the Screen to the ESP32 Board
The screen comes with a connector cable, that you can connect directly to the board. However, I found that it took much too much space in the frame, so I decided to use my own wires to connect the boards.
Waveshare 7.5 ↔ LOLIN32
Vcc ↔ 3V
GND ↔ GND
DIN ↔ 14
CLK ↔13
CS ↔15
DC ↔27
RST ↔ 26
BUSY ↔25
To test the wiring, I recommend to download the GxEPD2 library and test it out. You can get it both via PlatformIO if you use Visual Studio Code or via Arduino Libraries.
To initialize the display with the right pins, use the following code in the example from the library:
GxEPD2_3C display(GxEPD2_750c_Z08(/*CS=*/ 15, /*DC=*/ 27, /*RST=*/ 26, /*BUSY=*/ 25));
When you make the example work (get demo-text/graphics on the display), you can move on to the next step.
Step 2: Getting Events From Google Calendar
Google has made it a bit hard to integrate with the calendar, so I had to do a workaround via Google Scripts, to make the calendar entries accessible.
To get access, create a new web-app on script.google.com, and paste the following code into it.
- Go to script.google.com and select new project
- Paste the below code into it
- Save it with a good name
- Click "Publish" and select "Anyone, even anonymous" as security setting
- Copy the link "https://script.google.com/macros/s/[UNIQUE CODE]/exec" as you need it in the project
Notice: This makes calendar entries from your calendar publicly available to anyone with the link. However, the link is unique and only you have it. I would love other ideas for how to integrate - but for now this works.
To test the script, paste the URL with the unique code into your browser. You should see a list of events separated by semi-colon. Do not move to the next step, before you have seen this.
<p>function doGet(e) {</p><p> var calendars = CalendarApp.getAllCalendars();</p><p> //var cal = CalendarApp.getCalendarsByName('NAME_OF_CALENDAR')[0]; // 0 is subcalendar ID, mostly "0" //var cal = CalendarApp.getDefaultCalendar(); var calendars = CalendarApp.getAllCalendars(); if (calendars == undefined) { Logger.log("No data"); return ContentService.createTextOutput("no access to calendar hubba"); }</p><p> var calendars_selected = []; for (var ii = 0; ii < calendars.length; ii++) { if (calendars[ii].isSelected()) { calendars_selected.push(calendars[ii]); Logger.log(calendars[ii].getName()); } } Logger.log("Old: " + calendars.length + " New: " + calendars_selected.length);</p><p> const now = new Date(); var start = new Date(); start.setHours(0, 0, 0); // start at midnight const oneday = 24*3600000; // [msec] const stop = new Date(start.getTime() + 14 * oneday); //get appointments for the next 14 days //var events = cal.getEvents(start, stop); //pull start/stop time var events = mergeCalendarEvents(calendars_selected, start, stop); //pull start/stop time var str = ''; for (var ii = 0; ii < events.length; ii++) {</p><p> var event=events[ii]; var myStatus = event.getMyStatus(); // define valid entryStatus to populate array switch(myStatus) { case CalendarApp.GuestStatus.OWNER: case CalendarApp.GuestStatus.YES: case CalendarApp.GuestStatus.NO: case CalendarApp.GuestStatus.INVITED: case CalendarApp.GuestStatus.MAYBE: default: break; } // Show just every entry regardless of GuestStatus to also get events from shared calendars where you haven't set up the appointment on your own str += event.getStartTime() + ';' + //event.isAllDayEvent() + '\t' + //event.getPopupReminders()[0] + '\t' + event.getTitle() +';' + event.isAllDayEvent() + ';'; } return ContentService.createTextOutput(str); }</p><p>function mergeCalendarEvents(calendars, startTime, endTime) {</p><p> var params = { start:startTime, end:endTime, uniqueIds:[] };</p><p> return calendars.map(toUniqueEvents_, params) .reduce(toSingleArray_) .sort(byStart_); }</p><p>function toCalendars_(id) { return CalendarApp.getCalendarById(id); }</p><p>function toUniqueEvents_ (calendar) { return calendar.getEvents(this.start, this.end) .filter(onlyUniqueEvents_, this.uniqueIds); }</p><p>function onlyUniqueEvents_(event) { var eventId = event.getId(); var uniqueEvent = this.indexOf(eventId) < 0; if(uniqueEvent) this.push(eventId); return uniqueEvent; }</p><p>function toSingleArray_(a, b) { return a.concat(b) }</p><p>function byStart_(a, b) { return a.getStartTime().getTime() - b.getStartTime().getTime(); }</p>
Step 3: Enable Battery Level Measurement
To avoid the battery becoming completely discharged, you should enable the battery measurement gauge.
The code in the project, reads the current voltage of the battery, and displays a battery icon on the screen, showing either full, three quarters, half, quarter or empty. When empty the project goes into permanent deep-sleep until it is recharged again.
If you have a LOLIN D32 battery measurement is already build into the GPIO35 pin - so you just have to adjust the pin in the code "uint8_t batteryPin = 35".
If you have a normal ESP32, you need to insert a voltage divider between the battery and a selected analogue IO pin - to bring the battery's 3.7 voltage below the 3V that the board are able to measure.
In my setup, I have used a 30K and a 100K resistor setup, and read from pin 34.
It is a bit complicated to set up, but without it you might drain and damage your battery if you forget to recharge it.
Step 4: Configuring the Project
Now is the time to get the code ready for programming the ESP32 board.
To do this you can use either Visual Studio Code (with Platform IO) or Arduino.
For both platforms download the code, and place it in your project library.
Code here: https://github.com/kristiantm/eink-family-calendar-esp32
For Platform IO:
- Make sure you have Platformio installed and open the project folder as a workspace
- If configured correctly, PlatformIO) should fetch the required libraries itself. If not, you will have to go to PlatformIO / Libraries and install "GxEPD2" and "ArduinoJson"
For Arduino:
- Go to settings and paste "https://dl.espressif.com/dl/package_esp32_index.json" in "Additional Boards manager URL"
- Go to Tools/Boards/Boards Manager. Search for ESP32 and install the board package.
- Select the board WEMOS LOLIN32 (or the board you have bought)
- Go to Library manager and install "GxEPD2" and "ArduinoJson"
Now click compile, and hopefully you will not get errors. When ready connect the LOLIN32 board with a microusb cable to your computer, and program the board.
Boot up and connect to new wifi network:
When done, you need to configure the calendar over wifi. It should appear as a separate wifi network called "espressif32". Connect to this, and you will be redirected to a configuration page.
Configure Calendar:
Before connecting to your home wifi, you should configure the google-api, the open weather api, as well as your longitude and lattitude. You can also change these values later, but by doing it first, you do not have the trouble of finding the calendars new IP on your home network.
1) Register a free account on OpenWeatherMaps.com, get an API and paste it in.
2) Change your location to get local weather - google maps is your friend for getting latitude and longitude.
3) Find the webapp API (the [UNIQUE CODE]) from script.google (from the previous step), and add it to integrate with your calendar.
In the "Configure AP"
Set your home network SSID and password. The calendar will then connect here, and the Espressif hotspot will dissapear forever.
And it works:
If all is well, you will after 20 seconds see a refresh of the Waveshare board with your next 14 days of events, as well as your local weather. If this is not the case, try to do a serial connect to the COM port presented by the LOLIN32 board (you should be able to identify the port number via the device manager in Windows).
You can use a program like PuTTY to connect to the serial port, and observe where the chain is breaking. Also you can use this to find the calendar IP adress if you need to change any settings.
The calendar will be on the wifi for 5 minutes the first time the calendar boots, after which it will start its 24 hour cycle of refreshing at 5 in the morning.
Step 5: Package Your Family Calendar
Now all you have to do, is package the calendar nicely in your new IKEA frame. The Ribba frame is perfect for IOT projects like this, as it has a big closed room between the screen and the back-plate.
First put the display on top of the white Passepartout, and fiddle it a bit fort and back until you are satisfied.
Then place the paper that came with the frame on top of the screen, but lead the screen connector slip through at the side of the frame. Fix this with the white plastic inner frame - using the broad side to apply gentle pressure on the screen.
Then glue the e-paper connecting board, the LOLIN board and the battery to the paper in a way where they hang naturally given their connections.
When you click the battery in, the board will power up and refresh the screen. Use this as an opportunity to do a final adjustment to the placement of the display.
Now put on and secure the back-cover of the frame. Consider to add a short USB cable for powering the frame (cut a hole in the back cover as demonstrated). With a recharge cycle of 2-3 months, I decided that removing the back-cover is ok for me.
You are now the proud owner, of your very own E-Ink Family Calendar.
48 Comments
Question 8 months ago
Hallo Kristian (and others),
Thanks a lot for the instruction. This week I got my calendar up and running and everything works except from the weather. nothing is drawn above the battery icon. Are there other users with suggestions?
Some tips for the others during my setup:
- The display initialize needed a change to: (115200, true, 2, false)
- I used ArduinoJson version 6.19.4, using 6.20.0 resulted in errors
Greetings,
Frits
Answer 7 months ago
And today it is the other way around but I did not made any changes. Now I see the header and the weather information but not the Google calendar.
Reply 7 months ago
Heh - random. Must admit that my weather has been gone for a while now so I probably broke it in one of the releases where I was enabling wifi configuration. Check via the debug console if the data is collected from OWM - if yes - then the issue is in the print to screen part.
Let me know if you figure out how to get it working - then I will fix it in the next release. I will probably not have time myself before later in the month as work and life is busy these days 🙂
Reply 7 months ago
I increased the timeout on the agenda script to 30 seconds, not quite sure if that was the problem. Also I tried al lot to solve the weather problem but i am afraid that i am just not that good in programming.
In the serial monitor i see the api call:
(https://api.openweathermap.org/data/2.5/onecall?AP...
and than the error:
Creating object...and deserializeJson() failed: NoMemory
_PowerOn : 123996
When i try the api call in the browser it returns the weather information.
Question 1 year ago
ich habe wieder mal Zeit gefunden am Projekt
E-Ink Family Calendar zu arbeiten.
Ich habe mir den neuesten Code von Github heruntergeladen und in einen Arduino Sketch importiert.
Das Kompilieren hat geklappt, aber dann erhielt ich foglende Fehlermeldung:
text section exceeds available space in
boardDer Sketch verwendet 1478605 Bytes (112%) des
Programmspeicherplatzes. Das Maximum sind 1310720 Bytes.
Globale Variablen verwenden 103344
Was kann ich tun ?Bytes (31%) des dynamischen Speichers, 224336 Bytes für lokale
Variablen verbleiben. Das Maximum sind 327680 Bytes.
Sketch too big; see
https://support.arduino.cc/hc/en-us/articles/3600... for tips on
reducing it.
Fehler beim Kompilieren für das Board
LOLIN D32.
Noch folgende Verständnisfrage:
Die Include Dateien
credentials.h,
iconsOWM.c
timeheaders.h und
webconfig.h
habe ich als neue Tab in Arduino eingebunden. Ist das richtig oder müssen diese an einer anderen Stelle gespeichert werden ?
Grüße
Peter Spiller
Answer 1 year ago
Hallo Kristian
danke für Deine Hilfe, ich konnte das Problem inzwischen lösen:
Bei Arduino, Werkzeuge war unter Partion Scheme "Standard" ausgewählt, ich habe dies geändert in:
"Minimal SPIFFS (Large APPS with OTA"
Jetzt funktioniert alles bestens.
Nochmals Danke0
Grüße Peter
Answer 1 year ago
Hi Peter
Sorry for the late reply. I remember having that problem at one point. Looking at other solutions (google) I think you need to change the partition size to from "default" to "huge" under [Tools / Partition Scheme].
However - can recommend you use Visual Studio Code and PlatformIO instead - this will automatically download all dependencies, and make sure your libraries are updated. Know it is a bit more technical to get running - but it becomes much easier after having it set up the first time. I am currently mainly using and testing the solution using this.
Best regards,
Kristian
Question 1 year ago
Hallo,
ich habe das Objekt nachgebaut, es funktioniert auch, nur bei der Übernahme der Kalenderdaten gibt es Schwierigkeiten.
Im seriellen Monitor kommt folgende Meldung:
Configuration exist and internet connection works - displaying calendar
Getting calendar
https://script.google.com/macros/s/...,...
Connected to google script
Returncode: -1
Response:
IntexFrom
Wer kann mir helfen ?
Gruß Peter
Answer 1 year ago
Hi Kristian,
ich habe es mehrfach probiert, es kommt immer die gleiche Meldung. Zum Test habe ich einen anderen Link eingegeben, das Programm greift ohne Verzögerung auf die Seite zu und zeigt die Daten im EPaper an. Es gibt beim Zugriff auf Google Script eine kurze Verzögerung, das sind aber höchstens 2 bis 3 Sekunden.
Trotz vieler Versuche kein Erfolg
Gruß Peter
Reply 1 year ago
Hallo Kristian,
ich habe viel probiert ind bin auf eine neue Meldung gestoßen, die nicht immer kommt: "Failed to obtain time"
die erscheint im seriellen Monitor:
Connected to google script
Returncode: -1
Response:
IntexFrom
Failed to obtain time
Könnte das etwas mit dem Fehler zu tun haben ?
Gruß
Peter
Reply 1 year ago
Hmm. It sounds like it is not connecting to the internet. Is it available on the local wifi when you boot it up, or does it make a hotspot?
Reply 1 year ago
Das programm verbindet sich über den Hotspot, aber eine Internetverbindung muss da sein, denn nach dem Start kommt im seriellen Monitor die Meldung:
setup
36b688b7......
Configuration loaded
Internet connected
Configuration exist and internet connection works - displaying calendar
Getting calendar
https://script.google.com/macros/s/AKfyc.......
Connected to google script
Returncode: -1
Response:
IntexFrom
Failed to obtain time
Wie ich bei Github gesehen habe, gibt es eine ältere Version, die sich driekt mit dem WLAN verbindet. Diese Version möchte ich auch mal ausprobieren. Kann man die ncoh irgendwo bekommen ?
Gruß Peter
Answer 1 year ago
Hi Peter
The script line you posted works fine - just try to paste it in the browser. However - remove it from this post, as we all can see your personal calendar events now :)
It seems that google script time out before giving you an answer. This happens to me some times. If the calendar refresh with the right day and the weather information in the corner, it should be configured right. In that case try to reboot it a few times. If it does not show the right weekday and the weather, it means that it is having problems connecting to the internet. Then that is where you should troubleshoot.
Let me know how it goes, and if a few restarts solves the problem.
If anybody else has solved the issue with periodic timeout from google script, feel free to share solutions.
Best regards,
Kristian
Question 1 year ago
Hi Kristian.
I'm a novice in coding, so I'm having a bit trouble following your guide.
I've noticed that the Google Script has changed, so it's unfortunately not as simple as copy-paste.
Using Platform IO for the very first time makes it even harder, as the autoconnect.h wasn't automatically added.
Is there any chance that you can re-write your guide, so it's up to date, and maybe even easier for a rookie like me?
I've already bought every component, som I'm very excited to build your awesome calendar!
Answer 1 year ago
Have fixed it now - had to change a parameter in platformio.ini to ensure it included all dependencies. Now it should work again. Happy building :)
Answer 1 year ago
Hi Jan. Happy to hear that you like the project. Can take a quick look in the weekend. Kristian
1 year ago
Hello.
Christian nice to meet you.
Thank you for providing such a wonderful project.
It seems that I created a library for Unicode UTF8 about a year ago. Can you upload it again?
I tried to fix it myself, but I'm in trouble because I can't avoid the error.
It has the required font information.
Regards, Coffey
Reply 1 year ago
Hi Coffey
You are welcome - was a dream for a long time, so am very happy I realized it 😊
It was @JakobFP that did the UTF8 fonts. Never took the time to get them to work myself as I was busy at the time. Would happily include it in the project though if it is uploaded again 😎
Best regards,
Kristian
Reply 1 year ago
Thank you for your reply.
I will study more.
If that doesn't work, I'll contact @ Jakob
Anyway, this is a great invention, thank you.
Best Regards,
coffey
2 years ago
Kristian, a great project.
Once I worked out your WiFi access point upgrade using autoconnect (connecting to the esp32ap SSID with the default autoconnect 'passpass' password), I have got your setup partially working.
I'm having an issue with getting the calendar information in reliably. Initially the test for whether there was an internet connection using WiFiclientsecure failed (i.e. client.connect("script.google.com", 443) did not return true), however I thought it might be due to https not having SSL certificates etc. - and so I inserted the command "client.setInsecure();" to ignore this, and this then worked and proceeded to download the weather data and activate the google script.
However whilst the weather icons display correctly, the google script only sometimes works. The code does seem to pick up the redirection correctly, but most of the time does not correctly return the string of text the calendar requires (the string is length zero). Any thoughts on what might be wrong?
Simon