Halloween Kürbis 2.0


Einführung

In diesem Beitrag zeige ich Ihnen, wie Sie mit einem ESP32 oder ESP8266, einem TCA9548A I2C-Multiplexer, zwei 1,3 Zoll OLED Displays, drei VL53L0X ToF-Sensoren und ein wenig Bastelarbeit einem Kürbis zu Halloween ein bisschen Leben einhauchen. Los geht's.   

Was wir benötigen

Anzahl Bauteil Anmerkung
1 ESP32 NodeMCU oder ESP-32 Dev Kit C V4 oder ESP8266 D1 Mini
1,3" OLED I2C 128x64 Display
1 VL53L0X Time-of-Flight (ToF) Laser Abstandssensor
1 TCA9548A I²C Multiplexer
Verbindungskabel
1 Externe Spannungsquelle 5V (empfohlen)
1 100 µF Kondensator (empfohlen)
PC mit Arduino IDE und Internetverbindung
Lötkolben (für die Pinheader)
Kürbis (nicht ganz so groß, ca 15 bis 20 cm Durchmesser)
Schnitzwerkzeug (aus der Küche)


Für Batteriebetrieb: 

Anzahl Bauteil Anmerkung
1 LiPo Akku 3,7 V
1 MT3608 Step up Modul
1 TP4056 Laderegler Modul
Umschalter
Voltmeter


Ein Tipp vorweg

Wenn Sie nicht jedes Mal den BOOT-Button am ESP32 betätigen wollen, um ein Programm hochzuladen, folgen Sie dieser Anleitung. Benötigt wird ein 10 µF Kondensator zwischen den Pins EN (+) und GND (-).

Schaltplan

Bauen Sie die Schaltung nach folgendem Schema auf:

TCA9548A I²C Multiplexer

ESP8266 Pins

Spannungsquelle

VIN

5V

5V

GND

GND

GND

SDA

SDA (D1, GPIO5)

 

SCL

SCL (D2, GPIO4)

 

TCA9548A I²C Multiplexer

OLED Display 1 (Links)

 

SD0

SDA

 

SC0

SCK

 

 

GND

GND

 

VDD

5V

TCA9548A I²C Multiplexer

OLED Display 2 (Rechts)

 

SD1

SDA

 

SC1

SCK

 

 

GND

GND

 

VDD

5V

TCA9548A I²C Multiplexer

ToF Sensor 1 (Mitte)

 

SD2

SDA

 

SC2

SCK

 

 

GND

GND

 

VIN

5V

TCA9548A I²C Multiplexer

ToF Sensor 2 (Links)

 

SD3

SDA

 

SC3

SCK

 

 

GND

GND

 

VIN

5V

TCA9548A I²C Multiplexer

ToF Sensor 3 (Rechts)

 

SD4

SDA

 

SC4

SCK

 

 

GND

GND

 

VIN

5V


Sollten Sie einen ESP32 verwenden:

TCA9548A I²C Multiplexer

ESP32 Pins

Spannungsquelle

VIN

5V

5V

GND

GND

GND

SDA

SDA (GPIO 21)

 

SCL

SCL (GPIO 22)

 


Ich habe parallel zur Spannungsquelle einen 100µF_Kondensator hinzugefügt, da die ESPs sehr sensibel sind, was Spannungsschwankungen angeht. Das war ein Tipp aus einem Forum.

Der D1 Mini und auch einige ESP32 haben keinen ausgewiesenen VIN-Pin. Verwenden Sie in dem Fall eine konstante Spannung von 5V am 5V-Pin des Mikrocontrollers. Achten Sie dabei auf Hinweise im Datenblatt.

Sollten Sie wie ich einen LiPo Akku 3,7 V verwenden, empfehle ich Ihnen das MT3608 Step up Modul und das TP4056 Laderegler Modul, denn Sie müssen die Spannung anpassen. Verwenden Sie ein Voltmeter und stellen Sie am Potentiometer die richtige Spannung ein (sollte sich die Spannung nicht verändern, müssen Sie sehr lange gegen den Uhrzeigersinn drehen).

Über den Laderegler können Sie den Akku bequem wieder aufladen. Ich habe zusätzlich einen Schalter am Spannungsausgang des Konverters hinzugefügt, um nicht immer den Akku abklemmen zu müssen.

Wie Sie einen Arduino mit Strom aus einem Akku versorgen, zeigt Ihnen Gerald Lechner in seinem Blogbeitrag "5V Akku Stromversorgung mit 3.7 V LiPo Akku und Laderegler".

WICHTIGE HINWEISE

ACHTEN SIE DARAUF, DIE EXTERNE SPANNUNGSQUELLE UND DEN USB-ANSCHLUSS NICHT GLEICHZEITIG ANZUSCHLIESSEN! - ES IST KEINE SPERRDIODE ZWISCHEN DEN BEIDEN ANSCHLÜSSEN VORHANDEN

SCHLIESSEN SIE KEINE EXTERNE SPANNUNG AN DEN 3.3V-PIN! - auf dem Board ist ein Spannungswandler verbaut, der aus den 5V am 3.3V-Pin die entsprechende Spannung versorgt.

DIE EXTERNE SPANNUNG SOLLTE 5V NICHT ÜBERSCHREITEN - auf den ESP-Boards ist entgegen den bekannten Arduinos kein Spannungswandler verbaut, um am VIN-Pin konstante 5V zu erzeugen. Sie können hier also keine 9V-Batterien direkt anschließen

LASSEN SIE BILDER NICHT DAUERHAFT ANZEIGEN - es wird empfohlen, die Pixel zu wechseln, da das dauerhafte Anzeigen von gleichen Bildern zu einem Einbrennen führen kann


Das Multiplexer-Board, das ToF-Sensor-Board und auch das Display-Board haben Spannungswandler verbaut, sodass 5V oder 3.3V angeschlossen werden können.

Bitte achten Sie auf die Datenblätter Ihrer Komponenten. Für Beschädigungen aufgrund von unsachgemäßer Handhabung übernehme ich keine Haftung.

Boardeinstellungen

Dies sind meine Einstellungen für die jeweiligen Boards. Wichtig ist die Einstellung der CPU-Frequenz, da ich damit die Taktgeschwindigkeit für den I2C-Bus erhöhen konnte. Es wird auch empfohlen, am ESP8266 Erase Flash auf All Contents zu stellen.

ESP8266 D1 Mini:


ESP32 NodeMCU Devkit:


Die Komponenten

Der I2C-Multiplexer

Wie sie bereits weiter oben gesehen haben, finden Sie an dem Multiplexer-Board verschiedene Anschlüsse:


An VIN und GND wird das Board mit 3,3V oder 5V Spannnung versorgt. SDA und SCL sind die Eingänge für den I²C-Bus vom Mikrocontroller. Entgegen den TX/RX-Pins werden diese nicht kreuzverbunden. SDA an SDA und SCL an SCL (bzw. auch SCK). Mit dem RST-Pin auf GND wird das Board zurückgesetzt. Mit den Pins A0 bis A2 kann die Standard-I²C-Adresse (0x70) durch verschiedene Kombinationen geändert werden in 0x71 bis 0x77. A0 ist das niedrigste Bit. Setzt man den Pin auf HIGH, wird die Adresse um 1 erhöht. Mit dem Pin A1 auf HIGH wird die Adresse um 2 erhöht, mit Pin A2 auf HIGH um 4.

An die Pins SD0/SC0 bis SD7/SC7 werden die I²C-Komponenten angeschlossen. Dabei ist es unerheblich, wie deren Adressen lauten. Der Mikrocontroller spricht immer nur den Multiplexer an. Dieser bekommt über den Eingang am I²C-Bus eine Nummer von 0 bis 7 in ein Register geschrieben, welches den Datenstrom zum entsprechenden Ausgang durchschaltet. Ähnlich wie die Weiche einer Eisenbahn. Der Sensor unterstützt dabei eine Taktfrequenz von maximal 400 KHz. Es ist möglich, mehrere Multiplexer zu kaskadieren. Dadurch ist es möglich, bis zu 64 Geräte mit 8 Multiplexern anzusteuern.

VL53L0X Time-of-Flight Sensor

Nähere Infos finden Sie in dem Blogbeitrag VL53L0X Time-of-Flight (ToF) Laser Abstandsensor

Hier verwende ich drei Sensoren, deren I²C-Adressen ich nicht verändert habe.

1,3" OLED I2C 128x64 Display

Es handelt sich hierbei um Displays mit dem Chip SH1106. Die I²C-Adressen können nicht verändert werden, weswegen sich die Verwendung des Multiplexers empfiehlt. Die Ansteuerung ist nicht trivial, weswegen Sie Bibliotheken wie U8G2 oder OneBitDisplay verwenden sollten, die Sie über die Bibliotheksverwaltung installieren können.


Im kostenlosen eBook finden Sie eine Einrichtungsanleitung. In dem Blogbeitrag 1,3 Zoll OLED in Betrieb nehmen zeigte Albert Vu bereits, wie man das Display programmieren kann.

Der abstrakte Programmablauf

Wird das Programm gestartet, soll für eine erste Funktionsprüfung eine kurze Animation der Augen dargestellt werden. Diese verschwinden dann wieder und das Programm geht in einen Standby-Modus. Damit werden die Displays geschont und die Helle Anzeige stört nicht immer im Dunkeln. Der Überraschungseffekt, wenn sich jemand dem Kürbis nähert, ist auch wichtig.

Nähert sich eine Person und unterschreitet die angegebene Grenze, wacht der Kürbis langsam auf und die Augen werden leicht geöffnet angezeigt. Abhängig davon, welcher Sensor angeschlagen hat, schauen die Augen in die entsprechende Richtung (Mitte, Links, Rechts). Nähert sich die Person weiter, ist der Kürbis wach und die Augen sind geöffnet. Ist die Person zu nah am Kürbis, wird sich der Kürbis erschrecken und die Augen weiter aufreißen.

Entfernt sich die Person aus dem nahen Umfeld des Kürbisses, wird er sich noch eine Minute lang umsehen. Dafür werden nach Zufallsprinzip verschiedene Bilder der Augen angezeigt. Nach Ablauf dieser Zeit, sollte sich niemand mehr nähern, wird wieder die Ausschalt-Animation angezeigt und das Programm geht wieder in Standby.

Um es noch etwas lebendiger zu gestalten, habe ich ein Blinzeln implementiert. Dafür wird der Zufallsgenerator Zeiten zwischen 4 und 6 Sekunden wählen, nach denen die geschlossenen Augen für sehr kurze Zeit angezeigt werden.

Bilder für das Display

Ich habe einige Bilder erstellt, die die Augen darstellen sollen. Es sollen drei Richtungen angezeigt werden und drei Entfernungsstufen. Nähert sich eine Person, schauen die Augen in die Richtung (Mitte, Links oder Rechts).

Zuerst mit leicht geöffneten Augen (skeptisch), dann bei verringerter Distanz mit normal geöffneten Augen und bei sehr kurzer Distanz mit erschrockenen Augen. Dann sollen die Augen zwinkern, dazu zeigt das Bild geschlossene Augen. Um die Displays etwas zu schonen, werden nach einer gewissen Zeit die Augen ausgeblendet. Dafür werden sie animiert wie ein altes TV-Gerät, das abgeschaltet wird. So sehen dazu meine Bilder aus:


Die Bilder müssen im passenden Bildformat erzeugt werden. In diesem Fall in 64x128 Pixel. Anschließend müssen sie in das XBM-Format konvertiert werden. Das können Sie u.a. auf dieser Webseite tun. Das Grafikprogramm GIMP ist eine weitere Möglichkeit. Dort exportieren Sie die Bilder im XBM-Format.

Anschließend öffnen Sie die Dateien mit einem Texteditor, vorzugsweise einem wie Notepad++. Sie können dann die Daten herauskopieren und in die imagedata.cpp an der passenden Stelle einfügen. Das habe ich alles bereits getan. Mein Programm enthält alle Daten. Sollten Sie eigene Augen kreieren wollen, müssen Sie die dazugehörigen Daten nur ersetzen. 

Der Arduino Sketch

Ich werde an dieser Stelle die einzelnen Abschnitte im Quelltext beschreiben. Einen Link zum Download des kompletten Programms finden Sie weiter unten. Wenn Sie einfach nur das Programm ausführen wollen, überspringen Sie die Beschreibung und laden sich den Sketch herunter. Achten Sie darauf, dass neben der .ino Datei auch die imagedata.cpp und imagedata.h im gleichen Ordner liegen.

#include "Wire.h"                 // I2C Bibliothek
#include <U8g2lib.h>              // Display-Bibliothek
#include <VL53L0X.h>              // Bibliothek fuer ToF-Sensoren
#include "imagedata.h"            // Eigene Bilder im XBM-Format

// Wifi inkludieren, um es zu deaktivieren:
#if defined(ESP8266)
  #include <ESP8266WiFi.h> 
#elif defined(ESP32)
  #include <WiFi.h>
#endif

Für I²C benötigen wir die Wire-Bibliothek. Außerdem nutze ich hier die sehr umfangreiche Bibliothek U8G2 für die Displays. Für die Abstandsensoren kommt hier die Bibliothek VL53L0X zum Einsatz. Die Dateien imagedata.cpp und imagedata.h sollten sich im gleichen Ordner befinden, wie der Sketch.

Öffnen Sie das Programm in der Arduino IDE, sollten Sie diese beiden Dateien über die Reiter im oberen Teil des Programmfensters erreichen. Da die Headerdatei inkludiert ist, werden die beiden Dateien in der Arduino IDE automatisch mit geöffnet. Dort enthalten sind die Bilddaten aus den XBM-Dateien. Ich habe hier ein mehrdimensionales Array erzeugt, um über die Bilder mit Schleifen iterieren zu können.

Ein Tipp aus verschiedenen Foren war, den Wifi-Chip zu deaktivieren, um Strom zu sparen. Daher habe ich die Wifi-Bibliothek inkludiert. Hier wird zwischen ESP8266 und ESP32 unterschieden. Je nachdem, welchen der beiden Mikrocontroller sie verwenden.

#define I2CmultiADDR 0x70             // I2C-Multiplexer-Adresse
#define SENSORPROPERTY HIGH_SPEED     // HIGH_SPEED, LONG_RANGE, HIGH_ACCURACY

// diese Zeile auskommentieren, um die Sensorgrenze manuell zu setzen
// sollte nicht hoeher, als das Maximum des Sensors sein (VL53L0X: 2000 Long Range, sonst 1200)
//#define MANUALSENSORMAX 300

Hier wird zuerst die I²C-Adresse des Multiplexers als Konstante definiert. Außerdem habe ich die Eigenschaften der Sensoren als enum deklariert und diese hier ebenfalls als Konstante definiert. Durch Veränderungen der Sensorregister kann die Messdistanz bis auf 2m eingestellt werden. Dabei leidet allerdings die Genauigkeit. Sollten Sie diese Entfernung für Ihr Projekt benötigen, ändern Sie hier HIGH_SPEED in LONG_RANGE.

Im späteren Programmablauf werden die maximalen Grenzen passend zu diesen Konstanten eingestellt. Im LONG_RANGE-Mode sind es 2000 mm, in den anderen beiden Modi 1200 mm (das können Sie mit den Beispielsketches der Bibliothek ausprobieren). Um andere Maximalgrenzen (unterhalb der möglichen Werte) einzustellen, können Sie hier die Zeile für MANUALSENSORMAX einkommentieren und Ihren gewünschten Wert einstellen. Für mich war das praktisch während der Entwicklung, um das Programm mit kurzen Messdistanzen zu testen.

// Page Buffer Mode (langsam, weniger Speicherbedarf):
// U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2_L(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
// U8G2_SH1106_128X64_NONAME_1_HW_I2C u8g2_L(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

// Full screen buffer mode (schnell, sehr hoher Speicherbedarf):
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2_L(U8G2_R1, /* reset=*/ U8X8_PIN_NONE);   //U8G2_R1 Rotation 90 Grad
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2_R(U8G2_R1, /* reset=*/ U8X8_PIN_NONE);   //U8G2_R1 Rotation 90 Grad

In den Beispiel-Sketches der U8G2-Bibliothek finden Sie die entsprechenden Zeilen, um Objektinstanzen passend zu Ihrem Displaytypen einzustellen. Dabei wird zwischen Full Buffer und Page Buffer Mode unterschieden. Für schnellere Bildwechsel empfiehlt sich der schnellere Full Buffer Mode. Für jedes Display wird eine Instanz erzeugt.

Man kann hier bereits die Rotation einstellen. Der erste Parameter U8G2R1 entspricht einer 90 Grad Drehung. U8G2R0 wäre dann der Querbildmodus. Meine Bilder habe ich passend zum Motiv hochkant gewählt. Es ist auch möglich, mit einem Kommando die Drehung später im Programm einzustellen.

// ToF Sensor-Objekt-Instanzen
VL53L0X sensor_M;   // Mitte
VL53L0X sensor_L;   // Links
VL53L0X sensor_R;   // Rechts

// Skalierer fuer die Abstandsstufen:
uint8_t scalerCloser = 2;
uint8_t scalerToClose = 3;

Für jeden Sensor benötigen wir eine Objektinstanz der Klasse VL53L0X. Man könnte sie auch in eine Liste oder ein Array schreiben. Dann könnte man in Schleifen darüber iterieren und den Quellcode noch etwas verkürzen.

Die drei Distanzstufen für die Annäherung einer Person habe ich durch Skalierung realisiert. Das heißt, ich teile den Maximalwert durch diese Werte und erhalte immer das gleiche Abstandsverhältnis, egal welche Maximaldistanz eingestellt wird. Ansonsten müsste man diese Werte ebenfalls jedes Mal ändern.

uint16_t sensorValueMax = 0;
uint16_t sensorValueCloserMax = 0;
uint16_t sensorValueToCloseMax = 0;
unsigned long blinzelTimer = 0;
unsigned long blinzelDelay = 3000;
unsigned long standbyTimer = 0;
unsigned long standbyDelay = 60000;
unsigned long idleAnimationTimer = 0;
long idleAnimationDelay = 0;
long randomBlinzeln = 0;
bool isStandby = false;

Diese Variablen dienen der Maximalgrenzen, die später dort hineingeschrieben werden. Die Timer nutze ich, um einzustellen, in welchen Abständen geblinzelt oder in den Standby-Modus gewechselt wird. Es handelt sich dabei nicht um Interrupt-Timer. Ich habe lediglich die Namen der Variablen so gewählt.

// Fuer die Statemachine
enum States {
  IDLESTATE = 0,
  SENSOR_M = 1,
  SENSOR_L = 2,
  SENSOR_R = 3,
  BLINZELN = 4,
  TOSTANDBY = 5,
};

States state = TOSTANDBY;
States tempState = IDLESTATE;

Für die bessere Lesbarkeit habe ich statt Zahlen Enumerationen gewählt. Ich habe mit einer switch-case-Anweisung eine Zustandsmaschine implementiert. Der Startzustand ist TOSTANDBY. Dadurch wird als Erstes eine kurze Animation abgespielt und dann die Displays wieder verdunkelt.

Mit tempState wird später der aktuelle Zustand zwischengespeichert. Es wird dann geblinzelt und anschließend der vorherige Wert zurückgeladen. Dadurch kann zu allen möglichen Zeitpunkten das Blinzeln ausgeführt und wieder zum vorherigen Punkt zurückgekehrt werden.

// ToF Properties fuer Initialisierung
enum Properties {
  HIGH_SPEED = 0,
  LONG_RANGE = 1,
  HIGH_ACCURACY = 2
};

// Fuer Iterationen sind die Bilder in einem Array.
// Enum fuer bessere Lesbarkeit:
enum Eyes {
  EYE_CLOSED = 0,
  EYE_ANIM_1 = 1,
  EYE_ANIM_2 = 2,
  EYE_ANIM_3 = 3,
  EYE_ANIM_4 = 4,
  EYE_ANIM_5 = 5,
  EYE_MISSTR_L = 6,
  EYE_ERSCHR_L = 7,
  EYE_NORMAL_L = 8,
  EYE_MISSTR_M = 9,
  EYE_ERSCHR_M = 10,
  EYE_NORMAL_M = 11,
  EYE_MISSTR_R = 12,
  EYE_ERSCHR_R = 13,
  EYE_NORMAL_R = 14,
};

// I2C-Multiplexer-Ports
enum I2CDevice {
  DISPLAY_LEFT = 0,
  DISPLAY_RIGHT = 1,
  TOF_M = 2,
  TOF_L = 3,
  TOF_R = 4
};

Das sind weitere Enumerationen, um in den entsprechenden Funktionsaufrufen oder Schleifen einen besseren Überblick zu behalten.

///////////////////////////////////////////////////
// Multiplexer schaltet auf den angegebenen Port um

void I2Cmulti(I2CDevice num) {
  uint8_t i = (uint8_t) num;
  if (i > 7) return;
 
  Wire.beginTransmission(I2CmultiADDR);
  Wire.write(1 << i);
  Wire.endTransmission();  
}

Das ist die Funktion, mit der der Multiplexer durchgeschaltet wird. Man übergibt ihm die Nummer eines der 8 Ports (von 0 bis 7). Jedes Mal, wenn man eines der Geräte ansprechen möchte, muss man diese Funktion aufrufen und die passende Nummer (als Enum) übergeben. Mit I2Cmulti(DISPLAY_LEFT) wird dann z.B. der Port SD0 am Multiplexer aktiviert. Somit kann das erste Display initialisiert oder Bilddaten angezeigt werden. Um den Sensorwert des dritten ToF-Sensors einzulesen, wird I2Cmulti(TOF_R). Sie sehen also, warum enum hier Sinn macht. Ohne enum müsste man I2Cmulti(0) oder I2Cmulti(4) aufrufen, was irgendwann zu Verwirrungen führen könnte.

/////////////////////////////////////////////////
// alle ToF-Sensoren initialisieren,
// Displayausgabe im Fehlerfall:

bool initTof(Properties property){
  I2Cmulti(TOF_M);
  sensor_M.setTimeout(500);
  if (!sensor_M.init())
  {
    I2Cmulti(DISPLAY_LEFT);
    u8g2_L.clearBuffer();          
    u8g2_L.setFontMode(1);
    u8g2_L.setFont(u8g2_font_ncenB08_tr); 
    u8g2_L.drawStr(10,40,"sensor");  
    u8g2_L.drawStr(10,60,"error");  
    u8g2_L.drawStr(10,80,"middle");
    u8g2_L.sendBuffer();          
    
    I2Cmulti(DISPLAY_RIGHT);
    u8g2_R.clearBuffer();          
    u8g2_R.setFontMode(1);
    u8g2_R.setFont(u8g2_font_ncenB08_tr); 
    u8g2_R.drawStr(10,40,"sensor");  
    u8g2_R.drawStr(10,60,"error");  
    u8g2_R.drawStr(10,80,"middle");
    u8g2_R.sendBuffer();          
    return false;
  }

  I2Cmulti(TOF_L);
  sensor_L.setTimeout(500);
  if (!sensor_L.init())
  {
    I2Cmulti(DISPLAY_LEFT);
    u8g2_L.clearBuffer(); 
    u8g2_L.setFontMode(1);
    u8g2_L.setFont(u8g2_font_ncenB08_tr); 
    u8g2_L.drawStr(10,40,"sensor");
    u8g2_L.drawStr(10,60,"error");
    u8g2_L.drawStr(10,80,"left");
    u8g2_L.sendBuffer(); 
    
    I2Cmulti(DISPLAY_RIGHT);
    u8g2_R.clearBuffer();  
    u8g2_R.setFontMode(1);
    u8g2_R.setFont(u8g2_font_ncenB08_tr); 
    u8g2_R.drawStr(10,40,"sensor"); 
    u8g2_R.drawStr(10,60,"error"); 
    u8g2_R.drawStr(10,80,"left");
    u8g2_R.sendBuffer(); 
    return false;
  }

  I2Cmulti(TOF_R);
  sensor_R.setTimeout(500);
  if (!sensor_R.init())
  {
    I2Cmulti(DISPLAY_LEFT);
    u8g2_L.clearBuffer();
    u8g2_L.setFontMode(1);
    u8g2_L.setFont(u8g2_font_ncenB08_tr); 
    u8g2_L.drawStr(10,40,"sensor");
    u8g2_L.drawStr(10,60,"error"); 
    u8g2_L.drawStr(10,80,"right");
    u8g2_L.sendBuffer();   
    
    I2Cmulti(DISPLAY_RIGHT);
    u8g2_R.clearBuffer(); 
    u8g2_R.setFontMode(1);
    u8g2_R.setFont(u8g2_font_ncenB08_tr); 
    u8g2_R.drawStr(10,40,"sensor");  
    u8g2_R.drawStr(10,60,"error");  
    u8g2_R.drawStr(10,80,"right");
    u8g2_R.sendBuffer(); 
    return false;
  }

  // stellt den Sensor auf LONG_RANGE, HIGH SPEED, oder HIGH_ACCURACY,
  switch(property) {

    case LONG_RANGE: {
      I2Cmulti(TOF_M);
      // lower the return signal rate limit (default is 0.25 MCPS)
      sensor_M.setSignalRateLimit(0.1);
      // increase laser pulse periods (defaults are 14 and 10 PCLKs)
      sensor_M.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
      sensor_M.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);

      I2Cmulti(TOF_L);
      sensor_L.setSignalRateLimit(0.1);
      sensor_L.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
      sensor_L.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);

      I2Cmulti(TOF_R);
      sensor_R.setSignalRateLimit(0.1);
      sensor_R.setVcselPulsePeriod(VL53L0X::VcselPeriodPreRange, 18);
      sensor_R.setVcselPulsePeriod(VL53L0X::VcselPeriodFinalRange, 14);
    } break;

    case HIGH_ACCURACY: {
      I2Cmulti(TOF_M);
      sensor_M.setMeasurementTimingBudget(200000);
      I2Cmulti(TOF_L);
      sensor_L.setMeasurementTimingBudget(200000);
      I2Cmulti(TOF_R);
      sensor_R.setMeasurementTimingBudget(200000);
    } break;
    
    //HIGH_SPEED
    default: {
      I2Cmulti(TOF_M);
      sensor_M.setMeasurementTimingBudget(20000);
      I2Cmulti(TOF_L);
      sensor_L.setMeasurementTimingBudget(20000);
      I2Cmulti(TOF_R);
      sensor_R.setMeasurementTimingBudget(20000); 
    } break;
    
  }
  return true;
}

Mit dieser Funktion werden die Sensoren initialisiert. Ihr wird die Sensoreigenschaft (LONGRANGE, HIGHSPEED, HIGH_ACCURACY) übergeben. Sollte ein Sensor nicht ansprechbar sein, wird die passende Info auf den Displays angezeigt.

Diese müssen dafür natürlich vorher initialisiert sein. Die Funktion wird mit einem false verlassen. Man kann hier erkennen, warum es sinnvoll wäre, die Objektinstanzen in ein Array zu packen. Der Code wäre dadurch wesentlich kürzer, weil man es mit Schleifen realisieren kann.

Wurden alle Sensoren erkannt, werden sie mit den entsprechenden Eigenschaften initialisiert und die Funktion mit einem true verlassen.

/////////////////////////////////////
// Ausgabe der Bilder auf dem Display

void showEyes(int eye_L, int eye_R) {
    I2Cmulti(DISPLAY_LEFT);
    u8g2_L.firstPage();
    do {
      u8g2_L.drawXBMP( 0, 0, BREITE, HOEHE, Auge[eye_L]);
    } while ( u8g2_L.nextPage() );
    
    I2Cmulti(DISPLAY_RIGHT);
    u8g2_R.firstPage();
    do {
      u8g2_R.drawXBMP( 0, 0, BREITE, HOEHE, Auge[eye_R]);
    } while ( u8g2_R.nextPage() );  
}

Diese Funktion ist für das Anzeigen der Augen auf dem Display zuständig. Ihr werden zwei Parameter übergeben, die den Nummern im Bilderarray entstprechen. Es ist hier möglich, zwei verschiedene Bilder für die jeweiligen Display zu wählen. Dadurch konnte ich das Zwinkern mit einem Auge realisieren.

Man sieht, dass zuerst der Multiplexer eingestellt und dann die Bildausgabe im Full Screen Buffer Mode durchgeführt wird. Man erkennt das an der do-while-Schleife und der Funktion drawXBMP(). Dieser wird als letzter Parameter das Bild übergeben. Anhand der übergebenen Nummern kann im Bilderarray sehr einfach das passende Bild ausgewählt werden. Die Enumeration ist auch hier wieder sehr nützlich.

Eine Sache hat mich hier stark verwirrt. Obwohl die Displays im Full Buffer Mode initialisiert wurden, sieht die Ausgabe durch die Aufrufe von firstPage() und nextPage() eher wie der Pager Buffer Mode aus. Für die Ausgabe von Text muss das auch anders gemacht werden. Sie sehen das weiter oben in der Fehlerausgabe. Dort wird Text ausgegeben ohne die page-Funktionen aufzurufen. Erzeugt man die Objektinstanzen (siehe oben) im Page Buffer Mode, werden Bilder genauso ausgegeben, wie in diesem Fall die Texte. Sehr verwirrend.

////////////////////////////
// Animation vor dem Standby

void eyesOffAnimation() {
  showEyes(EYE_NORMAL_M, EYE_NORMAL_M);
  delay(500);
  showEyes(EYE_MISSTR_L, EYE_MISSTR_L);
  delay(500);
  showEyes(EYE_MISSTR_M, EYE_MISSTR_M);
  delay(50);
  showEyes(EYE_MISSTR_R, EYE_MISSTR_R);
  delay(500);
  showEyes(EYE_NORMAL_M, EYE_NORMAL_M);
  delay(500);
  showEyes(EYE_NORMAL_M, EYE_CLOSED);
  delay(200);
  showEyes(EYE_NORMAL_M, EYE_NORMAL_M);
  delay(200);
    
  for (int i = EYE_ANIM_1; i <= EYE_ANIM_5; i++) {
    showEyes(i, i);
  }
  I2Cmulti(DISPLAY_LEFT);
  u8g2_L.clearDisplay();
  
  I2Cmulti(DISPLAY_RIGHT);
  u8g2_R.clearDisplay();
}

Die Animation der Augen, bevor in den Standby-Modus gewechselt wird, habe ich in diese Funktion geschrieben. Durch die delay()-Aufrufe blockiert sie natürlich. Das könnte man ändern, war für mich in dem Fall nicht notwendig. Auch wenn in dem kurzen Zeitraum die Sensoren außer Gefecht sind.

Es wird hier in verschiedenen Abständen die Funktion für die Displayausgabe aufgerufen. Da sich die Bilder der Animation für das Ausschalten im Bilderarray hintereinander befinden, konnte ich hier mit einer Schleife durchlaufen. Am Ende werden die Displays gelöscht.

Möchten Sie eigene Animationen unabhängig von diesem Projekt erzeugen, sehen Sie, wie sie mehrere Bilder zu einer Animation zusammenfügen können. In der Bibliothek OneBitDisplay ist der Beispielsketch oledanimationdemo dabei, der das auch nochmal gut zeigt.

Der Wechsel zwischen den beiden Displays ist leider etwas verzögert, fällt aber nicht ganz so schlimm ins Gewicht.

/////////////////////////////////////////////////
// Wenn kein Objekt mehr erkannt wird,
// wird bis zum Standby eine Animation abgespielt
// aus zufaellig ausgewaehlten Bildern

void idleAnimation() {
  int randomBitmap = 0;
  int randomBitmap_old = 0;
  while (randomBitmap == randomBitmap_old) {
    randomBitmap_old = randomBitmap;
    randomBitmap = (int)random(6, 14);
  }
  I2Cmulti(DISPLAY_LEFT);
  showEyes(randomBitmap, randomBitmap);
  I2Cmulti(DISPLAY_RIGHT);
  showEyes(randomBitmap, randomBitmap);
}

Entfernt sich eine Person aus dem Scanbereich der Sensoren, sollte das Display nicht einfach gelöscht werden oder das Bild stehen bleiben. Der Kürbis soll aussehen, als würde er sich umsehen, wo die Person geblieben ist. Daher habe ich hier den Zufallsgenerator eingesetzt, der willkürlich eins der Bilder auf den Displays anzeigt. In welchen Abständen diese Bilder angezeigt werden, wird im IDLESTATE der Zustandsmaschine bestimmt.

void setup() {
  // Strom sparen?
  WiFi.mode( WIFI_OFF );
#if defined(ESP8266)
  WiFi.forceSleepBegin();
#elif defined(ESP32)
  btStop();
#endif
  
  // I2C initialisieren
  Wire.begin();

  // Displays initialisieren
  // vor init ToF, um Fehler auf den Displays anzeigen zu koennen
  I2Cmulti(DISPLAY_LEFT);
  u8g2_L.begin();
  
  I2Cmulti(DISPLAY_RIGHT);
  u8g2_R.begin();
  
  // ToF Sensoren initialisieren
  if (!initTof(SENSORPROPERTY)) {                   //defined HIGH_SPEED, LONG_RANGE, HIGH_ACCURACY)
    while (1) {
      yield();                                      // Zeit fuer Background Tasks, sonst Absturz
    }
  }

  // Farben umkehren fuer Bilderanzeige
  I2Cmulti(DISPLAY_LEFT);
  u8g2_L.setDrawColor(0);

  I2Cmulti(DISPLAY_RIGHT);
  u8g2_R.setDrawColor(0);

  // in den defines ist es moeglich, eigene Werte fuer den
  // maximalen Erkennungsabstand einzustellen
  #ifdef MANUALSENSORMAX                            // Die Erkennungsgrenze manuell setzen
    sensorValueMax = MANUALSENSORMAX;
  #else
    if (SENSORPROPERTY == LONG_RANGE) {             // Erkennungsgrenze abhaengig vom Modus der Sensoren setzen
      sensorValueMax = 2000;
    }
    else {
      // HIGH_SPEED und HIGH_ACCURACY
      sensorValueMax = 1200;
    }
  #endif

  // Skalierung fuer die Abstandsbereiche:
  // weit entfernt
  // nah
  // zu nah
  // Aus maximalem Erkennungsabstand berechnen
  sensorValueCloserMax = sensorValueMax / scalerCloser;
  sensorValueToCloseMax = sensorValueMax / scalerToClose;

  // Ein Mensch blinzelt alle 4 bis 6 Sekunden https://de.wikipedia.org/wiki/Lidschlag
  blinzelTimer = millis();
  blinzelDelay = (unsigned long)random(4000, 6000);

  // Zeitabstand zwischen den Bildern waehrend kein Objekt erkannt wird
  idleAnimationDelay = (unsigned long)random(400, 1500);
  standbyTimer = millis();
}

Im setup() werden zuerst Wifi und Bluetooth abgeschaltet. Abhängig vom verwendeten ESP. Dann wird die I²C-Schnittstelle initialisiert, danach die beiden Displays und anschließend die Sensoren. Sollte einer der Sensoren nicht ansprechbar sein, wird ein false zurückgegeben und eine Endlosschleife ausgeführt. Hier sehen Sie den Aufruf yield(). Er ist dafür da, um Background Tasks Rechenzeit zu geben. Macht man das nicht, kann das zu Bootschleifen führen. Diese traten bei mir häufig auf.

Als Nächstes musste ich noch die Farben des Displays umkehren. Macht man das vor der Textausgabe, werden die Texte nicht angezeigt. Macht man es nicht, sind die Bilder invertiert.

Danach werden die Distanzgrenzen der Sensoren eingestellt. Hier greifen die „defines“ vom Anfang des Quellcodes. Es folgen dann die Berechnungen für die drei Distanzstufen, die jeweils andere Bilder anzeigen, was ich mit den Skalierern umgesetzt habe.

Als Letztes werden dann die Timer gestartet und die ersten Zufallszahlen erzeugt.

// timer für Blinzeln
  // Bedingungen sind: nur wenn nicht im Standby und das Timerintervall ueberschritten wurde
  if (!isStandby && millis() - blinzelTimer >= blinzelDelay) {
    tempState = state;
    state = BLINZELN;
  }
  
  // timer für Standby
  // wird ausgeloest, wenn der Timer dafuer abgelaufen ist
  // Bedingungen sind: wenn nicht schon im Standby und gerade nicht geblinzelt wird
  // und wenn der Timerwert groesser, als das angegebene Intervall ist
  else if (!isStandby && state < BLINZELN && millis() - standbyTimer >= standbyDelay) {
    state = TOSTANDBY;
  }

In der loop()-Funktion wird als Erstes kontrolliert, ob geblinzelt oder in den Standby-Modus gewechselt werden soll. Das ist jeweils abhängig von mehreren Bedingungen. Die Zustandsmaschine wird dann auf die jeweiligen Zustände umgestellt, bevor sie "betreten" wird.

 ////////////////////////
  // STATEMACHINE
  switch (state) {
  
    case IDLESTATE: {
      if (isStandby) {
        // Displays bleiben dunkel
      }
      // wenn nicht im Standby, wird hier die Animation abegspielt,
      // wenn kein Objekt erkannt wurde
      else {
        if (millis() - idleAnimationTimer >= idleAnimationDelay) {
          idleAnimationDelay = (unsigned long)random(400, 1500);
          idleAnimationTimer = millis();
          idleAnimation();
        }
      }
      // Wenn ein Objekt unterhalb der Erkennungsgrenze an einem der
      // Sensoren erkannt wird:
      I2Cmulti(TOF_M);
      if (sensor_M.readRangeSingleMillimeters() < sensorValueMax) {
        standbyTimer = millis();
        isStandby = false;
        state = SENSOR_M;
      }
      I2Cmulti(TOF_L);
      if (sensor_L.readRangeSingleMillimeters() < sensorValueMax) {
        standbyTimer = millis();
        isStandby = false;
        state = SENSOR_L;
      }
      I2Cmulti(TOF_R);
      if (sensor_R.readRangeSingleMillimeters() < sensorValueMax) {
        standbyTimer = millis();
        isStandby = false;
        state = SENSOR_R;
      }
    } break;

Der erste Zustand ist der IDLESTATE. Hier wird nochmal unterschieden zwischen Standby, also Display aus und gerade noch Sensorwerte empfangen, nun aber nicht mehr. Also wird hier die Animation dafür aufgerufen in zufälligen Abständen. Das alles darf nicht blockieren, weil hier auch die Sensoren geprüft werden, ob sich jemand dem Kürbis nähert. Wenn ja, wird zum jeweiligen Sensor der dazugehörige Zustand eingestellt.

// Je nachdem, welcher Sensor vorher ein Objekt erkannt hat, wird hier in drei Abstandsstufen unterteilt.
    // Es erden die dazugehoerigen Bilder angezeigt:
    case SENSOR_M: {
      I2Cmulti(TOF_M);
      if (sensor_M.readRangeSingleMillimeters() < sensorValueMax && sensor_M.readRangeSingleMillimeters() > sensorValueCloserMax) {
        showEyes(EYE_MISSTR_M, EYE_MISSTR_M);
        standbyTimer = millis();
      }
      else if (sensor_M.readRangeSingleMillimeters() <= sensorValueCloserMax && sensor_M.readRangeSingleMillimeters() > sensorValueToCloseMax) {
        showEyes(EYE_NORMAL_M, EYE_NORMAL_M);
        standbyTimer = millis();
      }
      else if (sensor_M.readRangeSingleMillimeters() <= sensorValueToCloseMax) {
        showEyes(EYE_ERSCHR_M, EYE_ERSCHR_M);
        standbyTimer = millis();
      }
      else {
        idleAnimationTimer = millis();
        state = IDLESTATE;
      }
    } break;

Jeder Sensor hat seinen eigenen state. Hier wird dann kontrolliert, in welchem der drei Distanzbereiche sich die Person befindet. Ich habe die Sensoren hier getrennt, um zu verhindern, dass die Augen hin- und her springen, wenn einer der anderen Sensoren auch ein sich näherndes Objekt erkennt.

 // Hier wird kurz geblinzelt und anschliessend zum vorherigen Zustand zurueckgekehrt.
    case BLINZELN: {
      showEyes(EYE_CLOSED, EYE_CLOSED);
      delay(50);                                                    // darf hier mal blockieren
      blinzelDelay = (unsigned long)random(4000, 6000);
      blinzelTimer = millis();
      state = tempState;
    } break;

Dieser Zustand wird durch das Kommando weiter oben außerhalb der switch-case-Anweisung eingestellt. es wird das Bild mit den geschlossenen Augen eingestellt und kurz verzögert. Ich habe das hier blockierend umgesetzt. Auch hier kann man mit millis() auf nicht-blockierend umstellen. Für mich war das an dieser Stelle noch ok.

// Die Standbyanimation wird abgespielt und mit dem entsprechenden Flag in den
// Idlezustand gewechselt
case TOSTANDBY: {
  eyesOffAnimation();
  isStandby = true;
  state = IDLESTATE;
} break;

Auch dieser Zustand wird von außerhalb der Statemachine eingestellt. Es wird die Animation für den Standby-modus abgespielt, der Standby-Flag gesetzt und der Zustand gewechselt. So wird im IDLESTATE nichts auf den Displays angezeigt. Am Ende der loop() finden sie noch einmal den yield()-Aufruf.

Und hier wie versprochen der Download des kompletten Programms. Bitte alle drei Dateien im gleichen Verzeichnis abspeichern.

Bastelarbeit

Für einen schnell anzufertigenden Prototypen eignet sich Pappe hervorragend. Man kann schnell sehen, wie es aussehen wird. Auf diese Weise kann die Elektronik schon mal getestet werden.


Sieht ein bisschen aus, wie ein gewisser sprechender Koffer aus dem Fernsehen. Wenn das alles passt, kann man sich dem Kürbis widmen.

Im Internet finden Sie viele Anleitungen, wie Sie aus einem Kürbis einen Halloweenkopf schnitzen. Zu Beginn schneidet man die Oberseite mit einem Zackenmuster auf und erhält so einen Deckel. Dann muss man den Kopf aushöhlen. Ich habe dann mit einem weißen Wachstift den Mund und die Augen vorgemalt. Dabei habe ich mich an die Größe der Displays gehalten.

Für die Sensoren einen guten Platz zu finden, war nicht ganz so leicht. Ich habe mich für eine Art Sockel entschieden. Damit man im Fehlerfall leichter an die Sensoren herankommt, habe ich Pappstreifen geschnitten und zu einem Zylinder geformt. Auf der Rückseite habe ich sie dann miteinander verbunden, nachdem ich die Sensoren auf der Rückseite des Pappstreifen befestigt hatte. Vorher natürlich noch kleine Löcher in die Pappe schneiden, damit die Laserstrahlen ihren Weg finden. Für einen stabileren Halt habe ich mehrere Pappstreifen übereinandergelegt.


Wie sie sehen, habe ich die Sensoren mit kleinen Schrauben von innen befestigt. Ich wollte nicht, dass man die Platinen sieht. Dafür sieht man nun die Schrauben. Es sieht ein wenig aus wie Stacheln, was auch wieder zu Halloween passt.

Nachdem der Kürbis ausgeräumt ist und die Löcher für Augen und Mund herausgeschnitten sind, kann die Technik eingesetzt werden. Denken Sie bitte daran, dass der Kürbis noch feucht ist. Ich habe die Platinen und die Displays mit Frischhaltefolien geschützt, damit kein Kurzschluss entsteht. Man kann die Displays auch mit kleinen Schrauben vorsichtig von innen befestigen.

Den Mund könnte man noch mit Pergamentpapier abdecken. Ich habe noch keine Beleuchtung eingesetzt. Das würde ich noch nachholen. Der Mikrocontroller wird eventuell nicht mehr in der Lage sein, den Strom für LEDs aufzubringen. Man könnte am Ausgang des Step-up-Konverters hinter dem Schalter eine LED mit Widerstand anbringen. Diese leuchtet dann, wenn man die Technik einschaltet.


Wenn alles verbaut wurde, kann man nun den Deckel aufsetzen und den Kürbis an einer Stelle platzieren, an der öfter Personen vorbeilaufen. Die Augen sollten der Bewegung grob folgen können. Der Alterungsprozess meines Hokkaidos ließ sich während der Entwicklung leider nicht aufhalten. Ich empfehle Ihnen, eine andere Sorte Kürbis zu verwenden, damit er etwas länger durchhält.

Zum Abschluss

Es ist möglich, dass der Mikrocontroller in einer Bootschleife hängen bleibt. Das erkennen Sie daran, dass die Startanimation der Augen nicht angezeigt wird. Zu den Ursachen gibt es keine richtigen Informationen. Sehr wahrscheinlich ist, dass die Stromquelle nicht ausreicht. Eine Möglichkeit ist auch, dass die Boardeinstellungen nicht korrekt sind. Dafür gibt es in den Beispielen des ESP8266 den Test-Sketch CheckFlashConfig, der Informationen dazu anzeigt.

Für den ESP32 gibt es den Sketch ResetReason, mit dem man ebenfalls Fehler finden kann. Eventuell reicht es, den Stromanschluss nach dem Upload des Programms noch einmal zu trennen und wieder anzuschließen. Im Programmcode sehen Sie den Aufruf yield(). Es ist wichtig, den Background Tasks Rechenzeit zu geben. Auch das kann eine Fehlerquelle sein.

Wie Sie eventuell sehen werden, ist der Bildaufbau der beiden Displays ganz leicht verzögert zueinander. Das liegt an der Taktfrequenz des I²C-Bus der ESPs und des Multiplexers, die leider nicht weiter beschleunigt werden kann.

Eine Innenbeleuchtung sollte noch ergänzt werden. Wenn der Strom noch ausreicht, könnte man den DFPlayer einsetzen, um Geräusche zu erzeugen.

Sollten Sie Probleme haben, schreiben Sie es in die Kommentare. Ich würde mich auch über kreative Änderungen und Erweiterungen freuen.


Viel Spaß beim Basteln.


Andreas Wolter

für AZ-Delivery Blog
 

Hier geht's zum Download des Blog-Beitrags.

DisplaysEsp-32Esp-8266SensorenSpecials

4 Kommentare

Gerhard

Gerhard

Lieb! Es wäre schön ein Video zu sehen!

Bekannt

Bekannt

Tolle Umsetzung… Das Projekt wird nachgebastelt.

Alex

Alex

Naja,
das der Beitrag zum realisieren eine Woche zu spät ist, macht doch nichts.
Man kann bei diesem Beitrag wieder eine Menge dazulernen und sich mit neuen Koponenten
anfreunden, insofern man sie noch nicht hat.
Um dem Projekt evtl. noch eine Soundausgabe hinzuzufügen, so kann man ja
seine eigene Kreativität spielen lassen. :-)

Willem

Willem

Hallo,
Ein wunderschöner Beitrag, nur leider eine Woche zu spät um ihn zu realisieren. Teile bestellen usw, schade, aber für nächstes Jahr vorgemerkt. ;-))

Einen Kommentar hinterlassen

Alle Kommentare werden vor der Veröffentlichung moderiert

Empfohlene Blogbeiträge

  1. ESP32 jetzt über den Boardverwalter installieren
  2. Lüftersteuerung Raspberry Pi
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1
  4. ESP32 - das Multitalent
  5. OTA - Over the Air - ESP Programmieren über WLAN