Simple e-ink dashboard

Updated 31 January 2022

The aim of my project was to make a small display to show information such as the clock, weather, calendar, etc. I liked the idea of making a magic mirror, but wanted to try something more unique, less expensive, and easier to assemble (no woodworking required!)

Enter, my framed e-ink dashboard!

There were a few things I wanted to show on my display:

  • A greeting that changes with the time of day (e.g. "Good morning!")
  • The time and date
  • The weather
  • My Google calendar
  • My tasks from Remember the Milk

It was a bit ambitious to try and cram everything onto a small display, so I decided I'd rotate through different screens, switching every minute. The panel has a refresh time of less than 1 second so it's ideal for this. I ended up with five screens in total: a clock, weather, my calendar, tasks and some artwork.

Each screen was a HTML page rendered in a headless instance of Chromium. I used the Puppeteer API to capture a screenshot of each page, then ran ImageMagick to process and convert the screenshot to a bitmap. Finally, the bitmap was pushed to the display using the code provided by the display's manufacturer. Shell scripts were set up to automate the whole process, running on a cron schedule.

As Chromium was running locally on the Raspberry Pi, I didn't need to bother setting up a webserver.

The great thing about this approach is its simplicity and flexibility: anything that can be rendered in Chromium can be displayed on the panel.

I put the whole thing into an IKEA photo frame. Nice!

Step 1: Configure the Pi

First thing was to set up the software on the Pi. After flashing the SD card with Raspbian Buster Lite, setting up WiFi and SSH, and updating the system, I needed to...

    1. Select the correct timezone and enable the SPI interface (in sudo raspi-config)
    2. Install the software
      sudo apt install chromium-browser imagemagick git
    3. Install node.js. I used an unofficial build (the official version no longer supports the Pi Zero's ARMv6 architecture).
    4. Install puppeteer
      npm i puppeteer-core
    5. Compile and install the BCM2835 library
    6. Compile and install the Waveshare IT8951 binary.
      1. Clone the source:
        "git clone https://github.com/waveshare/IT8951.git"
      2. Comment out the following unneeded examples in IT8951/main.c like so:
        /*
        IT8951DisplayExample();
        IT8951DisplayExample2();
        printf("IT8951_GUI_Example ");
        IT8951_GUI_Example();
        */
      3. Compile with "make" command

Step 2: Configure the screens

Here are some examples of my JS code to give you the general gist of things. I used jquery, moment.js, ical.js and the FullCalendarJS libraries in my code.

Clock

For the clock, I opted for this stylish word clock by Snapstromegon. I used the Courier Prime font for that retro, typewriter look.

A demonstration of the Word Clock Module in action

Greeting, date and time

My code looked like this:

The greeting module in action

// Greeting

n =  new Date();

var greeting;

var time = n.getHours();

if (time < 5) {

        greeting = "Good night";

} else if (time < 12) {

        greeting = "Good morning";

} else if (time < 18) {

        greeting = "Good afternoon";

} else if (time < 22) {

        greeting = "Good evening";

} else {

        greeting = "Good night";

}

document.getElementById("greeting").innerHTML = greeting;


// Date and Time

date_suffix = function(dt){

        slist = [ null, "st", "nd", "rd", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th", "th", "st" ]

        return slist[dt.getDate()];

};

document.getElementById("date-time").innerHTML = moment(n).format("dddd, D") + "" + date_suffix(n) + " " + moment(n).format("MMMM h:mm a");

Weather

The weather page was powered by DarkSky (Unfortunately, they're no longer accepting any new registrations for their API.)

The weather icons were made by Freepik from flaticon.com. For simplicity, I named each SVG according to the values returned by data.icon

A demonstration of the weather module

To return the icons, and the highs and lows for the next five days, I used something like this:

n =  new Date();

// dayIndex is the day being forecasted (0 = today, 1= tomorrow, etc.)

function forecast(dayIndex) {

          var location = "xxxxxx"  // latitude, longitude

  var apiKey = "xxxxxxx"

  var timeKey = moment(n).add(dayIndex, 'days').format("YYYY-MM-DDTHH:mm:ss")

  $("#forecastDay" + dayindex).html(moment(n).add(dayIndex, 'days').format("dddd"));

  $.getJSON("https://api.darksky.net/forecast/" + apiKey + "/" + location + timeKey +  "?callback=?&units=auto", function(json) {

          $.each(json.daily.data, function(i, data) {

          $("#forecastIcon" + dayIndex).html("img src=res/" + data.icon + ".svg");

        $("#forecastMax" + dayIndex).html(Math.round(data.temperatureHigh) + "&deg");

                $("#forecastMin" + dayIndex).html(Math.round(data.temperatureLow) + "&deg");

                    }

                    )}

      )}

var j;

for (j = 0; j < 5; j++) {

 forecast(j);

}

Google Calendar

Rather than reinvent the wheel, I used the open source FullCalendarJS library to render a smart looking calendar. While it has a Google Calendar plugin, it only supports public calendars, which was not really what I was looking for.

I then came across Mozilla’s ical.js ICS parser, along with some code that maps parsed ICS data to FullCalendar events.

I just had to point my code to the private ICAL URLs of my Google Calendars. I modified the script to iterate through each calendar, adding them one-by-one.

The google calendar integration in action

var calendarEl = document.getElementById('calendar');

var calendar = new FullCalendar.Calendar(calendarEl, { /* FullCalendar options here */  });

var myCalendars = ['calendarURL1', 'calendarURL2', etc];  // Google Calendar ICAL feeds

var j;

for (j = 0; j < myCalendars.length; j++) {

           $.get(myCalendars[j]).then(function (data) {

             // map ics data to variable 'events'

             // see https://stackoverflow.com/a/47259181

             ...

           }

           calendar.addEventSource(events);

                }

calendar.render();

FullCalendar documents how to modify the style and layout. I chose a simple list view which works great on the small screen.

Remember the Milk

Remember the Milk has an API, but for simplicity, I just used the RSS/XML feeds generated for each list of tasks.

Remember the Milk task scheduling module in action

function rtm () {

  $.ajax({

        type: "GET",

        url: "https://www.rememberthemilk.com...",   // url of your task feed

        dataType: "xml",

        success: parseXml

  });   

function parseXml(xml) {

   if ( xml.getElementsByTagName("entry").length < 1 ) {

          $("#tasks").html("Hooray! All tasks completed!");

          }

   else {

          $("#tasks").html("
    "); $(xml).find("entry").each(function() { $("#list").append("
  • " + $(this).find("title").text() + "
  • "); }); $("#list").append("
");

               }        

            }

rtm();

I set up two automated smart lists in RTM to appear on my dashboard: one for overdue tasks, and one for tasks due today.

Step 3: Create the scripts

puppeteer.js (to capture the screenshot)

This is where the magic happens.

const puppeteer = require('puppeteer-core');

async function timeout(ms) {

  return new Promise(resolve => setTimeout(resolve, ms));

}

(async () => {

        // As the pages are hosted locally, we need to make a CORS request to get some of the data. Hence the need to run Chromium with the "--disable-web-security" flag

            const args = puppeteer.defaultArgs();

                            args.push('--disable-web-security');

                      const browser = await puppeteer.launch({ ignoreDefaultArgs: true, args, executablePath: '/usr/bin/chromium-browser'});

            const page = await browser.newPage();

            await page.goto('file:///home/pi/yourfile.html');

 

  // 5 second timeout: allows the page to fully render before taking the screenshot

            await timeout(5000);

 

  // browser viewport set to the dimensions of the display (800 x 600)

            await page.setViewport({ width: 800, height: 600 });  /

            await page.screenshot({path: '/home/pi/capture.png'});

            await browser.close();

})();

 

update.sh (to update the display)

# run puppeteer

/usr/local/bin/node /home/pi/puppeteer.js

# convert the screenshot to a BMP

convert -rotate 180 /home/pi/capture.png /home/pi/capture.bmp

# push the BMP to the display

sudo /home/pi/IT8951/IT8951 0 0 /home/pi/capture.bmp

In case you're wondering, I put the display in upside down (silly!), which is why I used -

rotate 180

Crontab

Finally, it was just a matter of creating a cron schedule to run the update scripts. I used separate puppeteer/update scripts for each screen.

Step 4: Assemble the hardware

With that done, it was time to put everything together.

  1. Disassemble the frame. I discarded the mat board that came with the IKEA frame, as I used the one I had custom made.
  2. Using a pair of pliers, I pulled out the tabs on the long edges of the photo frame. (The display wouldn't fit otherwise!)
    Sketch of design
  3. Next, I attached the mat board to the front of the display with some double sided tape.
    Sketch of design 2
  4. Then I put the plastic panel, display (with mat board attached), and mounting board back into the frame, and used the tabs on the short sides of the frame to hold everything in place. Make sure the e-ink panel's ribbon cable is accessible from behind the mounting board.
    Sketch of design 3
  5. Next step was to attach the e-ink driver HAT to the GPIO header on the Pi and connect all the ribbon cables to the display.
    The ribbon cable linking the pi to the eInk Display
  6. Final step was to attach the Raspberry Pi/HAT to the frame/mounting board using some double-sided foam tape.

The eInk Driver HAT in place

The end result

Overall, I'm pretty happy with how this project turned out, especially given my limited understanding of coding (it's amazing what you can google!)...

I had fun putting this together, so I hope you like it!

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.