Robo-Eyes

Ziel dieses Projekts ist es, Augen auf dem Display anzeigen zu lassen und der senseBox damit etwas Persönlichkeit zu geben. Die Augen sollen dabei mit der Blickrichtung dem Finger folgen, was wir mit Hilfe des ToF-Distanzsensors umsetzen. Die Augen können auch blinzeln, lachen, ihre Form und ihre Stimmung verändern. Probieren wir’s aus!

Aufbau

Schließe das Display mit einem der QWIIC-Kabel an einen I2C-Port an. Verbinde auf die gleiche Weise den ToF-Sensor mit dem anderen I2C-Port. Achte darauf, dass Sensor und Display in die gleiche Richtung zeigen und möglichst nah beieinader sind. Außerdem sollte beim ToF-Sensor die Seite mit der Beschriftung “VL53L8CX” nach oben zeigen.

/images/projects/RoboEyes/aufbau.png - Logo

Programmierung

Nutze entweder den Code Editor von senseBox-Blockly oder die Arduino IDE, um die Programmierung für dieses Projekt durchzuführen. Zuerst programmieren wir im Teil “Hello Worl…Robot!”, dass die Robo-Augen überhaupt auf dem Display der senseBox angezeigt werden und dann in unregelmäßigen Abständen blinzeln. Anschließend nutzen wir die Messdaten des ToF-Distanzsensors, damit die Augen der Position des Fingers folgen. Im dritten Teil programmieren wir dann, dass sich die Stimmung der Augen je nach Distanz verändert. Los geht’s!

Schritt 1: Hello Worl…Robot!

Libraries importieren, Variablen erstellen

Zuallererst brauchen wir für das Projekt die passenden Libraries (= Software Bibliotheken). Wenn du im Code Editor von Blockly arbeitest, sind diese bereits vorinstalliert. In der Arduino IDE musst du sie importieren. (Du weißt noch nicht, was Libraries sind und wie du sie importieren kannst? Kein Problem! Hier findest du weitere Informationen: Hinzufügen einer Arduino Software Bibliothek)

Um den ToF-Distanzsensor einfach einbinden zu können, nutzen wir die Software-Bibliothek “STM32duino VL53L8CX” (von STMicroelectronics). Für das Display nutzen wir die Library “Adafruit_SSD1306” (von Adafruit). Installiere beide über den Library Manager der Arduino IDE.

Um diese Libraries dann im Code verwenden zu können, müssen wir sie (auch im Blockly Code Editor) noch entsprechend einbinden:

#include <Wire.h>
#include <Adafruit_GFX.h> // http://librarymanager/All#Adafruit_GFX_Library
#include <Adafruit_SSD1306.h> // http://librarymanager/All#Adafruit_SSD1306
#include <vl53l8cx.h>

Zur Verwendung des Sensors und des Displays müssen wir diese außerdem noch einrichten, unter anderem durch die Definition der Höhe und Breite des Displays:

VL53L8CX sensor_vl53l8cx(&Wire, -1, -1);

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Für die Augen verwenden wir die Library RoboEyes (von FluxGarage). Diese muss für die Arduino IDE heruntergeladen und manuell installiert werden: Downloadseite RoboEyes, im Code-Editor von Blockly ist sie bereits vorinstalliert. Die Library binden wir ebenfalls mit Hilfe des “#include”-Befehls ein und kreieren dann eine Instanz, um diese zu verwenden:

#include <FluxGarage_RoboEyes.h>
roboEyes roboEyes; // create RoboEyes instance

Setup und Startup

In der setup()-Funktion starten wir den ToF-Sensor, das Display und die Robo-Augen und definieren unter anderem die Frequenz, Auflösung und Datenrate. (Da diese Funktionen nicht spezifisch für dieses Projekt sind, gehen wir hier nicht ins Detail. Wenn du mehr Informationen dazu möchtest, hilft zum Beispiel die Arduino Dokumentation weiter.)

void setup() {
  Serial.begin(9600);

  delay(1000);

  Wire.begin();
  Wire.setClock(1000000); //Sensor hat max I2C freq von 1MHz
  sensor_vl53l8cx.begin();
  sensor_vl53l8cx.init();
  sensor_vl53l8cx.set_ranging_frequency_hz(30);
  sensor_vl53l8cx.set_resolution(VL53L8CX_RESOLUTION_8X8);
  sensor_vl53l8cx.start_ranging();
  Wire.setClock(100000); //Sensor hat max I2C freq von 1MHz

  // Startup OLED Display
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3D)) { // Address 0x3C or 0x3D
    Serial.println(F("SSD1306 allocation failed"));
    for(;;); // Don't proceed, loop forever
  }

  // Startup robo eyes
  roboEyes.begin(SCREEN_WIDTH, SCREEN_HEIGHT, 100); // Bildschirmweite, Bildschirmhöhe, maximale Framerate

Automatische Augenbewegungen

Jetzt fehlt nur noch eins, um unsere Robo-Augen zu testen: In der Endlosschleife definieren wir, dass in jedem Durchlauf die Art und Position der Augen auf dem Display aktualisiert werden soll:

void loop() {
  roboEyes.update(); // Aktualisieren der Augen
}

Lade den Sketch auf deine senseBox: Die Augen sollten jetzt angezeigt werden.

Die Augen wirken jetzt noch “leblos”, da sie sich im Laufe des Programms nicht verändern. Die RoboEyes-Library bietet zahlreiche Möglichkeiten, um das zu verändern. Wir starten damit, die Augen blinzeln zu lassen. Dafür ergänzen wir in der Funktion setup() einfach den “Autoblinker”:

  roboEyes.setAutoblinker(ON, 3, 2); // Blinzelt in einem Intervall von (3) Sekunden mit einer Variation von (2) Sekunden

Lade den Sketch auf deine senseBox und probiere es aus!

Schritt 2: Augen folgen der Bewegung

Jetzt wollen wir, dass die Augen unserem Finger folgen. Dafür nutzen wir den ToF-Sensor. Dieser kann nicht nur eine Distanz, sondern 8x8 Distanzen in einem Quadrat messen. Die Augen sollen immer da hin schauen, wo das nächste Objekt (oder eben der Finger) ist. Wir messen also immer, welche der 64 Distanzen die geringste ist und in diese Richtung sollen die Augen sich dann bewegen.

Intervall

Um das umzusetzen, soll in der loop() Funktion regelmäßig die Distanz gemessen und die Position der Augen passend aktualisiert werden. Damit sie sich gleichmäßig bewegen und nicht in jedem Frame aktualisiert werden, definieren wir ein Intervall von 75 Millisekunden. Dafür wird nicht die delay() Funktion aus der Standard Arduino Library benutzt, sondern wir definieren unser eigenes Messintervall. Die delay() Funktion sorgt dafür, dass der Programmcode komplett unterbrochen wird, durch ein eigenes definiertes Messintervall lässt sich dies besser lösen. Dafür werden 3 zusätzliche Variablen am Anfang unseres Programms, noch vor dem setup() und der loop() Funktion, definiert. Die Variablen speichern die Zeit seit dem Start des Intervalls, die Länge des Intervalls und die aktuelle Laufzeit des Programmes.

long update_interval = 75; // Intervall von 75 Millisekunden
long start_time = 0;
long actual_time = 0;

Die Arduino Funktion millis() gibt die Anzahl an Millisekunden zurück, seitdem die senseBox gestartet ist. Mithilfe dieser Funktion kann eine if-Abfrage erstellt werden, welche gültig wird, wenn 75 Millisekunden vergangen sind.

void loop() {
  start_time = millis();
  if (start_time > actual_time + update_interval) {
    // 75 Millisekunden sind vergangen
    actual_time = millis();
  }
  roboEyes.update(); // Aktualisieren der Augen
}

Distanzen mit dem ToF-Sensor messen

Innerhalb dieses Intervalls wollen wir jetzt die Distanzen messen und dabei die niedrigste finden. Dafür passen wir die Funktion, die sich in der ToF-Anleitung befindet, für unseren Fall an. Wichtig ist nämlich, dass wir neben der geringsten Distanz (min_distance) auch die Position des Pixels mit der geringsten Distanz (min_index) speichern. Dafür lassen wir die Daten auslesen und als ResultsData speichern. Anschließend gehen wir die Daten in einer for-Schleife durch, um die geringste Distanz zu finden und zu speichern:

VL53L8CX_ResultsData Result;
uint8_t NewDataReady = 0;
uint8_t status;

Wire.setClock(1000000); //Sensor has max I2C freq of 1MHz
status = sensor_vl53l8cx.check_data_ready(&NewDataReady);
Wire.setClock(100000); //Sensor has max I2C freq of 1MHz

if ((!status) && (NewDataReady != 0)) {
  Wire.setClock(1000000); //Sensor has max I2C freq of 1MHz
  sensor_vl53l8cx.get_ranging_data(&Result);
  Wire.setClock(100000); //Sensor has max I2C freq of 1MHz
  // Finden des Pixels mit der geringsten Distanz
  int min_index = 0;
  uint16_t min_distance = (long)(&Result)->distance_mm[0];
  for (int i = 1; i < 64; i++) {
	if ((long)(&Result)->distance_mm[i] < min_distance) {
	  min_distance = (long)(&Result)->distance_mm[i];
	  min_index = i;
	}
  }

Richtung der RoboEyes anpassen

Bei den RoboEyes wird die Position in abgekürzten Himmelsrichtungen angegeben, wobei “DEFAULT” die Mitte beschreibt. Die Richtungen sind also: N, NE, NW, W, E, S, SE, SW. Im vorherigen Schritt haben wir den Index der niedrigsten Distanz herausgefunden. Wie können wir diesen jetzt in eine der Richtungen übersetzen?

Dafür kann es helfen, sich die Ergebnisse einmal bildlich vorzustellen. Wir haben mit dem Index eine Zahl, die einen Wert in einem 8x8-Feld repräsentiert. Wir müssen uns entscheiden, wie wir das Feld aufteilen, also wo z.B. N(orden) ist. Da sich 8 nicht durch 3 teilen lässt, kann das nicht ganz gleichmäßig passieren. Damit die Augen möglichst wenig in der Mitte bleiben, wollen wir die äußeren Enden größer definieren. Hier eine farblich markierte Aufteilung des 8x8-Felds, bei dem z.B. rot für W(est) und grün für E(ast) steht.

/images/projects/RoboEyes/tof-sensor_grid-overview.png - Logo

Eine Möglichkeit, den Index in eine Himmelsrichtung zu übersetzen, ist über die Zeile und Spalte. Mit einer Kombination aus Zeile und Spalte lässt sich eindeutig herausfinden, in welches Feld die Augen sich bewegen sollen. Die Zeile berechnen wir, indem wir den Index durch 8 teilen. Die Spalte erhalten wir mit dem Rest, der beim Teilen durch 8 herauskommt. Das lässt sich in vielen Programmiersprachen einfach mit “%” berechnen, sieht also so aus:

    int row = min_index / 8;
    int col = min_index % 8;

Die Blickrichtung können wir mit einer Reihen an if-else-Abfragen aus der Zeile und Spalte übersetzen (Infos zu “wenn-dann”-Bedingungen findest du auf der Lernkarte GI02). Außerdem wollen wir, dass die Augen nur der Bewegung folgen, wenn der Finger nah genug dran ist. Also stellen wir noch die Bedingung, dass die Entfernung unter 400mm betragen soll. Probiere selbst einmal, die if-else-Abfragen zu definieren! Anschließend kannst du diese mit unserem Vorschlag vergleichen:

	unsigned char direction;

	if (row == 3 || row == 4) {
	  if (col == 3 || col == 4) {
		  direction = DEFAULT;
	  } else if (col < 3) {
		  direction = W;
	  } else {
		  direction = E;
	  }
	} else if (row < 3) {
	  if (col == 3 || col == 4) {
		  direction = S;
	  } else if (col < 3) {
		  direction = SW;
	  } else {
		  direction = SE;
	  }
	} else {
	  if (col == 3 || col == 4) {
		  direction = N;
	  } else if (col < 3) {
		  direction = NW;
	  } else {
		  direction = NE;
	  }
	}

Schließlich teilen wir den Augen mit roboEyes.setPosition() die neue Blickrichtung mit, sodass diese mit roboEyes.update();am Ende der Schleife richtig aktualisiert werden. Der gesamte Code nach dem setup() sieht jetzt also so aus:

void loop() {

  start_time = millis();
  if (start_time > actual_time + update_interval) {
    actual_time = millis();
    VL53L8CX_ResultsData Result;
    uint8_t NewDataReady = 0;
    uint8_t status;

    Wire.setClock(1000000); //Sensor has max I2C freq of 1MHz
    status = sensor_vl53l8cx.check_data_ready(&NewDataReady);
    Wire.setClock(100000); //Sensor has max I2C freq of 1MHz

    if ((!status) && (NewDataReady != 0)) {
      Wire.setClock(1000000); //Sensor has max I2C freq of 1MHz
      sensor_vl53l8cx.get_ranging_data(&Result);
      Wire.setClock(100000); //Sensor has max I2C freq of 1MHz
      int min_index = 0;
      uint16_t min_distance = (long)(&Result)->distance_mm[0];
      // Finden des Pixels mit der geringsten Distanz
      for (int i = 1; i < 64; i++) {
        if ((long)(&Result)->distance_mm[i] < min_distance) {
          min_distance = (long)(&Result)->distance_mm[i];
          min_index = i;
        }
      }

      if (min_distance < 400) {
        int row = min_index / 8;
        int col = min_index % 8;

        // Herausfinden der Blickrichtung
        unsigned char direction;

        if (row == 3 || row == 4) {
          if (col == 3 || col == 4) {
              direction = DEFAULT;
          } else if (col < 3) {
              direction = W;
          } else {
              direction = E;
          }
        } else if (row < 3) {
          if (col == 3 || col == 4) {
              direction = S;
          } else if (col < 3) {
              direction = SW;
          } else {
              direction = SE;
          }
        } else {
          if (col == 3 || col == 4) {
              direction = N;
          } else if (col < 3) {
              direction = NW;
          } else {
              direction = NE;
          }
        }
        
        roboEyes.setPosition(direction);

      } 
    }
  }
  
  roboEyes.update(); // Aktualisieren der Augen

}

Übertrage den Code auf deine senseBox und probiere es aus!

Schritt 3: Stimmung der RoboEyes verändern

Die Stimmung der RoboEyes kann mit der Funktion roboEyes.setMood() verändert werden. Das können wir auf verschiedene Weise in unserem Programm einbauen.

Eine einfach umzusetzende Möglichkeit ist, dass die RoboEyes ihre Stimmung immer direkt anhand der gemessenen Distanz verändern. Das können wir mit if-else-Statements umsetzen. “Sehen” die Augen etwas in einer Entfernung von maximal 120mm, sind sie glücklich, in einer Entfernung von maximal 400mm neutral, und ansonsten müde:

//Stimmung je nach gemessener Distanz festlegen
if (min_distance < 120) {
	roboEyes.setMood(HAPPY);
} else if (min_distance < 400) {
	roboEyes.setMood(DEFAULT);
} else {
	roboEyes.setMood(TIRED);

Achte dabei darauf, dass du diese Codezeilen noch innerhalb der Statusabfrage if ((!status) && (NewDataReady != 0)) { platzierst, damit die Variablen min_distance und min_index an dieser Stelle ausgelesen werden können. Probier es dann direkt aus!

Extras & Weitere Ideen

Die Software-Bibliothek RoboEyes bietet noch einige weitere Funktionen. Zuallererst kannst du selbst definieren, wie die Augen deiner senseBox aussehen sollen. Alle Werte werden dabei in Pixeln angegeben. Zur Erinnerung: Das Display der senseBox ist 128x64 Pixel groß. Hier die Funktionen, mit denen du die Form der Augen verändern kannst:

  roboEyes.setWidth(36, 36); // byte linkesAuge, byte rechtesAuge
  roboEyes.setHeight(36, 36); // byte linkesAuge, byte rechtesAuge
  roboEyes.setBorderradius(8, 8); // byte linkesAuge, byte rechtesAuge
  roboEyes.setSpacebetween(10); // int PlatzZwischenAugen -> kann auch negativ sein

Außerdem gibt es weitere Funktionen, unter anderem Animationen, die abgespielt werden können, oder den Idle-Modus:

  roboEyes.anim_confused(); // verwirrt - Augen bewegen sich nach links und rechts
  roboEyes.anim_laugh(); // lachen - Augen bewegen sich nach oben und unten
  roboEyes.setIdleMode(ON, 2, 2); // Starten der Idle-Animation (Augen schauen in zufällige Richtungen) -> ON/OFF: An-/Ausschalten, Zahl1: Intervall in Sekunden, in dem sich Augenposition verändern, Zahl2: Variation beim Intervall in Sekunden
  

Hier einige Ideen, was du als nächstes ausprobieren kannst:

  1. Mit einem Klick auf den Knopf der senseBox fangen die Augen mit roboEyes.anim_laugh() an zu lachen.
  2. Hat die senseBox eine Weile nichts gesehen (Distanz unter 400mm), geht sie mit roboEyes.setIdleMode()in den “Idle-Modus”. In diesem Modus ändert sie automatisch immer wieder ihre Blickrichtung, sie schaut sich quasi enstpannt um.
  3. Ändert sich die Distanz und damit die Stimmung der Augen sehr oft hintereinander, sind die Augen mit roboEyes.anim_confused() kurz verwirrt.

Werde auch gerne selbst kreativ und stelle die Augen so ein, dass sie gut zu dir und deiner senseBox passen. Viel Spaß!

Gesamter Code

Den gesamten Code findest du hier: Projekt-Code auf Github