E-Ink Family Calendar Using ESP32

21,135

118

44

Introduction: E-Ink Family Calendar Using ESP32

About: I enjoy exploring new technologies and making practical things that fit into our everyday life as a family. Especially like making things with and for my 6 year old son :) Current focus is microcontrollers/se…

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

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.

  1. Go to script.google.com and select new project
  2. Paste the below code into it
  3. Save it with a good name
  4. Click "Publish" and select "Anyone, even anonymous" as security setting
  5. 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:

  1. Make sure you have Platformio installed and open the project folder as a workspace
  2. 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:

  1. Go to settings and paste "https://dl.espressif.com/dl/package_esp32_index.json" in "Additional Boards manager URL"
  2. Go to Tools/Boards/Boards Manager. Search for ESP32 and install the board package.
  3. Select the board WEMOS LOLIN32 (or the board you have bought)
  4. 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.

Be the First to Share

    Recommendations

    • Rice & Grains Challenge

      Rice & Grains Challenge
    • CNC and 3D Printing Contest

      CNC and 3D Printing Contest
    • Puzzles Challenge

      Puzzles Challenge

    44 Comments

    0
    spillerp30
    spillerp30

    Question 3 months ago

    Hallo Kristian,
    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
    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.

    Was kann ich tun ?

    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


    1
    spillerp30
    spillerp30

    Answer 2 months 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

    0
    kristiantm
    kristiantm

    Answer 2 months 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

    0
    spillerp30
    spillerp30

    Question 8 months 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

    0
    spillerp30
    spillerp30

    Answer 8 months 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

    0
    spillerp30
    spillerp30

    Reply 8 months 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

    0
    kristiantm
    kristiantm

    Reply 8 months 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?

    0
    spillerp30
    spillerp30

    Reply 8 months 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

    0
    kristiantm
    kristiantm

    Answer 8 months 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

    0
    JanK7
    JanK7

    Question 9 months 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!

    0
    kristiantm
    kristiantm

    Answer 9 months 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 :)

    0
    kristiantm
    kristiantm

    Answer 9 months ago

    Hi Jan. Happy to hear that you like the project. Can take a quick look in the weekend. Kristian

    0
    Coffy032
    Coffy032

    12 months 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

    0
    kristiantm
    kristiantm

    Reply 12 months 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

    0
    Coffy032
    Coffy032

    Reply 12 months ago

    Hi Kristian

    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
    1
    Dolby dude
    Dolby dude

    1 year 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

    0
    kristiantm
    kristiantm

    Reply 1 year ago

    Hi Simon

    I have fixed the connection issue now (see new code on github) - switched from manual http redirection to using HTTPRequest (which has been fixed in the new 3.2.0 version of the ESP32 platform).

    Let me know if it also works in your end.

    Best regards,
    Kristian

    0
    JakobFP
    JakobFP

    Reply 1 year ago

    Thank you for this update. I abandoned the project because of this intermittent error, but now it is up and running again.

    0
    Dolby dude
    Dolby dude

    Reply 1 year ago

    Kristian, that seems to have now worked.
    I had an initial issue with the Weather Data collection where the JSON serialisation returned "No Memory" (8 or 9 out of 10 times run). I didn't investigate much further as I experimented putting the weather data collection function ahead of the google calendar part to see if there was still a memory issue with collecting the weather data - and the No memory issue went away just be reordering the code - not sure why but it no longer matters!

    I also had another tweak I needed to make - the code wasn't getting a battery readout, might have been a coincidence but the simple fact of obtaining the pin read level and printing it to serial seems to have made it work.

    Now working great. Now looking to see how long the battery lasts - I modified the code to update a couple of times in the day as well as first thing each morning, but hopefully it doesn't drain the battery too fast to be a nuisance.

    Thanks again for sharing a great project that made it really accessible for someone like me that hasn't done anything like this before - and only ever fairly basic coding at school a long time ago. My family is very impressed !.

    Simon
    ps - for others looking at these comments, I note that the WiFi access point default password maybe 12345678 for the initial setup of the WiFi router and other parameters.

    0
    kristiantm
    kristiantm

    Reply 1 year ago

    Hi simon

    Have been fighting a bit with the reliability of google script myself recently. Tried to do a loop with five retries, which sometimes work, but is not a super solution. It worked great a year ago :/

    Experiment and let me know if you find a good workaround. I will too :)

    Best regards,
    Kristian