Skip to Content

Temperatur-Plotter auf e-Paper-Display

Temperatur-Plotter auf e-Paper-Display

In diesem Tutorial bauen wir einen Temperatur-Plotter auf einem 4,2″ e-Paper-Display mit einem ESP32 und dem BME280-Sensor.

Eine gängige Methode, Temperaturdaten darzustellen, ist als Liniendiagramm in einem cartesian coordinate system, wobei die Zeit auf der x-Achse und die Temperatur oder andere Umweltdaten auf der y-Achse liegen. Siehe das Beispielbild unten, wo die Temperatur (rote Linie) zwischen 15° und 25° und die Zeit zwischen 1 und 12 Uhr liegt.

Plotting temperature in cartesian coordinate system
Temperatur im kartesischen Koordinatensystem plotten

Das Plotten der Temperatur in kartesischen Koordinaten hat den Vorteil, dass Temperaturänderungen leicht zu erkennen sind, ignoriert aber die Tatsache, dass Zeit zyklisch ist, z.B. die 24 Stunden eines Tages sich wiederholen. In diesem Fall ist ein polar coordinate system oft die bessere Wahl. Siehe das Beispiel unten, das die Temperatur um einen Kreis oder Ring mit den markierten Uhrzeiten anzeigt.

Plotting temperature in polar coordinate system
Temperatur im Polarkoordinatensystem plotten

Da die Zeitachse den Uhrzeiten folgt, ist es etwas einfacher zu erkennen, wie die Temperatur zu einer bestimmten Zeit war. Noch wichtiger ist, dass der Plot kontinuierlich ist, sodass 12 Uhr und 11:59 nebeneinander liegen. Wir können mehrere Tage plotten, ohne scrollen zu müssen.

In diesem Tutorial lernst du, wie man einen solchen Polar-Plotter für Temperaturdaten baut. Der Plotter zeigt aktuelle Temperatur, Luftfeuchtigkeit und Luftdruck zusätzlich zu Zeit und Datum an. Der Screenshot unten zeigt, wie der fertige Plotter aussehen wird.

Screen shot of Polar Plotter
Screenshot des Polar-Plotters

Beachte, dass ich die geplottete Temperaturkurve rot hervorgehoben habe. Da wir ein Graustufen-e-Paper-Display verwenden, gibt es keine Farbe. Damit kommen wir zu den benötigten Teilen.

Benötigte Teile

Ich verwende den ESP32 lite als Mikroprozessor, da er günstig ist und eine Batterieschnittstelle hat, die es erlaubt, den Plotter mit einer LiPo-Batterie zu betreiben. Jeder andere ESP32 oder ESP8266 mit ausreichend Speicher funktioniert ebenfalls, aber am besten einen mit Batterieschnittstelle wählen.

4,2″ e-Paper Display

BME280

BME280 Sensor

ESP32 lite Lolin32

ESP32 lite

USB data cable

USB-Datenkabel

Dupont wire set

Dupont-Kabelsatz

Half_breadboard56a

Breadboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

BME280 Sensor

Wir verwenden den BME280 sensor, um die Temperaturdaten für unseren Temperatur-Plotter zu messen. Beachte jedoch, dass der BME280 auch Luftfeuchtigkeit und Luftdruck messen kann, die wir ebenfalls plotten könnten.

Ein weiteres Merkmal des BME280 ist der Energiesparmodus, in dem er nur 0,1µA verbraucht (3,6 µA im Normalmodus). In Kombination mit einem e-Paper ergibt das eine energiesparende Lösung, die mit Batteriestrom läuft.

Der BME280-Sensor selbst ist winzig und wird typischerweise auf einer Breakout-Platine mit I2C-Schnittstelle geliefert; siehe Bild unten. Die I2C-Schnittstelle macht das Anschließen und Verwenden sehr einfach.

BME280 breakout board
BME280 Breakout-Platine

Der Sensor misst Druck von 300 hPa bis 1100 hPa, Temperatur von -40°C bis +85°C und Luftfeuchtigkeit von 0% bis 100%. Für mehr Informationen zum BME280 und seinen Anwendungen siehe die How To Use BME280 Pressure Sensor With Arduino und die Weather Station on e-Paper Display Tutorials.

4,2″ E-Paper Display

Das in diesem Projekt verwendete e-Paper-Display ist ein 4,2″ Modul mit 400×300 Pixeln Auflösung, 4 Graustufen, einer partiellen Aktualisierungszeit von 0,4 Sekunden und einem eingebetteten Controller mit SPI-Schnittstelle.

Vorder- und Rückseite des 4,2″ e-Paper-Display-Moduls

Beachte, dass das Modul auf der Rückseite einen kleinen Jumper/Schalter hat, um von 4-Draht-SPI auf 3-Draht-SPI umzuschalten. Wir verwenden hier den Standard 4-Draht-SPI, also musst du nichts ändern.

Rückseite des Display-Moduls mit SPI-Schnittstelle und Controller

Das Display-Modul läuft mit 3,3V oder 5V, hat einen sehr niedrigen Schlafstrom von 0,01µA und verbraucht beim Aktualisieren nur etwa 26,4mW. Für mehr Informationen zu e-Paper-Displays allgemein siehe die folgenden zwei Tutorials: Interfacing Arduino To An E-ink Display und Partial Refresh of e-Paper Display.

Anschließen und Testen des e-Paper

Zuerst verbinden und testen wir die Funktion des e-Paper. Das folgende Bild zeigt die komplette Verkabelung zwischen ESP32 und Display für Stromversorgung und SPI-Schnittstelle.

Connecting 4.2" e-Paper to ESP32 via SPI
Anschluss des 4,2″ e-Paper an ESP32 via SPI

Unten eine Tabelle mit allen Verbindungen zur Übersicht. Beachte, dass du das Display mit 3,3V oder 5V versorgen kannst, aber der ESP32-lite hat nur einen 3,3V-Ausgang und die SPI-Datenleitungen müssen 3,3V sein!

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

GxEPD2 Bibliothek installieren

Bevor wir auf dem e-Paper zeichnen oder schreiben können, müssen wir zwei Bibliotheken installieren. Die Adafruit_GFX Grafikbibliothek, die eine gemeinsame Menge an Grafikgrundelementen (Text, Punkte, Linien, Kreise usw.) bereitstellt. Und die GxEPD2 Bibliothek, die die Grafiktreiber-Software für das E-Paper-Display liefert.

Einfach die Bibliotheken wie gewohnt installieren. Nach der Installation sollten sie im Library Manager wie folgt erscheinen:

Adafruit_GFX and GxEPD2 libraries in Library Manager
Adafruit_GFX und GxEPD2 Bibliotheken im Library Manager

Adafruit_BME280 Bibliothek installieren

Da wir den BME280 Sensor verwenden, um Temperatur, Luftfeuchtigkeit und Luftdruck zu messen, müssen wir auch die Adafruit_BME280 Bibliothek installieren. Installiere sie wie gewohnt, sie sollte dann im Library Manager wie folgt erscheinen:

Adafruit_BME280 library in Library Manager
Adafruit_BME280 Bibliothek im Library Manager

Testen des e-Paper Displays

Nachdem du die Bibliotheken installiert hast, empfehle ich, den folgenden Testcode auszuführen, um sicherzustellen, dass das Display funktioniert.

#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() {}

Der Textcode gibt den Text „Makerguides“ aus, und nach einem kurzen Flackern des Displays (Vollaktualisierung) solltest du ihn auf deinem Display wie unten gezeigt sehen:

Test output on 4.2" e-Paper display
Testausgabe auf 4,2″ e-Paper Display

Wenn nicht, stimmt etwas nicht. Wahrscheinlich ist das Display nicht korrekt verkabelt oder der falsche Display-Treiber gewählt. Die kritische Codezeile ist diese, in der wir den 4,2″ Display-Treiber angeben:

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

Die Readme GxEPD2 Bibliothek listet alle unterstützten Displays auf, die Details findest du in den Header-Dateien, z.B. GxEPD2.h. Finde den Display-Treiber, der zu deinem Display passt. Das kann etwas Ausprobieren erfordern.

Anschließen und Testen des Temperatursensors

Dank der I2C-Schnittstelle ist das Anschließen des BME280 Sensors an den ESP32 sehr einfach. Verbinde SCL des Sensors mit Pin 25 und SDA mit Pin 33 des ESP32. Dann verbinde die GND-Pins und den 3,3V-Pin des ESP32 mit VIN des BME280 Sensors. Siehe das vollständige Schaltbild mit e-Paper und BME280 Verbindungen unten:

Connecting the BME280 to ESP32
Anschluss des BME280 an ESP32

Als nächstes prüfen wir, ob der BME280 Sensor funktioniert, während das e-Paper Display angeschlossen ist.

Testen des BME280 Sensors

Der folgende Testcode nutzt den BME280 Sensor, um Temperatur, Luftdruck und relative Luftfeuchtigkeit zu messen und gibt die Werte im Serial Monitor aus.

#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);
}

Beachte, dass wir Software-I2C verwenden und die Pins, an die SDA und SCL angeschlossen sind, mit Wire.begin(33, 25) angeben müssen. Der BME280 sitzt typischerweise auf der I2C-Adresse 0x76. Wenn du keine Daten lesen kannst, hat dein BME280 vielleicht eine andere Adresse und du musst bme.begin(0x76, &Wire) entsprechend anpassen.

Wenn alles funktioniert, solltest du ähnliche Daten wie die folgenden im Serial Monitor sehen. Stelle sicher, dass die Baudrate auf 115200 eingestellt ist.

Output of BME280 Sensor on Serial Monitor
Ausgabe des BME280 Sensors im Serial Monitor

Damit können wir jetzt den Code für den Temperatur-Plotter schreiben.

Code für einen Temperatur-Plotter auf e-Paper

In diesem Abschnitt implementieren wir den Code für den Plotter. Der folgende Screenshot des Plotter-Displays zeigt dir die Elemente und Funktionen, die er haben wird.

Elements of Polar Plotter Display
Elemente des Polar-Plotter-Displays

Im äußeren Ring plotten wir die Temperatur über die Zeit (rote Linie). Der äußere Ring zeigt auch die Stundenbeschriftungen und den Temperaturbereich. Die kleinen schwarzen Punkte entlang des äußeren Rings (Zeitbogen) zeigen die aktuelle Zeit an – im Bild oben ist es etwa 15 Minuten nach 11 Uhr.

In der Mitte des Rings geben wir die aktuelle Temperatur (18,3°), die aktuelle relative Luftfeuchtigkeit (47%) und den Luftdruck (1005mb) aus. Darunter zeigen wir die aktuelle Zeit (Mo 11:14) und das Datum (09/09/24).

Unten findest du den kompletten Code für den Temperatur-Plotter. Schau ihn dir kurz an, um einen Überblick zu bekommen, dann besprechen wir die 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);
}

Bibliotheken

Wir beginnen mit dem Einbinden der WiFi.h und der esp_snt.h Bibliotheken, die wir brauchen, um die Uhr des Plotters über Wi-Fi mit einem SNTP-Internetzeitserver zu synchronisieren.

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

So können wir die Temperatur genau zum Messzeitpunkt anzeigen. Siehe das How to synchronize ESP32 clock with SNTP server Tutorial für mehr Details.

Als nächstes binden wir die Adafruit_BME280 library ein, die benötigt wird, um Temperatur, Luftfeuchtigkeit und Druckdaten vom BME280 Sensor zu lesen.

#include "Adafruit_BME280.h"

Schließlich binden wir die GxEPD2_BW.h Header-Datei für das schwarz-weiße (BW) e-Paper Display ein. Wenn du ein 3-Farben-Display hast, würdest du GxEPD2_3C.h, oder GxEPD2_4C.h für ein 4-Farben-Display und GxEPD2_7C.h für ein 7-Farben-Display einbinden.

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

Wir binden auch drei Schriftdateien ein, die wir für die Anzeige der Plotter-Beschriftungen, Temperatur, Datum und andere Informationen verwenden. Du findest ein die AdaFruit GFX fonts hier.

Konstanten

Als nächstes definieren wir einige Konstanten. Am wichtigsten sind die SSID und PASSWORD für dein WiFi sowie die TIMEZONE, in der du lebst.

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

Die obige Zeitzonenspezifikation „AEST-10AEDT,M10.1.0,M4.1.0/3“ gilt für Australien und entspricht der Australian Eastern Standard Time (AEST) mit Sommerzeit-Anpassungen.

Die Teile dieser Zeitzonendefinition sind wie folgt

  • AEST: Australian Eastern Standard Time
  • -10: UTC-Versatz von 10 Stunden vor der koordinierten Weltzeit (UTC)
  • AEDT: Australian Eastern Daylight Time
  • M10.1.0: Übergang zur Sommerzeit am ersten Sonntag im Oktober
  • M4.1.0/3: Rückkehr zur Normalzeit am ersten Sonntag im April, mit 3 Stunden Unterschied zur UTC.

Um deine Zeitzonendefinitionen zu finden, schau dir das Posix Timezones Database an.

Dann definieren wir Konstanten für die Abmessungen (W,H) des e-Paper Displays, seinen Mittelpunkt (CW, CH) und den maximalen Radius R eines Kreises auf dem Display. Beachte, dass Breite und Höhe vertauscht sind, da wir das Display (setRotation(1)) in der initDisplay Funktion drehen.

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;

Zur Vereinfachung definieren wir auch zwei Konstanten für Schwarz und Weiß sowie die Wochentagsnamen:

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

Schließlich definieren wir die maximalen, mittleren und minimalen Werte für die Temperatur (TMAX, TMID, TMIN) und die entsprechenden Radien der Polarachse (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;

Objekte

Als nächstes erstellen wir Objekte für das e-Paper Display, den BME280 Sensor und eine Zeichenfläche (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);

Das epd Objekt repräsentiert das Display (epaper display). Diese Objektdefinition muss zum Typ deines e-Paper Displays passen. Hier haben wir ein 4,2 Zoll (= _420) e-Display. Siehe die Readme GxEPD2 Bibliothek und die GxEPD2.h Datei für unterstützte Displays.

Das Canvas ist ein Puffer, auf den wir im Hintergrund zeichnen können. Es wird für das Plotten der Temperaturkurve und die partielle Aktualisierung benötigt. Für mehr Details siehe das Partial Refresh of e-Paper Display Tutorial.

initDisplay Funktion

Die initDisplay() Funktion initialisiert das e-Paper Display, setzt Textfarbe, Größe und Schriftart für das Canvas und füllt das Canvas mit Weiß. Dann zeichnet sie zuerst die Polarkoordinatenachsen für minimale, mittlere und maximale Temperatur und druckt anschließend die Temperaturbeschriftungen auf die Achse.

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);
}

Der folgende Screenshot zeigt, wie das aussieht, nachdem initDisplay() aufgerufen wurde:

Polar axes with labels on e-Paper display
Polarkoordinatenachsen mit Beschriftungen auf e-Paper Display

setTimezone Funktion

Die setTimezone() Funktion setzt die ZEITZONE für die interne Uhr des ESP32. Wie bereits erwähnt, findest du weitere Zeitzonendefinitionen im Posix Timezones Database.

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

syncTime Funktion

Die syncTime Funktion stellt eine WiFi-Verbindung her und synchronisiert dann die interne Uhr des ESP32 mit dem SNTP-Server „pool.ntp.org“ durch Aufruf von configTzTime().

Du kannst auch andere oder mehrere SNTP-Server angeben. Siehe das How to synchronize ESP32 clock with SNTP server Tutorial für mehr Informationen.

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

initSensor Funktion

Die initSensor() Funktion initialisiert den BME280 Sensor. Achte darauf, dass SDA und SCL wie angegeben an Pin 33 und 25 des ESP32 angeschlossen sind. Stelle auch sicher, dass die I2C-Adresse 0x76 für deinen Sensor korrekt ist.

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

polar2cart Funktion

Die polar2cart() Funktion wandelt Polarkoordinaten mit Mittelpunkt x, y, Radius r und Winkel alpha in kartesische Koordinaten um, die in cx und cy zurückgegeben werden.

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));
}

Das folgende Bild zeigt, wie die polar2cart() Funktion Polarkoordinaten in kartesische Koordinaten umwandelt.

Relationship between Polar and Cartesian Coordinates
Beziehung zwischen Polarkoordinaten und kartesischen Koordinaten

Wir brauchen diese Funktion z.B., um die Uhrzeiten auf einem Kreis in die kartesischen x,y-Koordinaten umzuwandeln, die das e-Paper Display verwendet.

printAt und printfAt Funktionen

Die printAt() und printfAt() Funktionen geben Text an den Koordinaten x, y auf dem e-Paper Display aus.

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);
}

Die printAt() Funktion erlaubt nur das Drucken von einfachem Text, während die printfAt() Funktion wie printf funktioniert und dir erlaubt, Formatangaben zu verwenden, um Text und Daten auszugeben, z.B. printfAt(x, y, "%02d-%02d-%02d", m, h, y);

drawDottedCircle Funktion

Die drawDottedCircle() Funktion zeichnet einfach einen gepunkteten Kreis mit Mittelpunkt x, y, Radius r und Abstand s zwischen den Punkten. Die Funktion wird verwendet, um die Polarachsen zu zeichnen, entlang derer die Temperatur geplottet wird.

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 Funktion

Die curSeconds() Funktion ermittelt die aktuelle Zeit und wandelt sie in Sekunden um. Zum Beispiel wird 8:50:10 in 8 * 60 * 60 +50 * 60 +10 = 31810 Sekunden umgerechnet.

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

sec2angle Funktion

Die sec2angle() Funktion wandelt die Zeit in Sekunden in einen Winkel auf der Uhr um.

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

drawTimeArc Funktion

Die drawTimeArc() Funktion zeichnet die schwarzen Punkte, die die aktuelle Zeit auf dem Ring (Zeitbogen) anzeigen. Wie du siehst, verwendet sie die curSeconds() und die sec2angle() Funktionen, um die aktuelle Zeit in Sekunden und einen Winkel umzuwandeln, der das Ende des Zeitbogens bestimmt.

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);
  }
}

Das folgende Bild zeigt das Ende des Zeitbogens:

Time arc on e-Paper display
Zeitbogen auf e-Paper Display

drawClockLabels Funktion

Die drawClockLabels() Funktion zeichnet einfach die Stundenbeschriftungen auf dem 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);
  }
}

Das folgende Bild eines Abschnitts des Displays zeigt die Stundenbeschriftungen.

Clock hour labels on e-Paper display
Stundenbeschriftungen auf e-Paper Display

plotTemp Funktion

Die plotTemp() Funktion plottet die Temperatur über die Zeit in Polarkoordinaten. Sie liest zuerst die aktuelle Temperatur, begrenzt sie auf den Bereich TMIN bis TMAX und skaliert sie dann auf einen Radius r. Danach wird die aktuelle Zeit in einen Winkel alpha umgerechnet, und die Temperatur wird als einzelnes Pixel an den Polarkoordinaten alpha, r geplottet.

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);
}

Beachte, dass alle Zeichnungen auf dem Canvas erfolgen und daher nicht sofort sichtbar sind. Das folgende Bild zeigt, wie die Ausgabe der Funktion beim Anzeigen aussieht.

Polar Plot of temperature data
Polar-Plot der Temperaturdaten

Beachte, dass die Temperaturkurve wieder rot hervorgehoben ist, in Wirklichkeit aber als schwarze Linie erscheint.

printBMEData Funktion

Die printBMEData() Funktion gibt die aktuelle Temperatur, relative Luftfeuchtigkeit und den Luftdruck in der Mitte des Plots aus.

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);
}

Hier ein Beispielbild der Ausgabe:

BME280 Daten auf e-Paper Display

printTimeDate Funktion

Die printTimeDate() Funktion gibt den aktuellen Wochentag, die Zeit und das Datum in der Mitte des Plots aus.

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);
}

Die Ausgabe sieht wie folgt aus:

Zeit/Datum auf e-Paper Display

drawAll Funktion

Die drawAll() Funktion ruft alle Funktionen auf, die benötigt werden, um das komplette Plotter-Display zu zeichnen. Dazu gehören die Stundenbeschriftungen, der Zeitbogen, die Sensordaten, die Zeit und das Plotten der Temperatur.

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

Sie erzeugt folgende Ausgabe:

Kompletter Plotter auf e-Paper Display

drawCanvas Funktion

Die drawCanvas() Funktion interpretiert das Canvas als Bitmap und zeigt es auf dem e-Paper an. Erst hier wird alles, was auf dem Canvas gezeichnet wurde, tatsächlich auf dem Display sichtbar.

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

partialRefresh Funktion

Die partialRefresh() Funktion ruft drawAll() auf, um alles auf dem Canvas neu zu zeichnen, und führt dann eine partielle Aktualisierung des e-Paper durch. Das ist viel schneller (<0,5 Sek.) als eine Vollaktualisierung, hinterlässt aber Nachbilder.

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

Für mehr Informationen siehe das Partial Refresh of e-Paper Display Tutorial.

fullRefresh Funktion

Die fullRefresh() Funktion führt eine Vollaktualisierung durch, die viel langsamer als die partielle ist und das e-Paper Display flackern lässt, aber alle Nachbilder entfernt.

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

setup Funktion

Die setup() Funktion initialisiert den BME280 Sensor und das Display und setzt die Zeitzone.

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

loop Funktion

Die loop() Funktion sorgt im Wesentlichen für ein Update des Displays alle 30 Sekunden. Je nach Anzahl der Updates (iter) wird entweder eine partielle oder eine Vollaktualisierung durchgeführt. Konkret erfolgt alle 120 Iterationen eine Vollaktualisierung, was 120*30/60 = 60 Minuten entspricht.

Ebenso wird die interne Uhr des ESP32 etwa alle 50*30/60 = 25 Minuten mit dem SNTP-Server synchronisiert.

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);
}

Damit ist die Beschreibung des Codes abgeschlossen.

Fazit

In diesem Tutorial hast du gelernt, wie man einen Polar-Plotter baut, der Temperatur (und andere Daten) auf einem 4,2″ e-Paper Display mit einem ESP32 und einem BME280 Sensor anzeigt.

Es gibt viele Möglichkeiten, diesen Plotter zu modifizieren oder zu erweitern. Erstens ist die Temperaturauflösung der aktuellen Implementierung ziemlich niedrig, da der Ring, in dem die Temperatur geplottet wird, recht schmal ist. Du könntest den Text in der Mitte verkleinern und den Ring breiter machen, um eine bessere Auflösung zu erzielen.

Zusätzlich zur Temperatur wäre es schön, auch Luftdruck und Luftfeuchtigkeit über die Zeit zu überwachen. Das könnte man erreichen, indem man drei Canvas-Flächen (für Temperatur, Druck und Feuchtigkeit) hat und zwischen ihnen entweder periodisch oder per Knopfdruck wechselt.

Du könntest sogar die WLAN-Geschwindigkeit über die Zeit überwachen und plotten, da der ESP32 sowieso mit dem Internet verbunden ist. Generell ist jeder Parameter, den du über einen Tag (z.B. light intensity, air quality, dust, …) oder andere periodische Zeitintervalle messen möchtest, eine gute Wahl für einen Polar-Plotter. Die Ecken des Displays eignen sich auch gut, um Wettervorhersagedaten anzuzeigen.

Zum Schluss noch ein Wort zum Deep-Sleep. Es wäre schön, den ESP32 zwischen den Display-Updates in den Deep-Sleep-Modus zu versetzen, aber das ist nicht einfach, da dabei der gesamte Speicher inklusive der Zeichnung auf dem Canvas verloren geht und das Canvas zu groß für RTC memory ist. Stattdessen müssten wir die Temperatur-Zeitreihe in einem kleinen Array speichern, das in den RTC-Speicher passt. Nicht unmöglich, aber etwas einschränkend.

Viele Ideen zum Ausprobieren. Viel Spaß beim Tüfteln : )