Skip to Content

Temperature Plotter on e-Paper Display

Temperature Plotter on e-Paper Display

In this tutorial we will build a Temperature Plotter on a 4.2″ e-Paper display using an ESP32 and the BME280 sensor.

A common way to display temperature data is as a line plot in a cartesian coordinate system, where the time is on the x-axis and the temperature or other environmental data is on the y-axis. See the example figure below, where temperature (red line) ranges between 15° and 25° and the time between 1 and 12 o’clock.

Plotting temperature in cartesian coordinate system
Plotting temperature in cartesian coordinate system

Plotting temperature in cartesian coordinates has the advantage that changes in temperature are easy to see but ignores the fact that time is cyclic, e.g. the 24 hours in a day repeat. In this case a polar coordinate system is a often a better choice. See the example plot below that displays the temperature around a circle or ring with the clock hours marked.

Plotting temperature in polar coordinate system
Plotting temperature in polar coordinate system

Since the time axis follows the clock hours it makes it a bit easier to see what the temperature was at a certain time. But more importantly, the plot is continuous in that 12 o’clock and 11:59 are next to each other. We can plot several days without the need to scroll the plot, for instance.

In this tutorial, you will learn how to build such a Polar Plotter for temperature data. The Plotter will show current temperature, humidity and air pressure in addition to time and date. The screen shot below shows how the completed Plotter is going to look like.

Screen shot of Polar Plotter
Screen shot of Polar Plotter

Note that I highlighted the plotted temperature curve in red. Since we are going to use a gray-scale e-Paper display, there will be no color. Which brings us to the required parts.

Required Parts

I am using the ES32 lite as a microprocessor, since it is cheap and has a battery charging interface, which allows you to run the plotter on a LiPo battery. Any other ESP32 or ESP8266 with sufficient memory will work as well but preferably get one with a battery charging interface.

4.2″ e-Paper Display

BME280

BME280 Sensor

ESP32 lite Lolin32

ESP32 lite

USB data cable

USB Data Cable

Dupont wire set

Dupont Wire Set

Half_breadboard56a

Breadboard

Makerguides.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to products on Amazon.com. As an Amazon Associate we earn from qualifying purchases.

BME280 Sensor

We are going to us the BME280 sensor to measure the temperature data for our Temperature Plotter. Note, however, that the BME280 also allows you to measure humidity and air pressure, which we also could plot.

Another feature of the BME280 is that it also has a low power, sleep mode, where it consumes only 0.1µA (3.6 μA in normal mode). In combination with an e-Paper this makes for a low-power setup that can run on battery power.

The BME280 sensor itself is tiny and typically comes on a breakout board with an I2C interface; see the picture below. The I2C interface makes it very easy to connect and use.

BME280 breakout board
BME280 breakout board

The sensor can measure pressure from 300 hPa to 1100 hPa, temperature from -40°C to +85°C, and humidity from 0% to 100%. For more information on the BME280 and its applications have a look at the How To Use BME280 Pressure Sensor With Arduino and the Weather Station on e-Paper Display tutorials.

E-paper Display

The e-Paper display used in this project is a 4.2″ display module, with 400×300 pixels resolution, 4 grey levels, a partial refresh time of 0.4 seconds, and an embedded controller with an SPI interface.

Front and Back of 4.2″ e-Paper display module

Note that the module has a little jumper pad/switch at the back to switch from 4-wire SPI to 3-wire SPI. We are going to use the default 4-wire SPI here. So you should not have to change anything.

Back of display module with SPI-interface and controller

The display module runs on 3.3V or 5V, has a very low sleep current of 0.01µA and consumes only about 26.4mW while refreshing. For more information on e-Paper displays, in general, have a look at the following two tutorials: Interfacing Arduino To An E-ink Display and Partial Refresh of e-Paper Display.

Connecting and Testing the e-Paper

First, let us connect and test the function of the e-Paper. The following picture shows the complete wiring between ESP32 and display for power and the SPI interface.

Connecting 4.2" e-Paper to ESP32 via SPI
Connecting 4.2″ e-Paper to ESP32 via SPI

Below a table with all the connections for convenience. Note that you can power the display with 3.3V or 5V but the ESP32-lite has only a 3.3V output and the SPI data lines must be 3.3V!

e-Paper displayESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

Install GxEPD2 library

Before we can draw or write on the e-Paper display we need to install two libraries. The Adafruit_GFX graphics library, which provides a common set of graphics primitives (text, points, lines, circles, etc.). And the GxEPD2 library, which provides the graphics driver software for the E-Paper Display.

Just install these libraries the usual way. After the installation they should appear in the Library Manager as follows:

Adafruit_GFX and GxEPD2 libraries in Library Manager
Adafruit_GFX and GxEPD2 libraries in Library Manager

Install Adafruit_BME280 library

Since we are going to use the BME280 sensor to measure temperature, humidity and air pressure, we also need to install the Adafruit_BME280 library. Install it as usual and it should show up in the Library Manager as follows:

Adafruit_BME280 library in Library Manager
Adafruit_BME280 library in Library Manager

Testing the e-Paper Display

Once you have installed the libraries, I suggest you run the following test code to ensure that the display works.

#include "GxEPD2_BW.h"

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(90, 190);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

void loop() {}

The text code prints the text “Makerguides” and after some flickering of the display (full refresh) you should see it on your display as shown below:

Test output on 4.2" e-Paper display
Test output on 4.2″ e-Paper display

If not, then something is wrong. Most likely either the display is not correctly wired or the wrong display driver is chosen. The critical line of code is this one, where we specify the 4.2″ display driver:

GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

The Readme for GxEPD2 library lists all the supported displays and you can find the specifics in the header files, e.g. GxEPD2.h. Find the display driver specific for your display. This may take some trial and error.

Connecting and Testing the Temperature Sensor

Due to the I2C interface connecting the BME280 Sensor to the ESP32 is very simple. Just connect SCL of the Sensor to pin 25 and SDA to pin 33 of the ESP32. Then connect ground the GND pins and the 3.3V pin of the ESP32 to VIN of the BME280 Sensor. See the complete wiring diagram, with the e-Paper and the BME280 connections below:

Next let’s make sure the BME280 sensor works, while the e-Paper display is connected.

Testing the BME280 Sensor

The following test code uses the BME280 Sensor to measure temperature, air pressure and relative humidity and prints the measured values to the Serial Monitor.

#include "Adafruit_BME280.h"

Adafruit_BME280 bme;

void setup() {
  Serial.begin(115200);
  Wire.begin(33, 25);  
  bme.begin(0x76, &Wire);
}

void loop() {
  Serial.print("Temperature in degC = ");
  Serial.println(bme.readTemperature());

  Serial.print("Pressure in hPa     = ");
  Serial.println(bme.readPressure() / 100.0F);

  Serial.print("Humidity in %RH     = ");
  Serial.println(bme.readHumidity());

  Serial.println();
  delay(5000);
}

Note that we are using software I2C and have to specify the pins the SDA and SCL data line are connected to by calling Wire.begin(33, 25). The BME280 typically sits on I2C address 0x76. If you cannot read any data, maybe your BME280 has a different address and you have to change bme.begin(0x76, &Wire), accordingly.

If everything works fine, you should see data similar to the following one to be printed on the Serial Monitor. Make sure to set the baud rate to 115200.

Output of BME280 Sensor on Serial Monitor
Output of BME280 Sensor on Serial Monitor

With that in place, we now can write the code for the Temperature Plotter.

Code for a Temperature Plotter on e-Paper

In this section we are going to implement the code for the Plotter. The following screen shot of the plotter display shows you the elements and functionality it will have.

Elements of Polar Plotter Display
Elements of Polar Plotter Display

On the outer ring we will plot the temperature over time (red line). The outer ring also shows the hour labels and the temperature range. The little black dots along the outer ring (time arc) indicate the current time – in the picture above it is about 15 mins past 11 o’clock.

In the center of the ring we will print the current temperature (18.3°), the current relative humidity (47%) and air pressure (1005mb). Below that we also print the current time (Mo 11:14) and date (09/09/24).

Below you will find the complete code for the Temperature plotter. Have a quick look to get an overall idea, and then we will discuss its details.

#include "WiFi.h"
#include "esp_sntp.h"
#include "Adafruit_BME280.h"
#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const int H = GxEPD2_420_GDEY042T81::WIDTH;
const int W = GxEPD2_420_GDEY042T81::HEIGHT;
const int CW = W / 2;
const int CH = H / 2;
const int R = min(W, H) / 2;

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };

const float TMAX = 25.0;
const float TMID = 20.0;
const float TMIN = 15.0;

const int RMAX = R - 5;
const int RMID = R - 25;
const int RMIN = R - 45;

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Adafruit_BME280 bme;
GFXcanvas1 canvas(W, H);

void initDisplay() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);

  canvas.setTextColor(BLACK);
  canvas.setTextSize(1);
  canvas.setFont();
  canvas.fillScreen(WHITE);

  drawDottedCircle(CW, CH, RMAX, 2);
  drawDottedCircle(CW, CH, RMID, 8);
  drawDottedCircle(CW, CH, RMIN, 2);


  printfAt(CW, CH - (RMIN + 5), "%.0fc", TMIN);
  printfAt(CW, CH - (RMID + 5), "%.0fc", TMID);
  printfAt(CW, CH - (RMAX + 5), "%.0fc", TMAX);
}

void setTimezone() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
}

void initSensor() {
  Wire.begin(33, 25);  // sda, scl, Software I2C
  bme.begin(0x76, &Wire);
}

void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
  cx = int(x + r * sin(alpha));
  cy = int(y - r * cos(alpha));
}

void printAt(int16_t x, int16_t y, const char* text) {
  int16_t x1, y1;
  uint16_t w, h;
  canvas.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  canvas.setCursor(x - w / 2, y - h / 2);
  canvas.print(text);
}

void printfAt(int16_t x, int16_t y, const char* format, ...) {
  static char buff[64];
  va_list args;
  va_start(args, format);
  vsnprintf(buff, 64, format, args);
  printAt(x, y, buff);
}

void drawDottedCircle(float x, float y, float r, float s) {
  int n = int(TWO_PI / (s / r));
  for (int i = 0; i < n; i++) {
    float alpha = TWO_PI * i / n;
    int cx, cy;
    polar2cart(x, y, r, alpha, cx, cy);
    canvas.drawPixel(cx, cy, BLACK);
  }
}

int curSeconds() {
  static struct tm t;
  getLocalTime(&t);
  return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}

float sec2angle(int sec) {
  return TWO_PI * sec / (12 * 60 * 60);
}

void drawTimeArc() {
  int cx, cy;
  float secs = curSeconds();
  float alpha = sec2angle(secs);
  int n = round(6 * 12 * alpha / TWO_PI);
  for (int i = 0; i < n; i++) {
    float a = alpha * i / n;
    polar2cart(CW, CH, RMIN - 7, a, cx, cy);
    canvas.fillCircle(cx, cy, 2, BLACK);
  }
}

void drawClockLabels() {
  int cx, cy;
  canvas.setFont();
  for (int h = 1; h <= 12; h++) {
    float alpha = sec2angle(h * 60 * 60);
    polar2cart(CW, CH, RMIN - 20, alpha, cx, cy);
    printfAt(cx, cy, "%d", h);
  }
}

void plotTemp() {
  float temp = bme.readTemperature();
  temp = constrain(temp, TMIN, TMAX);
  float c = (RMAX - RMIN) / (TMAX - TMIN);
  float r = RMIN + (temp - TMIN) * c;

  int cx, cy;
  float alpha = sec2angle(curSeconds());
  polar2cart(CW, CH, r, alpha, cx, cy);
  canvas.drawPixel(cx, cy, BLACK);
}

void printBMEData() {
   float temp = bme.readTemperature();
  canvas.setFont(&FreeSansBold24pt7b);
  printfAt(CW, CH + 5, "%.1f", temp);

  float hum = bme.readHumidity();
  float pres = bme.readPressure() / 100;
  canvas.setFont();
  printfAt(CW, CH + 2, "%.0f%% %.0fmb", hum, pres);
}

void printTimeDate() {
  static struct tm t;
  getLocalTime(&t);
  canvas.setFont(&FreeSansBold9pt7b);
  printfAt(CW, CH + 35, "%s %2d:%02d",
                 DAYSTR[t.tm_wday], t.tm_hour, t.tm_min);
  printfAt(CW, CH + 55, "%02d/%02d/%02d",
                 t.tm_mday, t.tm_mon + 1, t.tm_year - 100);
}

void drawAll() {
  canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
  drawClockLabels();
  drawTimeArc();
  printBMEData();
  printTimeDate();
  plotTemp();
}

void drawCanvas() {
  epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}

void partialRefresh(const void* pv) {
  drawAll();
  epd.setPartialWindow(0, 0, W, H);
  drawCanvas();
}

void fullRefresh(const void* pv) {
  epd.setFullWindow();
  drawCanvas();
}

void setup() {
  initSensor();
  initDisplay();
  setTimezone();  
}

void loop() {
  static uint16_t iter = 0;

  if (iter % 50 == 0)
    syncTime();
  if (iter % 120 == 0)
    epd.drawPaged(fullRefresh, 0);
  iter = (iter + 1) % 1000;

  epd.drawPaged(partialRefresh, 0);
  epd.hibernate();

  delay(30 * 1000);
}

Libraries

We start by including the WiFi.h and the esp_snt.h libraries, which we will need to synchronize the plotter clock with an SNTP internet time server over Wi-Fi.

#include "WiFi.h"
#include "esp_sntp.h"

This allows us to report the temperature exactly at the time it was measured. Have a look at the How to synchronize ESP32 clock with SNTP server tutorial for more details.

Next we include the Adafruit_BME280 library needed to read temperature, humidity and pressure data from the BME280 sensor.

#include "Adafruit_BME280.h"

Finally, we include the GxEPD2_BW.h header file for the black and white (BW) e-Paper display. If you have a 3-color display you would include GxEPD2_3C.h, or GxEPD2_4C.h for a 4-color display, and GxEPD2_7C.h for a 7-color display, instead.

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "Fonts/FreeSansBold24pt7b.h"

We also include three font files, we use to display the plotter labels, temperature, date and other information. You can find an overview of the AdaFruit GFX fonts here.

Constants

Next we define some constants. Most importantly, you will have to define the SSID and PASSWORD for your WiFi, and the TIMEZONE you are living in.

const char* SSID = "SSID";
const char* PWD = "PASSWORD";
const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";

The above time zone specification “AEST-10AEDT,M10.1.0,M4.1.0/3” is for Australia, which corresponds to the Australian Eastern Standard Time (AEST) with daylight saving time adjustments.

The parts of this time zone definition are as follows

  • AEST: Australian Eastern Standard Time
  • -10: UTC offset of 10 hours ahead of Coordinated Universal Time (UTC)
  • AEDT: Australian Eastern Daylight Time
  • M10.1.0: Transition to daylight saving time occurs on the 1st Sunday of October
  • M4.1.0/3: Transition back to standard time occurs on the 1st Sunday of April, with a 3-hour difference from UTC.

To find your time zone definitions have a look at the Posix Timezones Database.

Then we have constants for the dimensions (W,H) of the e-Paper display, its center point (CW, CH) and the maximum radius R of a circle on the display. Note that width and hight are swapped, since we rotate the display (setRotation(1)) in the initDisplay function.

const int H = GxEPD2_420_GDEY042T81::WIDTH;
const int W = GxEPD2_420_GDEY042T81::HEIGHT;
const int CW = W / 2;
const int CH = H / 2;
const int R = min(W, H) / 2;

For convenience we also define two constants for the black and white color, and the day names:

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;
const char* DAYSTR[] = { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" };

Finally, we define the maximum, middle and minimum values for temperature (TMAX, TMID, TMIN) and the corresponding radiuses of the polar axis (RMAX, RMID, RMIN).

const float TMAX = 25.0;
const float TMID = 20.0;
const float TMIN = 15.0;

const int RMAX = R - 5;
const int RMID = R - 25;
const int RMIN = R - 45;

Objects

Next we create objects for the e-Paper display, the BME280 sensor and a canvas.

GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));
Adafruit_BME280 bme;
GFXcanvas1 canvas(W, H);

The epd object represents the display (epaper display). This object definition must match the e-Paper display type you have. Here we have 4.2 inch (= _420) e-Display. See the Readme for GxEPD2 library and the GxEPD2.h file for the supported displays.

The canvas is a buffer we can draw to in the background. It is needed for plotting the temperature curve and the partial refresh. For more details have a look at the Partial Refresh of e-Paper Display tutorial.

initDisplay function

The initDisplay() function initializes the e-Paper display, sets text color, size and font for the canvas and fill the canvas with white. Then it first draws the polar coordinate axes for the minimum, medium and maximum temperature, after which it prints the temperature labels on the axis.

void initDisplay() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);

  canvas.setTextColor(BLACK);  
  canvas.setTextSize(1);
  canvas.setFont();
  canvas.fillScreen(WHITE);

  drawDottedCircle(CW, CH, RMAX, 2);
  drawDottedCircle(CW, CH, RMID, 8);
  drawDottedCircle(CW, CH, RMIN, 2);

  printfAt(CW, CH - (RMIN + 5), "%.0fc", TMIN);
  printfAt(CW, CH - (RMID + 5), "%.0fc", TMID);
  printfAt(CW, CH - (RMAX + 5), "%.0fc", TMAX);
}

The following screen shot shows how this looks like after initDisplay() is called:

Polar axes with labels on e-Paper display
Polar axes with labels on e-Paper display

setTimezone function

The setTimezone() function sets the TIMEZONE for the internal clock of the ESP32. As mentioned before, you can find other time zone definitions in the Posix Timezones Database.

void setTimezone() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

syncTime function

The syncTime function creates a WiFi connection and then synchronizes the internal clock of the ESP32 with the SNTP server “pool.ntp.org” by calling configTzTime().

You can specify other, or even multiple SNTP servers to connect to. Have a look at the How to synchronize ESP32 clock with SNTP server tutorial for more information.

void syncTime() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED)
    ;
  configTzTime(TIMEZONE, "pool.ntp.org");
}

initSensor function

The initSensor() function initializes the BME280 sensor. Make sure SDA and SCL are connected to pin 33 and 25 of the ESP32, as specified. Also make sure the I2C address 0x76 is correct for your sensor.

void initSensor() {
  Wire.begin(33, 25);  // SDA, SCL
  bme.begin(0x76, &Wire);
}

polar2cart function

The polar2cart() function converts polar coordinates given by center point x, y, radius r and angle alpha to cartesian coordinates that are returned in cx and cy.

void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
  cx = int(x + r * sin(alpha));
  cy = int(y - r * cos(alpha));
}

The following image illustrates how the polar2cart() function converts polar coordinates to cartesian coordinates.

Relationship between Polar and Cartesian Coordinates
Relationship between Polar and Cartesian Coordinates

We need this function, for instance, to convert the clock hours on a circle to the cartesian x, y coordinates the e-Paper display is using.

printAt and printfAt functions

The printAt() and printfAt() functions print text at the coordinates x, y on the e-Paper display.

void printAt(int16_t x, int16_t y, const char* text) {
  int16_t x1, y1;
  uint16_t w, h;
  canvas.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  canvas.setCursor(x - w / 2, y - h / 2);
  canvas.print(text);
}

void printfAt(int16_t x, int16_t y, const char* format, ...) {
  static char buff[64];
  va_list args;
  va_start(args, format);
  vsnprintf(buff, 64, format, args);
  printAt(x, y, buff);
}

The printAt() function allows to print plain text only, while the printfAt() works like printf and allows you to provide a format specifier to print text and data, e.g. printfAt(x, y, "%02d-%02d-%02d", m, h, y);

drawDottedCircle function

The drawDottedCircle() function simply draws a dotted circle with center point x, y, radius r and space s between dots. The function is used to draw the polar axes the temperature is plotted along.

void drawDottedCircle(float x, float y, float r, float s) {
  int n = int(TWO_PI / (s / r));
  for (int i = 0; i < n; i++) {
    float alpha = TWO_PI * i / n;
    int cx, cy;
    polar2cart(x, y, r, alpha, cx, cy);
    canvas.drawPixel(cx, cy, BLACK);
  }
}

curSeconds function

The curSeconds() function gets the current time and converts it into seconds. For instance, 8:50:10 would be converted to 8 * 60 * 60 +50 * 60 +10 = 31810 seconds.

int curSeconds() {
  static struct tm t;
  getLocalTime(&t);
  return (t.tm_hour % 12) * 60 * 60 + t.tm_min * 60 + t.tm_sec;
}

sec2angle function

The sec2angle() function converts time provided in seconds into an angle on the clock.

float sec2angle(int sec) {
  return TWO_PI * sec / (12 * 60 * 60);
}

drawTimeArc function

The drawTimeArc() function draws the black dots that indicate the current time on the ring (time arc). As you can see it uses the curSeconds() and the sec2angle() functions to convert the current time to seconds and an angle that determines the end of the time arc.

void drawTimeArc() {
  int cx, cy;
  float secs = curSeconds();
  float alpha = sec2angle(secs);
  int n = round(6 * 12 * alpha / TWO_PI);
  for (int i = 0; i < n; i++) {
    float a = alpha * i / n;
    polar2cart(CW, CH, RMIN - 7, a, cx, cy);
    canvas.fillCircle(cx, cy, 2, BLACK);
  }
}

The following picture shows the end of the time arc:

Time arc on e-Paper display
Time arc on e-Paper display

drawClockLabels function

The drawClockLabels() function simply draws the clock hour labels on the ring.

void drawClockLabels() {
  int cx, cy;
  canvas.setFont();
  for (int h = 1; h <= 12; h++) {
    float alpha = sec2angle(h * 60 * 60);
    polar2cart(CW, CH, RMIN - 20, alpha, cx, cy);
    printfAt(cx, cy, "%d", h);
  }
}

The following picture of a section of the display shows the clock hour labels.

Clock hour labels on e-Paper display
Clock hour labels on e-Paper display

plotTemp function

The plotTemp() function plots the temperature over time in polar coordinates. It first reads the current temperature, constrains it to the range TMIN to TMAX, and then scales it to a radius r. Next the current time is converted to an angle alpha, and the temperature is then plotted as a single pixel at polar coordinates alpha, r.

void plotTemp() {
  float temp = bme.readTemperature();
  temp = constrain(temp, TMIN, TMAX);
  float c = (RMAX - RMIN) / (TMAX - TMIN);
  float r = RMIN + (temp - TMIN) * c;

  int cx, cy;
  float alpha = sec2angle(curSeconds());
  polar2cart(CW, CH, r, alpha, cx, cy);
  canvas.drawPixel(cx, cy, BLACK);
}

Note that all drawing occurs on the canvas and is therefore not immediately visible. The following image shows how the output of the function looks like when display.

Polar Plot of temperature data
Polar Plot of temperature data

Note that the temperature plot is again highlighted in red but in reality appears as a black line.

printBMEData function

The printBMEData() function prints the current temperature, relative humidity and air pressure in the center of the plot.

void printBMEData() {
  float temp = bme.readTemperature();
  canvas.setFont(&FreeSansBold24pt7b);
  printfAt(CW, CH + 5, "%.1f", temp);

  float hum = bme.readHumidity();
  float pres = bme.readPressure() / 100;
  canvas.setFont();
  printfAt(CW, CH + 2, "%.0f%% %.0fmb", hum, pres);
}

Here an example picture of the output:

BME280 data on e-Paper display

printTimeDate function

The printTimeDate() function prints the current day name, time and date in the center of the plot.

void printTimeDate() {
  static struct tm t;
  getLocalTime(&t);
  canvas.setFont(&FreeSansBold9pt7b);
  printfAt(CW, CH + 35, "%s %2d:%02d",
                 DAYSTR[t.tm_wday], t.tm_hour, t.tm_min);
  printfAt(CW, CH + 55, "%02d/%02d/%02d",
                 t.tm_mday, t.tm_mon + 1, t.tm_year - 100);
}

The output looks as follow:

Time/Date on e-Paper display

drawAll function

The drawAll() function calls all the functions needed to draw the complete plotter display. This includes the clock labels, the time arc, the sensor data, the time and the plotting of the temperature.

void drawAll() {
  canvas.fillCircle(CW, CH, RMIN - 1, WHITE);
  drawClockLabels();
  drawTimeArc();
  printBMEData();
  printTimeDate();
  plotTemp();
}

It produces the following output:

Complete Plotter on e-Paper display

drawCanvas function

The drawCanvas() function interprets the canvas as a bitmap and displays it on the e-Paper. This is when whatever is drawn on the canvas actually becomes visible on the display.

void drawCanvas() {
  epd.drawBitmap(0, 0, canvas.getBuffer(), W, H, WHITE, BLACK);
}

partialRefresh function

The partialRefresh() function calls drawAll() to redraw everything on the canvas and then performs a partial refresh of the e-Paper. This is much faster (<0.5sec) than a full refresh but leaves after images.

oid partialRefresh(const void* pv) {
  drawAll();
  epd.setPartialWindow(0, 0, W, H);
  drawCanvas();
}

For more information have a look at the Partial Refresh of e-Paper Display tutorial.

fullRefresh function

The fullRefresh() function performs a full refresh, which is much slower than the partial refresh and causes the e-Paper display to flicker but it removes all after-images.

void fullRefresh(const void* pv) {
  epd.setFullWindow();
  drawCanvas();
}

setup function

The setup() function initializes the BME280 sensor and the display, and sets the time zone.

void setup() {
  initSensor();
  initDisplay();
  setTimezone();  
}

loop function

The loop() function essentially causes an update of the display every 30 seconds. Depending on the number of updates (iter), either a partial or a full refresh is performed. Specifically, a full refresh is performed every 120th iteration, which comes to 120*30/60 = 60 minutes.

Similarly, the internal clock of the ESP32 is synchronized with the SNTP server about every 50*30/60 = 25 minutes.

void loop() {
  static uint16_t iter = 0;

  if (iter % 50 == 0)
    syncTime();
  if (iter % 120 == 0)
    epd.drawPaged(fullRefresh, 0);
  iter = (iter + 1) % 1000;

  epd.drawPaged(partialRefresh, 0);
  epd.hibernate();

  delay(30 * 1000);
}

And that completes the description of the code.

Conclusions

In this tutorial you learned how build a Polar Plotter that displays temperature (and other data) on a 4.2″ e-Paper display using an ESP32 and an BME280 sensor.

There are many ways in which you could modify or extend this Plotter. Firstly, the temperature resolution of current implementation is pretty low, since the ring the temperature is plotted in, is fairly thin. You could reduce the text in the center and make the ring wider for a better resolution.

In addition to the temperature, it would be nice to also monitor air pressure and humidity over time. This could be achieved by having three canvases (for temperature, pressure and humidity), and switch between them, either periodically or by adding a button to the circuit.

You may even monitor and plot Wi-Fi speed over time, since the ESP32 is connected to the internet anyway. Generally, any parameter you want to measure over a day (e.g. light intensity, air quality, dust, …) or other periodic time interval is a good choice for a Polar Plotter. The corners of the display also would make for a good space to display weather forecast data.

Finally a word about deep-sleep. It would be nice to put the ESP32 into deep-sleep between display updates but this is not straightforward, since all memory including the drawing on the canvas is lost in deep sleep and the canvas is too big for RTC memory. Instead, we would have to store the temperature time series in a small array that fits into RTC memory. Not impossible but a bit more limiting.

Many ideas to play with. Happy Tinkering : )