Goodwatch – Der eInk-Wecker, der auf meine Stimme hört – cloudfrei und läuft monatelang mit Batterie!
Ich habe bereits den MCW gebaut – einen eInk-Wecker, der auf einem Arduino Pro Mini läuft – und war fasziniert vom tollen Aussehen und der Energieeffizienz von eInk-Displays. Ein Problem, das ich nicht wirklich effizient lösen konnte, war die Programmierung des Weckers. Immer so viele Tasten… also möchte ich, dass mein Wecker mich versteht – meine Worte auf Deutsch: die Zahlen null bis neun und die Wörter „ja“ und „nein“.
Eine einfache Möglichkeit wäre, ein Mikrofon einzubauen und die Uhr mit Googles SpeechToText-API verbinden zu lassen – den Text zurückzubekommen – und den Wecker einzustellen. Aber mal ehrlich, wer will schon eine direkte Verbindung von seinem Schlafzimmer in die Cloud. Ich könnte auch einfach Alexa nutzen – nicht mein Stil.
Also habe ich drei Projekte gestartet:
- Einem ESP32-Mikrocontroller beibringen, meine Stimme zu verstehen: Edge-KI – Embedded Machine Learning
- Eine wirklich smarte ePaper/eInk-Uhr ohne „Knöpfe“ bauen – mit Sprache, Bewegung und Licht zur Steuerung,
- !!!!! NEU NEU !!! Verwende einen Syntiant NDP 101 für die Spracherkennung https://44-2.de/syntiant-ndp-101—always-on-low-power-speech-recognition/
Hardware… Hardware… Hardware……
Der Schlüssel zum Erfolg – ist die Wahl der richtigen Hardware – das war eine wichtige Lektion, die ich aus meinem letzten Projekt gelernt habe. Hier ist meine Wahl:
eInk-Display
- Gutes Display – E-Ink-EPD-Display 4,2 Zoll E-Ink Raspberry GDEW042T2
Das E-Ink-Display unterstützt Teilaktualisierung und Kommunikation über den I2C-Bus – wichtig, nicht zu viele IO-Pins vom ESP32 zu belegen. - DESPI-C02 – eine kleine Platine, um das E-Paper mit dem ESP32 zu verbinden (kein Anschluss für die Stromversorgung des FrontLight-Panels) und DESPI-C03 (mit Anschluss). Achtung: Das DESPI-C03 ermöglicht es auch, beide FFC-Kabel für das Display und das FrontLight anzuschließen. Wenn du denkst, du kannst beide gleichzeitig mit Strom versorgen, zerstörst du dein Display
eInk FrontLight
Wenn du nachts die Uhrzeit ablesen willst, brauchst du Licht. Bei eInk ist das etwas kompliziert, du brauchst eine spezielle FrontLight. Das ist im Grunde ein Stück Glas mit LEDs am Rand. Ich hatte überlegt, es selbst zu bauen. Aber ich glaube, es zu kaufen, war eine viel bessere Idee. Trotzdem war es etwas komplizierter, das gewünschte Ergebnis zu erzielen:
- Good Display – FrontLight-Panel <- das ist der Clou, und ich finde, es sieht nachts einfach cool aus. Das FrontLight muss genau auf das Display passen. Ich habe ziemlich viele E-Mails mit Good Display ausgetauscht, um diese Kombination auszuwählen, und war sehr überrascht von ihrem tollen Kundenservice. Am Ende kam es direkt aus China 🙂
- FFC-Adapter RM 2,54 zum Anschluss des FrontLight-Panels an die Stromversorgung (siehe oben)
- HW-045 DC-DC-Boost-Spannungsregler: Das FrontLight-Panel hat 7 LEDs à 200 Ohm in Reihe; wenn sie 15 mA zum Leuchten brauchen, brauchst du 21 V. Ups – noch ein Bauteil nötig, ein Spannungsverstärker.
- IRF3708 Transistor N-MOSFET 30V 62A 87W TO220AB – zum Dimmen des Frontlichts, das muss sich natürlich an die Außenlichtverhältnisse anpassen. Eine erste Idee, PWM als Eingang für den Spannungsregler zu nutzen, war etwas naiv, daher nutzt der ESP32 den MOSFET, um den Ausgang des Spannungsreglers per PWM zu steuern, bevor das Display mit Strom versorgt wird. Damit das Ganze lange im Akkubetrieb läuft, muss jede Hardwarekomponente im Energiesparmodus sein, wenn der ESP32 im Ruhezustand ist. Für den Verstärker habe ich folgende Lösung gefunden – achte auf das winzige blaue Kabel – das versetzt den Verstärker ebenfalls in den Tiefschlafmodus:
- MAX98357A
Zusätzliche Hardware und Sensoren
Hier kommt der Rest. Ich musste mehrere Runden durchlaufen, bis ich die beste Wahl gefunden hatte – aber ich bin wirklich zufrieden mit der folgenden Auswahl:
- ESP32 als „https://www.wemos.cc/en/latest/d32/d32_pro.html“ (320 KB RAM, 4 MB PSRAM, 16 MB Flash). Zwei sehr wichtige Dinge: Mein Exemplar hatte 16 MB Flash (Programmcode, SPIFF), was super ist, wenn du mit Machine Learning arbeiten und etwas ausgefallenere Sachen machen willst, sowie ein integriertes LIPO-Ladegerät und 4 MB PSRAM (das wird auch von der Sound-Bibliothek und für ML benötigt)
- Adafruit DS3231 – Präzisionsuhr: Ein Wecker braucht Zeit und Alarme, beides kann der DS3231 leisten – unterstützt von einer kleinen Batterie, sodass Zeit und Alarme einen kurzen Stromausfall überstehen. Ich habe mich schließlich für die teurere Version von Adafruit entschieden, da zwei viel günstigere Versionen aus dem Internet einfach zu schnell liefen – ca. 10 Sekunden pro Tag – keine Option 🙂 Der DS3231 löst alle 5 Minuten einen Interrupt aus, um die Uhrzeit anzuzeigen und natürlich, wenn ein Alarm anliegt.
- CS43434 I2S Digitalmikrofon – es gibt verschiedene Mikrofone auf dem Markt, ich habe sie alle ausprobiert. Zumindest von diesem kann ich sagen, dass es mit dem ESP32-I2S-Bus kompatibel ist und wirklich in guter Qualität aufnimmt. Meines habe ich über Tindie bekommen.
- PIR HC-SR602 Infrarotsensor: Im Video siehst du, dass ich den Alarm durch eine Handbewegung über der Uhr deaktiviere – das ist ein PIR-Sensor. Erstaunlich – super, super geringer Stromverbrauch und so empfindlich. Der PIR-Sensor löst einen Interrupt auf dem ESP32 aus, um das Gerät zu wecken.
- Adafruit I2S 3W Class-D-Verstärker-Breakout – MAX98357A: Beim Aufwachen soll ein schöner Ton erklingen, und der ESP32 hat genug Rechenleistung, um sogar MP3-Dateien zu decodieren oder von einem Radiosender zu streamen. Auch dieser Abschnitt befasst sich mit der Hardware – https://github.com/schreibfaul1/ESP32-audioI2S, um mit 5 Zeilen Code Ton in den Wecker zu bringen.
- APDS-9960 – Digitaler Näherungs-, Umgebungslicht-, RGB- und Gestensensor: Ich wollte Gesten nutzen, um den Wecker zu aktivieren und zwischen verschiedenen Betriebsmodi zu wählen. Der APDS-9960 ist wirklich clever, er erkennt verschiedene Handbewegungen und misst Entfernungen. Außerdem kann er die Lichtstärke und -farbe erfassen. Ich habe viel Zeit investiert, aber er verbraucht auch im Energiesparmodus immer noch viel Strom. Die einzige Möglichkeit, ihn effizient zu nutzen: Den PIR-Sensor (siehe oben) immer laufen lassen – und dann die Messung starten. Leider verursacht das eine Verzögerung bei der Verarbeitung – und damit kommen wir zu meinem nächsten Projekt: Aufwachen mit meiner Stimme.
Software
Anfangs habe ich die Arduino-Plattform genutzt, um dieses Projekt zu entwickeln. Diese Plattform enthält ein vorkompiliertes ESP-IDF-Paket. Das ESP-IDF ist Espressifs offizielles IoT-Entwicklungsframework für den ESP32. Das hat zur Folge, dass alle Einstellungen, die zum Kompilieren des ESP-IDF verwendet wurden, feststehen und nicht geändert werden können.
Boot-Geschwindigkeit ist entscheidend
Beim Aufwachen der Goodwatch möchte ich eine schnelle Reaktion sehen – habe aber eine Pause von 1–2 Sekunden bemerkt, bevor der ESP32 aktiv wurde. Der ESP nutzt diese Zeit, um den Flash-Speicher und den RAM zu validieren. Da der Wemos D1 Pro über 4 MB PSRAM und 16 MB Flash verfügt, dauert das eine Weile.
Durch eine Änderung der Compiler-Optionen konnte diese Überprüfung deaktiviert werden.
Arduino als Komponente
Der Trick besteht darin, Arduino als Komponente zu installieren. Das für die Kompilierung verwendete Haupt-Framework ist das ESP IDF, das die Arduino-Bibliotheken als Unterordner enthält. Dies ermöglicht den Zugriff auf ein Dienstprogramm namens „menueconfig“, das eine Konfigurationsdatei generiert, z. B. sdkconfig.lolin_d32_pro.
Das Problem ist, dass Arduino und ESP-IDF unterschiedliche Build-Tools verwenden. Nach langem Suchen habe ich es geschafft, das zum Laufen zu bringen. Geschwindigkeit ist wieder KING, ich würde sagen, weniger als 500 ms Reaktionszeit auf eInk ist gut.
Den Quellcode findest du hier. Ich habe in der platformio.ini einige Kommentare zum Build eingefügt.
Alles ist auf GitHub: https://github.com/happychriss/GoodWatch_espidf
In Kreisen denken…
Ich hatte Spaß daran, die Kreisbögen zu zeichnen – das weckte Erinnerungen an die Schulzeit mit meinen Freunden „Sinus“ und „Cosinus“. Ich bin mir sicher, dass es eine viel elegantere Lösung gibt – aber zu einem bestimmten Zeitpunkt war ich einfach nur glücklich, die Kreise zu sehen. Insgesamt glaube ich, dass ich einen recht effizienten Weg gefunden habe. Schau dir diesen „frechen“ Code-Schnipsel zum Zeichnen eines Bogens an – und das Ergebnis:


void DrawArcCircle_Q3(GxEPD2_GFX &my_display, st_pwin *pwin, int x0, int y0, double start_angle, double end_angle, int ri, int ro) {
start_angle = (270 - start_angle) * (M_PI / 180);
end_angle = (270 - end_angle) * (M_PI / 180);
double tan_start_angel = tan(start_angle);
double tan_end_angel = tan(end_angle);
int ri2 = ri * ri;
int ro2 = ro * ro;
for (int cy = 0; cy <= ro; cy++) {
int cx_i;
if (abs(ri) < abs(cy)) {
cx_i = -1;
} else {
cx_i = (int) (sqrt(ri2 - cy * cy) + 0.5);
}
int cx_o = (int) (sqrt(ro2 - cy * cy) + 0.5);
int cyy = cy + y0;
int dx_1 = cy / tan_end_angel;
int dx_2 = cy / tan_start_angel;
if (dx_1 < cx_o) { cx_o = dx_1; };
if (dx_2 > cx_i) { cx_i = dx_2; };
if (cx_i <= cx_o) {
gfx_line(my_display, pwin, x0 - cx_i, cyy, x0 - cx_o, cyy);
}
}
}
Zeit braucht Zeit – und das Rechnen mit Tagen und Monaten – noch mehr
Ein weiteres einfaches Thema – das mich einige Zeit gekostet hat, bis ich es hinbekommen habe: Das Rechnen mit Zeit. Mein Wecker hat 5 verschiedene Weckalarme, der DS3231 kann 2 Alarme verwalten und ein Interrupt-Signal erzeugen, um den ESP32 zu wecken. Ein Alarm wird alle 5 Minuten zum Wecken verwendet, um einen neuen Bogen zu zeichnen. Der andere muss pünktlich aktualisiert werden – also musst du die Wecker sortieren, um den nächstmöglichen zu finden. Jetzt hast du Wecker an Wochentagen und am Wochenende, wiederkehrende Wecker und einmalige Wecker – du verstehst schon… Ich bin nicht stolz auf meinen Code hier – aber das Sortieren war am Ende einfach.
struct strct_alarm {
uint8_t i =0;
bool active = {false};
bool valid = {false};
DateTime time;
};
int SortTimes(void const *a, void const *b) {
const struct strct_alarm *ad = (struct strct_alarm *) a;
const struct strct_alarm *bd = (struct strct_alarm *) b;
return (int) (ad->time.secondstime() - bd->time.secondstime());
}
// Calling the qsort
qsort(rtcData.d.alarms, ALARM_NUMBERS_DISPLAY, sizeof(strct_alarm), SortTimes);
Flash – Speicher sichern
Die Alarmzeiten müssen auf dem ESP32 gespeichert werden und sollten einen Stromausfall überstehen (ähnlich wie die Uhrzeit). Der ESP32 verfügt über einen FLASH-Speicher, perfekt – genau das, was ich brauchte. Nachdem ich ein bisschen mit CRC-Werten herumprobiert hatte, um zu prüfen, ob die Daten im Flash gültig sind, und mit Adressen… erfuhr ich in der ESP-Dokumentation von einer ESP32-Funktion namens „NVS“. – Da wurde es dann wirklich einfach. Es prüft sogar, ob der Flash konsistent ist.
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
// NVS partition was truncated and needs to be erased Retry nvs_flash_init
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK( err );
// Write:
err = nvs_open(STORAGE_NAMESPACE, NVS_READWRITE, &my_handle);
size_t required_size = sizeof(d);
if (err != ESP_OK) DPL("NVS_OPEN ERROR");
err = nvs_set_blob(my_handle, STORAGE_NAMESPACE, (uint8_t *) &d, required_size);
if (err != ESP_OK) DPL("NVS_WRITE ERROR");
// Commit
err = nvs_commit(my_handle);
if (err != ESP_OK) DPL("NVS Commit Error");
// Read:
err = nvs_get_blob(my_handle, STORAGE_NAMESPACE, (uint8_t *) this, &required_size);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND) {
DPL("NVS READ ERROR");
}
Programmierung des eInk-Displays
Das wäre eigentlich der komplizierteste Teil gewesen, aber dank: https://github.com/ZinggJM/GxEPD2 war es wirklich einfach. Es war ein bisschen Arbeit, diese Bibliothek zu finden und das Konzept zu verstehen. Es gibt auch ein Support-Forum und der Autor ist wirklich hilfsbereit. DANKE.
Es ist erwähnenswert, dass das Display über einen einfachen Adapter DESPI-C02 oder DESPI-C023 mit einem FCC-Kabel an den ESP32 angeschlossen ist, von dort aus ist es mit dem ESP verbunden. Die meisten Fehlerursachen liegen in der Verkabelung und der korrekten Zuordnung der PINS – und ich habe MISO und MOSI vertauscht — das hat mich so lange gekostet 🙂
// From my hardware // EPD Pins - GxEPD2_display_selection_new_style.h using SPI // EPD Pins**** // CS=5, DC=0, RST=2,BUSY=15; // SPI: SCK=18, SDI=MISO=23 // BUSY -> 15, RST -> 2, DC -> 0, CS -> 5, CLK -> SCK(18), DIN -> MOSI(23), GND -> GND, 3.3V -> 3.3V #define EPD_CS 5 // Chip Select - #define EPD_DC 0 #define EPD_RST 2 #define EPD_BUSY 15 GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> display(GxEPD2_DRIVER_CLASS(/*CS=5*/ EPD_CS, /*DC=*/ EPD_DC, /*RST=*/ EPD_RST, /*BUSY=*/ EPD_BUSY));





