ESP32 als Siebenschläfer

Ein Bericht über meine Versuche mit einem ESP32 und der Arduino IDE einen autarken Email-Versand von Sensordaten aus dem Garten zu realisieren. Das dafür am geeignetsten erscheinende Board ist eines von Banggood, welches zusätzlich zum ESP32 auch noch ein OLED-Display und eine Akkuhalterung für 18650er Akkus integriert hat.

Wenn man sich, ohne wirklich Ahnung zu haben, so wie ich, in die Arduino Welt eingearbeitet hat und erste ‘Real life’-Anwendungen entwickeln möchte, stößt man auf vorher nicht wahrgenommene Probleme. So ging es mir mit der schon tausendfach von anderen realisierten Idee einen Outdoor-Wettersensor zu entwickeln. Eigentlich ist die Idee super simpel. Ein ESP32-Board fragt regelmäßig einen DHT22-Sensor ab und sendet die Werte als Email an meinen Email-Account. Nix Besonderes.

Über Bugs und Troubleshooting

Aber schon mit dem DHT22 fangen die Probleme an. Einmal hatte ich einen, der keine vernünftigen Luftfeuchtigkeitswerte zur Verfügung stellte, ein anderes Mal einen, der nach einigen Tagen gar nicht mehr funktionierte und zur Stimulierung einmal kurz von der Stromversorgung getrennt werden wollte.

Ich habe das jetzt so beiläufig niedergeschrieben, aber in der Praxis bedeuteten diese Dinge jede Menge Aufwand. Anfangs weiß ich ja nicht, warum meine Anwendung nicht richtig funktioniert. Oft denke ich, dass es bestimmt mein Fehler sein muss und ich versuche stundenlang ihn zu finden. Bei dem Problem mit den falschen Werten dachte ich lange an einen Fehler von mir, hatte aber keine Zeit mich darum zu kümmern. Als ich mich dann nach einigen Wochen wieder damit beschäftigte, wusste ich nicht mehr genau, was und wie ich alles installiert hatte. Es handelte sich dabei allerdings um eine Raspberry Pi Anwendung mit Python und C-Bibliotheken und einer HTML/JavaScript-Anzeige. Ich fand den Fehler nicht und lies es erst einmal darauf beruhen. Das System lieferte daraufhin jahrelang falsche Werte für die Luftfeuchtigkeit (Zum Glück war es ja nur ein Experimentier-Projekt) in meine Datenbank!

Im Zusammenhang mit meinem ESP32 Experiment kaufte ich kürzlich einen zweiten Sensor, dachte, den könnte ich ja zum Testen einfach einmal an den Raspberry stecken und sofort wurden richtige Werte geliefert! Das ist wirklich frustrierend.

Doch genau mit diesem Sensor traten nach einigen Tagen wieder Probleme auf, die dazu führten, dass wieder keine Werte in der Datenbank ankamen. Auch diesmal verbrachte ich mehrere Stunden mit der Fehlersuche, nur um irgendwo im Internet zu lesen, dass der DHT22 tatsächlich manchmal einfach ausfällt. Man braucht ihm dann nur kurz den Strom weg zu nehmen und schon läuft er wieder!

Ich habe mich jetzt nicht weiter darum gekümmert, aber für die, die das Problem irgendwie automatisiert in den Griff bekommen wollen, ist eine mögliche Lösung, den positiven Anschluss des Sensors nicht einfach mit Plus zu verbinden, sondern mit einem auf High gesetzten GPIO-Ausgang, der im Fehlerfall kurz auf Low gesetzt wird.

Mobile Stromversorgung

Denkt man an eine mobile Stromversorgung, denkt man heutzutage an eine Powerbank. Und in der Tat funktioniert das mit den meisten ESP32 Boards und der dabei verbauten USB-Buchse scheinbar wunderbar. Diese Ansicht ändert sich aber schnell, wenn man das Board richtig lange betreiben will:

Der größte Nachteil einer Powerbank ist die damit einhergehende Elektronik, die aus dem oder den in der Powerbank eingesetzten Akkus die 5 Volt für die USB-Buchsen erzeugt. Egal wie effizient die Elektronik ihren Job erledigt, es kostet immer etwas Energie. Legt man den ESP32 in den Tiefschlaf, so ist es sogar ein Vielfaches der Energie, die der ESP32 benötigt! Aber es wird noch schlimmer: Der ESP32 verträgt gar keine 5 Volt! Das heißt eine weitere Schaltung – diesmal auf dem Board selbst – muss die 5 Volt von der Powerbank wieder auf 3.3 Volt herunter konvertieren und die benötigt auch wieder Energie. Der Einsatz einer Powerbank ist also doppelt unsinnig und sollte bei mobilen Anwendungen möglichst vermieden werden.

Der zweite Nachteil mancher Powerbanks ist die Eigenschaft sich einfach abzuschalten, wenn der Strom eine bestimmte Menge unterschreitet. Letzteres kann man allerdings softwaregestützt vermeiden, wenn man innerhalb des Zeitraums, indem die Powerbank auf mehr Strom wartet, einfach mal kurz am Strom zieht. 🙂 Dazu kann man an einen GPIO-Eingang einen passenden Widerstand anbringen oder man aktiviert einfach einmal kurz das Wifi!

Beide Nachteile sind denkbar schlechte Voraussetzungen für einen geruhsamen Schlaf des ESP32.

ESP32 mit OLED und 18650 Akku

Vor kurzem entdeckte ich, dass es die ESP32 mit OLED Display auch noch zusätzlich mit einem Fach für einen 18650er Akku gibt. Sogar eine Ladeelektronik ist vorhanden. Und das Board kann gleichzeitig betrieben und geladen werden. Bei der Recherche zu diesem Artikel habe ich gelesen, dass die Spannung eines 18650er Akku, die je nach Ladezustand 3.7 bis 4.2 Volt beträgt, zu hoch für den ESP32 (2.3 bis 3.6 Volt) sein soll und sie darum noch einmal herunter konvertiert werden muss. Bei meinen Versuchen mit dem Board konnte ich einen Strom im ‘Deep Sleep’ dieses Boards von um die 10 Milliampere messen.

Ich habe nachgemessen und bei einer Akkuspannung von 4 Volt lagen bei VCC nur etwa 3.3 Volt an. Also scheint es einen Gleichspannungswandler zu geben. Das hat mich dazu gebracht noch einmal genauer nachzuforschen. Auf dem Board ist ein ‘Low Dropout’-Regler mit der Bezeichnung AMS1117 verbaut. Vermutlich wird der sowohl für die 5 Volt vom USB-Anschluss als auch für den 18650er verwendet.

Mit meinem Solarpanel wird das Board geladen, jedenfalls leuchtet die entsprechende LED auf dem Board auf, wenn genügend Sonnenlicht auf das Panel fällt. Langzeittests stehen allerdings noch aus. Aber schon jetzt scheint mir das Board zusammen mit einem 18650er eine sehr vielversprechende Lösung für eine mobile Computeranwendung zu sein. Denn bei ersten Versuchen scheint es länger durchzuhalten als meine Soshine E3S-Powerbank, sogar wenn ich zwei 18650er Akkus eingelegt habe!

Zeitprobleme

Wenn wir heutzutage einen Computer einschalten, so ist es ganz selbstverständlich, dass der weiß wie viel Uhr es gerade ist. Er kann dass, weil er irgendwo in seinem Innern einen kleinen Akku besitzt, der dafür sorgt, dass ein kleiner Zähler immer weiter zählen kann, auch wenn wir denken, das er eigentlich stromlos ist. Der ESP32 hat auch so einen Zähler, RTC (Real Time Clock) genannt, allerdings hat er keinen eingebauten Akku. Für die richtige Stromversorgung müssen wir Entwickler sorgen.

Wird unsere mobile Anwendung zum ersten Mal eingeschaltet, so fängt der Zähler bei 0 an zu laufen. Der ESP32 hat in diesem Moment keine Ahnung, ob er in der Vergangenheit, in der Zukunft oder in einem Paralleluniversum aufwacht. Soll er seine Daten mit einem korrekten Datum versenden, müssen wir es ihm mitteilen. Das ginge mit:

  • Mit einem angelötetem Taster und einem mehr oder weniger trickreichen Protokoll, wie ich es zum Beispiel von einer Honda CB1000R her kenne.

  • Per DCF77-Antenne, da holt er sich die unglaublich genaue Zeit einer Atomuhr aus den Funkwellen um uns herum.

  • Aus einem extern angeschlossenen RTC-Modul. Scheint ein bisschen Overkill, da der ESP32 ja schon ein RTC-Modul eingebaut hat.

  • Oder, und so mache ich es, von einem Zeitserver über WiFi aus dem Internet.

Um die Email zu versenden wird ja sowieso gelegentlich eine Internet-Verbindung benötigt. Da kann man auch gleich die Zeit holen. Alle anderen Lösungen benötigen deutlich mehr Aufwand. Leider ist der Zähler nicht sonderlich genau, darum muss er von Zeit zu Zeit synchronisiert werden. Das ist auch der Grund, warum manche Leute eine wesentlich genauere externe RTC anschließen. Es ist aber auch möglich zu ermitteln, um wie viele Sekunden die Uhr pro Tag falsch geht und sie einmal am Tag um diesen Wert zu korrigieren. Damit sollte man einige Monate auskommen.

Deep Sleep

Ein ESP32 benötigt ohne WiFi und ohne Zusatzbeschaltung so ungefähr 20 – 40 mA. Bei direktem Einsatz des ESP32 mit einem 18650er Akku und bei 3.3 Volt sollte der Akku ungefähr 115 Stunden (3500 mAh / 30 mAh) halten, also knapp 5 Tage. Das ist gar nicht schlecht, aber weit von autark entfernt. Nun könnte man ein Solarpanel anschließen und bei etwas Sonne an jedem Tag, sollte die Autarkie gewährleistet sein. Aber schon eine längere Regenzeit zerstört unser Vorstellungen. Darum gilt es das Board so stromsparend wie nur irgend möglich zu machen. Zum Glück bietet der ESP32 dazu eine tolle Möglichkeit an: Er kann in eine Art Tiefschlaf versetzt werden, in denen er nur wenige Mikroampere benötigt! Im Detail kann man darüber hier nachlesen. Denn der ESP32 hat nicht nur einen Dual-Core Prozessor, sondern auch noch einen extrem stromsparenden Co-Prozessor! Das ermöglicht ihm den leistungsfähigen Dual-Core Mikroprozessor komplett abzuschalten und trotzdem weiter zu ‘existieren’. Dabei wird aber auch das RAM abgeschaltet, das heißt keine Variablen und keine Daten mehr. Jedes Mal wenn der ESP32 ‘aufwacht’, ist es für ihn wie beim Booten eines Computers. Seine Speicher waren stromlos und enthalten keinerlei Informationen mehr. Doch die Entwickler des ESP32 haben mit gedacht und dem Co-Prozessor einen kleinen Speicher mit 8 KByte mitgegeben, der auch im Deep Sleep mit minimal Strom versorgt wird. Wenn man ihn nutzen möchte, schreibt man einfach ‘RTC_DATA_ATTR’ vor die Deklaration einer Variablen.

Programmierung

Normalerweise läuft bei einem Arduino Programm die Setup-Funktion genau einmal und anschließend nur noch die Loop-Funktion. Bei einem ‘Deep Sleep’ Einsatz ist das anders. Zwar könnte man auch die Loop-Funktion verwenden, da aber nach einem ‘Deep Sleep’ auf jeden Fall wieder die Setup-Funktion aufgerufen wird, bietet es sich an, den ganzen Code dort unter zu bringen und die Loop-Funktion leer zu lassen.

Man könnte das ganze Programm jetzt in folgende Einzelschritte auflösen:

  • Programm wacht auf.

  • Verbindet sich mit dem Internet.

  • Holt sich die Zeit aus dem Internet.

  • Ermittelt die Sensor-Werte.

  • Sendet eine Email mit den Werten.

  • Beendet die Internetverbindung.

  • Geht in den ‘Deep Sleep’.

Ich möchte aber das Programm auch als Rumpf für andere Aufgaben verwenden und darum soll es deutlich häufiger wach werden, als es Emails versenden muss. Die Zeit soll nur geholt werden, wenn sie auch erforderlich ist. Also nur am Anfang und später dann nur noch, wenn sie für meinen Geschmack zu weit von der tatsächlichen Zeit abgewichen ist. Darum ist es wichtig zu wissen, ob der ESP32 das erste Mal eingeschaltet wurde, oder ob er aus einem ‘Deep Sleep’ zurück kommt. Dafür habe ich mir eine Variable ‘timeIsHere’ deklariert:

RTC_DATA_ATTR boolean timeIsHere = false;

Verschlüsseln

Im Code gibt es zwei Stellen, an denen man den User-Namen und das Passwort als Base64 verschlüsseln muss. Das geht ganz einfach mit dieser Seite. Ich kann Euch aber nicht garantieren, das die eingegebenen Daten nicht noch anderweitig verwendet werden. Falls ihr es lieber selbst kodieren möchtet, verweise ich Euch hiermit an die nötigen Informationen in der Wikipedia.

Solarpanel

Das von mir verwendete Solarpanel lag schon länger bei mir herum und hat mit etwa 12 Volt eine viel zu hohe Spannung für das Board. Darum habe ich mir noch bei Banggood diesen Konverter gekauft. Er stabilisiert die 12 Volt mit angeblich 95%iger Effizienz auf 5 Volt herunter. Wenn zu wenig Licht vorhanden ist, schaltet er ab und ermöglicht so bei wiederkehrender Sonne einen Restart des Ladevorgangs.

Nachgemessen

Während eines Zugriffes auf das Internet über das WiFi verbraucht meine Anwendung für wenige Sekunden 140 mA. Ist dass WiFi aus, sind es nur 40 mA. Und wenn der ESP32 im Tiefschlaf ist, sind es weniger als 1 mA! Mir ist bekannt, dass der reine ESP32 ohne externe Beschaltung mit einigen wenigen µA auskommt. Aber so, mit dem DHT22 und dem Regler für die Spannung finde ich 1 mA schon ziemlich gut.

Code

#include <WiFi.h>
#include <TimeLib.h>
#include <SimpleDHT.h>
#include <SSD1306.h>

#include "soc/rtc_cntl_reg.h"

#define TTY_SPEED 115200        /* Geschwindigkeit der seriellen Schnittstelle */
#define uS_TO_S_FACTOR 1000000  /* Multiplikator für die Umrechnung von Sekunden zu Mikrosekunden */
#define TIME_TO_SLEEP  60       /* Wie viele Sekunden soll der ESP32 schlafen? */

RTC_DATA_ATTR long bootCount = 0;
RTC_DATA_ATTR boolean timeIsHere = false;
RTC_DATA_ATTR boolean timeIsSynchronized = false;

int pinLED = 16;
int pinDHT22 = 17;
const char* SSID = "SSID";
const char* PASS = "PASSPHRASE";
char server[] = "smtp.1und1.de";

esp_sleep_wakeup_cause_t wakeup_reason;

SSD1306 display(0x3c, 5, 4);
SimpleDHT22 dht22(pinDHT22);
WiFiClient client;

String dayTime;
float temperature = 0;
float humidity = 0;

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

  Serial.begin(TTY_SPEED);
  Serial.print(bootCount);
  Serial.println(" Mal");

  display.init();
  display.setFont(ArialMT_Plain_10);
  display.flipScreenVertically();
  display.setContrast(255);

  print_wakeup_reason();

  // Ermittlung der aktuellen Zeit in Sekunden.
  struct timeval tv;
  gettimeofday(&tv, NULL);
  long curTime = tv.tv_sec;
  Serial.print(curTime);
  Serial.println(" Sekunden");

  // Ist ein Tag vergangen, muss die Zeit neu vom Zeitserver abgefragt werden.
  if (curTime % 86400 <= TIME_TO_SLEEP) {
    timeIsSynchronized = false;
  }

  // Wenn seit dem letzten Einschalten noch nie die Zeit abgefragt worden ist oder
  // wenn sie neu synchronisiert werden soll, dann versuchen die Zeit neu abzufragen
  // und neu in der ESP32 RTC setzen.
  if (timeIsHere == false || timeIsSynchronized == false) {
    tv = setRealTime();
    tv.tv_sec = tv.tv_sec + 3600;
    tv.tv_usec = 0;
    settimeofday(&tv, NULL);
  }

  // Zeit ausgeben
  char buffer[30];
  strftime(buffer, sizeof(buffer), "%d.%m.%Y  %T", localtime(&tv.tv_sec));
  dayTime = buffer;
  Serial.println(dayTime);
  display.clear();
  display.drawString(0, 0, dayTime);

  // Wenn eine gültige Zeit vorhanden ist...
  if (timeIsHere == true) {
    // ... Sensordaten ermitteln und auf dem Display ausgeben.
    int err = SimpleDHTErrSuccess;
    if ((err = dht22.read2(&temperature, &humidity, NULL)) == SimpleDHTErrSuccess) {
      display.drawString(0, 10, (String) temperature + " C");
      display.drawString(0, 20, (String) humidity + " %");
      display.display();
    } else {
      Serial.print("Read DHT22 failed, err=");
      Serial.println(err);
    }
    // ...nachsehen ob eine Viertelstunde vergangen
    // ist und falls ja, die Daten als Email versenden.
    if (curTime % 900 <= TIME_TO_SLEEP) {
      if (connectWiFi()) {
        byte ret = sendEmail();
      }
      WiFi.disconnect();
    }
  }

  bootCount = bootCount + 1;
  delay(5000);
  
  // Display löschen. Spart bei einem OLED-Display Energie!
  display.clear();
  display.display();

  // Den ESP32 in den 'Deep Sleep' bringen.
  Serial.println("Schlafen gehen...");
  esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
  esp_deep_sleep_start();
  delay (100);
}

void loop() {
}

struct timeval setRealTime () {
  display.clear();
  display.drawString(0, 0, "Try to get act time ...");
  display.display();

  if (connectWiFi()) {
    configTime(0, 0, "ptbtime1.ptb.de", "ptbtime2.ptb.de", "ptbtime3.ptb.de");
    struct tm timeinfo;
    if (getLocalTime(&timeinfo)) {
      timeIsHere = true;
      timeIsSynchronized = true;
      display.drawString(0, 10, "Success!");
      display.display();
    } else {
      display.drawString(0, 10, "I try next boot again...");
      display.display();
    }
    delay (2000);
  }
  WiFi.disconnect();

  struct timeval tv;
  gettimeofday(&tv, NULL);
  return tv;
}

bool connectWiFi () {
  bool connectState = false;
  int tryCounter = 10;
  delay(10);
  Serial.println("");
  Serial.println("");
  Serial.print("Connecting to ");
  Serial.println(SSID);
  WiFi.begin(SSID, PASS);
  while ((WiFi.status() != WL_CONNECTED) && (tryCounter > 0)) {
    delay(500);
    Serial.print(".");
    tryCounter = tryCounter - 1;
  }
  Serial.println("");
  if (tryCounter > 0) {
    Serial.println("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());
    connectState = true;
  } else {
    Serial.println("WiFi connect failed");
  }
  return connectState;
}

byte sendEmail() {
  byte thisByte = 0;
  byte respCode;

  if (client.connect(server, 587) == 1) {
    Serial.println(F("connected"));
  } else {
    Serial.println(F("connection failed"));
    return 0;
  }
  if (!eRcv()) return 0;

  Serial.println(F("Sending EHLO"));
  client.println("EHLO smtp.1und1.de");
  if (!eRcv()) return 0;
  Serial.println(F("Sending auth login"));
  client.println("auth login");
  if (!eRcv()) return 0;
  Serial.println(F("Sending User"));
  // Change to your base64, ASCII encoded user
  client.println("XXXXXXXXXX"); //<--------- User
  if (!eRcv()) return 0;
  Serial.println(F("Sending Password"));
  // change to your base64, ASCII encoded password
  client.println("XXXXXXXXXX"); //<--------- Password
  if (!eRcv()) return 0;
  Serial.println(F("Sending From"));
  // change to your email address (sender)
  client.println(F("MAIL From: dummy@compusurf.de"));
  if (!eRcv()) return 0;
  // change to recipient address
  Serial.println(F("Sending To"));
  client.println(F("RCPT To: dummy@compusurf.de"));
  if (!eRcv()) return 0;
  Serial.println(F("Sending DATA"));
  client.println(F("DATA"));
  if (!eRcv()) return 0;
  Serial.println(F("Sending email"));
  // change to recipient address
  client.println(F("To:  dummy@compusurf.de"));
  // change to your address
  client.println(F("From: dummy@compusurf.de"));
  client.println(F("Subject: Dummys Wetter\r\n"));
  client.print(F("Temperatur: "));
  client.print((String) temperature);
  client.println(F(" C"));
  client.println(F("\n"));
  client.print(F("Luftfeuchtigkeit: "));
  client.print((String) humidity);
  client.println(F(" %"));
  client.println(F("\n"));
  client.print(F("Sendezeit: "));
  client.println(dayTime);
  client.println(F("\n"));

  client.println(F("."));
  if (!eRcv()) return 0;
  Serial.println(F("Sending QUIT"));
  client.println(F("QUIT"));
  if (!eRcv()) return 0;
  client.stop();
  Serial.println(F("disconnected"));
  return 1;
}

byte eRcv() {
  byte respCode;
  byte thisByte;
  int loopCount = 0;
  while (!client.available()) {
    delay(1);
    loopCount++;
    // if nothing received for 10 seconds, timeout
    if (loopCount > 10000) {
      client.stop();
      Serial.println(F("\r\nTimeout"));
      return 0;
    }
  }
  respCode = client.peek();
  while (client.available()) {
    thisByte = client.read();
    Serial.write(thisByte);
  }
  if (respCode >= '4') {
    efail();
    return 0;
  }
  return 1;
}

void efail() {
  byte thisByte = 0;
  int loopCount = 0;
  client.println(F("QUIT"));
  while (!client.available()) {
    delay(1);
    loopCount++;
    // if nothing received for 10 seconds, timeout
    if (loopCount > 10000) {
      client.stop();
      Serial.println(F("efail \r\nTimeout"));
      return;
    }
  }
}

void print_wakeup_reason() {
  wakeup_reason = esp_sleep_get_wakeup_cause();
  switch (wakeup_reason) {
    case 1  : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
    case 2  : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
    case 3  : Serial.println("Wakeup caused by timer"); break;
    case 4  : Serial.println("Wakeup caused by touchpad"); break;
    case 5  : Serial.println("Wakeup caused by ULP program"); break;
    default : Serial.println("Wakeup was not caused by deep sleep"); break;
  }
}

Ein Kommentar

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert