Humansimonsays

Aus toolbox_interaktion
Wechseln zu: Navigation, Suche
Simon

HumanSimonSays ist eine Anlehnung an das Spiel Simon, welches sich besonders in den 1980er Jahren großer Beliebtheit erfreute.

Idee

HumanSimonSays besteht aus vier großen Feldern in den Farben Rot, Blau, Gelb und Grün. Diese leuchten abwechselnd auf. Der Spieler muss sich diese Reihenfolge merken und nach Abschluss der Vorgabe durch das Spiel durch Positionieren seines Körpers wiederholen. Mit jeder Runde kommt eine weitere Farb-Ton-Kombination hinzu. Mit steigendem Schwierigkeitsgrad leuchten die Felder in schnellerer Reihenfolge auf. Ein kleines Demovideo: [1]

Vorbereitung

Schaltplan

- Einarbeitung in die Software Eyesweb

- Einarbeitung in die Arduinoprogrammierung

- Überlegungen zur Realisierung der Hardware

- Verknüpfung dieser einzelnen Komponenten miteinander, dass ein funktionsfähiges System entsteht

Hardware

Spielfläche

Im ersten Versuch, wollte wir als Bodenplatten, Acrylglasflächen benutzen und diese mit LED-Stripes beleuchten lassen. Da dieser Aufbau aber zu Kosten- und Zeitaufwendig geworden wäre, haben wir auf ganz normale Holzplatten zurückgegriffen und haben diese an den Vorderseiten mit aufgeklebten LED-Stripes beleuchtet.

Eine Verstärkerschaltung zwischen dem Arduino-Board und den LED-Stripes wurde mit Hilfe einer MOSFET-Schaltung realisiert.

Software

Eyesweb

Die Verarbeitung in Eyesweb ist in einzelne Blöcke unterteilt. HumanSimon EyesWeb.jpg

Hintergrundsubstraktion

Eingang: Hintergrundspeichern (Typ: Trigger)

Parameter: Schwellwert für Binarisierung (Typ: Int)

Ausgang: Binarisiertes Bild (Typ: Image), Kontrollausgang vor der Binarisierung (Typ: Image)

HumanSimon Hintergrund.jpg

Die Hintergrundsubstraktion ist auf klassische weise durch einzelne Blöcke realisiert. Es wird das Bild von der Web-Cam eingelesen und in ein Graustufenbild verwandelt. Durch den Triggereingang wird das aktuelle Bild gespeichert. Jetzt wird im Substraktionsblock bei jedem Pixel des Eingangsbildes der Grauwert des gespeicherten Bildes abgezogen. Je nachdem ob man einen hellen oder dunklen Hintergrund hat, müssen die Eingänge am Subtraktionsblock vertauscht werden. Dieses Bild wird zur Kontrolle der Qualität der Hintergrundsubstraktion aus dem Block ausgegeben. Zur weiteren Verarbeitung wird das Bild Binarisiert, dies bedeutet dass es nur noch zwei unterschiedliche grau-Werte im Bild gibt (0,255). Dies erleichtert die weitere Verarbeitung des Bildes. Den Schwellwert für die Binarisierung kann als Parameter an den Block übergeben werden.

Blob-Erkennung

Eingang: Binarisiertes Bild (Typ: Image)

Ausgang: x-Koordinate des Schwerpunktes des größten Blobs (Typ: Double 0.0 - 1.0)

HumanSimon Blob.jpg

Manchmal kommt es vor, dass mehrere Objekte nach der Hintergrundsubstraktion übrig bleiben. (z.B. Fehler der Binarisierung, Vorhang der im Wind flattert, benachbarter Spieler) Die Blob Erkennung sucht nach zusammenhängen Pixel und speichert diese als Blobs. Bei der Bloberkennung kann eingestellt werden, wie groß eine Lücke zwischen zwei Bereichen sein darf, dass es immer noch als ein Blob erkannt wird. Aus diesem Array an Blobs wird im nächsten Modul der größte Blob ausgewählt und anschließend dessen Schwerpunkt berechnet. Der Schwerpunkt liegt als Point 2D double vor. Nach einer Aufspaltung in x- und y-Koordinate wird die x-Koordinate ausgegeben.

Zum anzeigen eines Blobs gibt es die Funktion "Draw Blob". Diese hat als Eingang ein Hintergrundbild und einen Blob. Um den Blob vor einem schwarzen Hintergrund zu malen, einfach als Eingang eine ImageGenerator wählen und "continuous" auf "true" stellen.

Berechnung des Feldes

Eingang: x-Koordinate (Typ: Double)

Ausgang: Feld (Typ: Int)

HumanSimon Field.jpg

Die einzelnen Grenzen der Felder sind als Schwellwerte in diesem Block gespeichert. Die Erkennung funktioniert einfach über 3 Schwellwerterkennungen, die bei größeren Werten 1 ausgeben und bei kleineren 0. Die Ausgänge werden zusammen mit einer Konstanten 1 aufsummiert und ausgegeben.

In-Ruhe Detektion

Eingang: x-Koordinate (Typ: Double)

Ausgang: In-Ruhe (Typ: Bool)

Für die In-Ruhe Detektion haben wir 2 unterschiedliche Herangehensweisen verfolgt. Am Schluss haben wir uns für die fertige Lösung entschieden, weil sie einfach einzustellen ist.

fertige Lösung

HumanSimon InRuhe.jpg

Der fertige In-Ruhe Block hat als Eingang die x- und y-Postition als Int-Wert. Da wir nur die x-Postion haben und diese als Double vorliegt, wird sie zuerst mit 1000 Multipliziert und dann auf den Datentyp Int gewandelt. Auf den Block wird einfach zwei mal die x-Koordinate gegeben. Im Block kann die maximal tolerierte Bewegung und die Stillstandzeit eingestellt werden.

Com-Port

Eingang: Senden (Typ: Bool)

Parameter: Befehl (Typ: Int)

HumanSimon Com.jpg

Der Block Com-Port hat 2 unterschiedliche Betriebsarten. Entweder kontinuierlich senden wenn Senden == 1, oder nur bei jeder Steigenden-Flanke von Senden einmal senden. Dazu muss nur der Schalter durch den Trigger ersetzt werden. Welches Bitmuster gesendet wird, kann über die Constant-Int-Generator(Continuous Output = true) eingestellt werden. Der selector verbindet den Entsprechenden Befehl mit dem Com-Port. Die Übertragungseinstellungen können direkt im Block eingestellt werden. (z.B. Baud = 9600, Carry = No, Daten = 8, Stop = 1) Der richtige Com-Port wird im Eyesweb-Menü unter Tools --> Options eingestellt.

Arduino Code und Funktionsweise der seriellen Datenübertragung

Arduino-uno-perspective-transparent.png

Zur Ansteuerung der Felder/der LEDs wird ein Arduino Uno Board verwendet. Beschaffbar ist das Board unter [[2]] Viele bereits erstellte Projekte und Codebeispiele sind unter dem Reiter "Learning" zu finden. Unter dem Reiter "Reference" sind alle Funktion zu finden die die Programmierumgebung des Arduino unterstützt.

Vorteil des Arduino Boards ist dass man in rein C programmiert und sich nicht um irgendwelchen Microcontroller spezifischen Code kümmern muss.

Die LEDs werden über ein kleines von uns gefertigtes Board angesteuert, das an die entsprechenden digitalen Ausgänge angeschlossen wird, von welchem dann die eigentlichen großen LED-Leisten angeschlossen werden.


Der Code an sich ist in 3 grobe Abschnitte zu unterteilen.

1: Wie bei den meisten MCs die Initialisierung der benötigten Komponenten 2: Die vom MC selbstständig abgearbeitete Logik mit Zufallswerterzeugung 3: Die eigentliche Spielereingabe mit serieller Datenkommunikation und Wertüberprüfung

Es gibt 2 Standart-Funktionen die Arduino vorgibt:

Teil 1 ist in der Funktion setup() Teil 2 und 3 laufen in einer while(1)-Schleife --> Funktion loop()



#define DELAY 400
#define TIMEBASEDDELAY 1000
#define LIGHTDELAY 100
#define MAX_ROUND 20
#define COMPLEXITY 4
#define TIMEBASED
int incoming = 0;
int gamerounds[MAX_ROUND];
int index = 0;
bool gameround = true;
bool gameover = false;
void setup() { 
 //intialize serial port with 9600 baud/s               
 Serial.begin(9600);
 // initialize the digital pins 9 - 12 as an output
 pinMode(9, OUTPUT);
 pinMode(10, OUTPUT);
 pinMode(11, OUTPUT);
 pinMode(8, OUTPUT); 
 pinMode(13, OUTPUT); 
 
 //seeding random function on analogue port(white noise)
 randomSeed(analogRead(0));

}


Listing 1: setup()- Funktion, Variablen und gesetzte #defines


Listing 1 zeigt den Kopf des Programms.

  1. defines:

Es werden mehrere delay-Zeiten (in ms) definiert die später für unterschiedliche Blink - und Warte-Zeiten verwendet werden. Dann wird noch MAX_ROUND definiert (wird im Code nur dafür verwendet um die größe des Arrays fest zu legen das die erzeugten Werte speichert). Es ist keine Sicherheitsabfrage implementiert ob wir über die Grenzen des Arrays hinaus schreiben! COMPLEXITY bestimmt die Reichweite der Werte die per random()-Funktion bestimmt werden.

setup(): In der setup()-Funktion wird zunächst der serielle Port initialisiert und als parameter die gewünschte Übertragungsgeschwindigkeit in Baud/s übergeben. Danach werden die Ports 8-11 (LEDs) und 13(ungenutzt) aktiviert und als Output-Port definiert.

Zum Schluss initialisieren wir die Randomfunktion auf einen Wert der von einem freien analog Port bezogen wird um einen "echten" Zufallswert zu bekommen. Dies wird vor jedem Aufruf der random()-Funktion wiederholt.



void triggerLED(int LEDid, int state) {

 int act = 0;
 
 switch(LEDid)
 {
 case 1: act = 8;
         break;
 case 2: act = 9;
         break;
 case 3: act = 10;
         break;
 case 4: act = 11;
         break;
 }
 
 digitalWrite(act, state);
 

}

void runLightLED(bool reverse) {

 int i = 1,j;
 if(reverse) i = 4;
 
              for(j=0;j<4;j++)
              {
                triggerLED(i, HIGH);
                delay(LIGHTDELAY);
                triggerLED(i, LOW);
                if(reverse) i--;
                else i++;
              }

}

void kitt() {

 runLightLED(false);      //LEDs triggered in row 1-4
 runLightLED(true);       //LEDs triggered in row 4-1
 runLightLED(false);      //LEDs triggered in row 1-4
 runLightLED(true);       //LEDs triggered in row 4-1
 runLightLED(false);      //LEDs triggered in row 1-4
 runLightLED(true);       //LEDs triggered in row 4-1
 runLightLED(false);      //LEDs triggered in row 1-4
 runLightLED(true);       //LEDs triggered in row 4-1
 runLightLED(false);      //LEDs triggered in row 1-4
 runLightLED(true);       //LEDs triggered in row 4-1

}

bool checkLastRandoms() {

 if((index != 0) && (index != 1))           //check if this is first or second gameround
 {
   //compare current round with previous 2 rounds 
   if((gamerounds[index] == gamerounds[index - 1]) && (gamerounds[index] == gamerounds[index - 2]))      
   {
     return true;                           //continue do/while loop in main
   }
   else
   {
     return false;                          //stop do/while loop in main
   }
 }
 return false;                              //first and second round dont matter --> continue
 

}

void flush() {

 int i, count=Serial.available();
 for(i=0;i<count;i++)
 {
   Serial.read();
 } 

}


Listing 2: Unterstützende Funktionen triggerLED, runLightLED(bool), kitt(), checkLastRandoms() und flush()


triggerLED(): Steuert alle 4 LEDs an um eine vereinfachte Handhabung zu bekommen und sie leichter ansteuern zu können.

LEDid steuert die Nummer state kann den Systeminternen Wert HIGH oder LOW annehmen (LEDs sind HIGH aktiv).

runLightLED(bool): Implementiert ein Lauflicht. Der boolsche Wert steuert ob die LEDs nacheinander von 1 -> 4 angesprochen werden(false) oder von 4 -> 1 (true). Die Zeit zwischen der HIGH/LOW-Triggerung wird mit der System-Funktion delay(Zeit in ms) eingestellt. --> Wird jedes Mal beim Wechsel von System- in Usereingabe-Modus an benutzt um dies abtrennen zu können.

kitt(): Nimmt die Funktion runLightLED() und lässt die LEDs mehrere Male hin und her laufen. --> Bildet in unserem Fall die Anzeige dass der User einen Fehler gemacht hat und das Spiel beendet ist

checkLastRandoms(): Überprüft die letzten beiden vorhergehenden Runden ob die Werte übereinstimmen. -->Es darf nicht 3 Mal hinter einander die selbe LED getriggert werden

flush(): Löscht sämtliche Werte im Puffer des seriellen Ports bis zu dem Zeitpunkt wenn die Funktion aufgerufen wird. (Vor Version 1.0 der Arduino-Software konnte dies über die System Funktion Serial.Flush() gemacht werden was jetzt nicht mehr möglich ist) --> Wird verwendet um den Inhalt zu löschen wenn vor der zeitbasierten Aktivierung im Arduino bereits Werte vom PC/Eyesweb gesendet werden



void loop() {

 if(!gameover)
 {
   if (gameround)                          // automatic gameroundcreation
   {
     do
     {
       randomSeed(analogRead(0));          // renew seeding for better randomization
       gamerounds[index] = random(COMPLEXITY)+1;
     }
     while(checkLastRandoms());            //continue creating random values and continue when the value is 
                                           //different to both values of the previous 2 rounds
     
     for(int i = 0; i <= index; i++)
     {
        
        triggerLED(gamerounds[i], HIGH);
        delay(DELAY+400);
        triggerLED(gamerounds[i], LOW);
        delay(DELAY);
          switch(gamerounds[i]
          
          )
         {
         case 1: Serial.print("LED 1\n");
                 break;
         case 2: Serial.print("LED 2\n");
                 break;
         case 3: Serial.print("LED 3\n");
                 break;
         case 4: Serial.print("LED 4\n");
                 break;
         }
        
     }
     index++;
     gameround = false;                     //automatic input 	over switch to userinput
     
   }

Listing 3: loop()-Funktion 1. Teil (Block 2: Zufallswertgenerierung und Ausgabe)


In Listing 3 wird als erstes geprüft ob die boolsche Variable gameover true ist. Wenn dem so ist "stoppt" das Programm. Die nächste if-Abfrage steuert ob gerade eine User-Eingabe stattfindet oder ob ein Zufallswert generiert wird. Die folgende do/while-Schleife generiert einen Wert der, verglichen mit den 2 vorhergenden Spielrunden/Arrayeinträgen, kein drittes Mal vorkommen darf. Dieser wird dann in den aktuellen Arrayplatz eingespeichert der durch die index Variable bestimmt wird die jedes mal hochgezählt wird.

Im Folgenden werden alle Einträge im Array(Spielrunden) ausgegeben über die LEDs, danach folgen Befehle um Statusmeldungen über den seriellen Port zurück zu geben(wurden nur beim debuggen ausgewertet) und zum Schluß gameround false gesetzt um beim nächsten Durchlauf der loop()- Funktion in den User-Eingabe-Teil zu springen.



else      //userinput made available
   {
     for(int i = 0; i < index; i++)        //userinput as long as there is saved data available
     {
       #ifdef TIMEBASED                    //system is waiting a predefined time peroid to delete all buffered data 
                                           //in the serial buffer and continue
                                           //--> next incoming serial data will be taken as relevant data
                                     
       delay(TIMEBASEDDELAY);              //wait
       flush();                            //Delete all data in serial buffer and continue
         
       #endif
       
       while(1)
       {
         runLightLED(false);
           
         if(Serial.available())            //check if serial data is in buffer available
         {
           #ifdef SIGNALBASED              //systems waits for a signal (integer 6 from eyesweb) to proceed 
                                           //--> next signal should be relevant data!
           while(incoming != 6)            //as long as incoming serial data is not integer 6 read on
           {
             if(Serial.available())        //check again wether serial data is available  
             {
               incoming = Serial.read(); 
             }
           }
           #endif
           
           #ifdef TIMEBASED
           
           incoming = Serial.read(); 
           
           #endif
            triggerLED(incoming, HIGH);    //LED on corresponding to data
            delay(DELAY+400);    
            triggerLED(incoming, LOW);     //LED off 
            delay(DELAY);
            
            if(incoming != gamerounds[i])  //check if input data was correct
            {
              Serial.print("wrong\n");    
              
              kitt();                      //make special "false input" sign
              
              i = index + 1;               //set run variable 1 over maximum to jump out of for loop
              gameover = true;             //stop whole game (reset only possbile over hardware reset)
            }
            break; 
         }
       }
     }
     gameround = true; // userinput over switch to automatic
     
   }
 }

}



Listing 4: loop()-Funktion 2. Teil (Block 2: Usereingabe mit Visualisierung)


In Block 2 wurden 2 Möglichkeiten implementiert (mit define) um den Zeitpunkt zu bestimmen wann eine Usereingabe übernommen wird.

TIMEBASED: Nach einer bestimmten Zeit wird die Usereingabe aktiviert SIGNALBASED: Die Usereingabe wird nach dem Wert 6 der im seriellen Puffer steht übernommen

In der eigentlichen Schleife für die Usereingabe wird der serielle Port gepollt und abgefragt mit Serial.available() abgefragt ob Daten im Puffer stehen. Wenn der Rückgabewert > 0 ist wird mit Serial.Read() ein Byte gelesen und in incoming geschrieben.

incoming wird dann per LED ausgegeben und darauf verglichen ob der mit dem im Array gespeicherten Wert übereinstimmt. Dies wird so oft wiederholt bis das Array einmal durchlaufen wurde und alles übereingestimmt hat.

Falls ein Wert falsch ist wird gameover = true gesetzt und kitt() "abgespielt". Falls alles richtig war wird gameround = true gesetzt und ein weiterer Wert wird in Block 2 generiert.

Fazit

Eine schönes kleines Projekt, dass jeden Teilnehmer Spass gemacht hat und verschiedenste Aufgabengebiete abdeckt.

Quellen