Verkehrsampel V2

In diesem Programm sind die abstrakteren Konzepte (Traffic light, state) von den niedrigen Ebenen (lamp, LEDs) getrennt.

// Two traffic lights, using the standard pattern (R,R+Y,G,Y).

// version 2

#include <Adafruit_NeoPixel.h>

// we use a single strip of 14 LEDs, two LEDs per lamp
// traffic light A with three lamps:  RED( 1, 2), YELLOW( 3, 4), GREEN( 5, 6)
// traffic light B with three lamps:  RED(11,12), YELLOW( 9,10), GREEN( 7, 8)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(14,26);

 // position of the first LED of each LAMP for each TL
int ledPos[2][3] = { 
    {1,3,5},            // tlA
    {11,9,7}            // tlB
};

#define PIX_PER_LAMP 2                      // number of led pixels per lamp

int tlA=0, tlB=1;                           // the two Traffic Lights


// switching times; note that "red time" is only the overlap between the traffic lights
// id addition a TL will always be red when its opposing TL shows yellow or green

// standard times per STATE (durations in msec)
int stdTimes[4] = { 500, 1000, 10000, 2000 };

// specific times for each TL
int times[2][4];                    // specific durations for each traffic light

// symbolic constants for the different STATES of a TL
#define STOP                0       // red lamp on
#define ALERT               1       // simultaneously red and yellow on
#define GO                  2       // green lamp on
#define HALT                3       // yellow lamp on

// the three base colors; note that brightness will be adjusted separately
uint32_t colors[3] = {
    strip.Color( 255,   0,   0 ),       // RED
    strip.Color( 128, 128,   0 ),       // YELLOW
    strip.Color(   0, 255,   0 )        // GREEN
};
// symbolic constants for the lamp color index
#define RED_LAMP     0
#define YELLOW_LAMP  1
#define GREEN_LAMP   2


/*
 * We have several layers of functions, from TOP to BOTTOM:
 *   (7) setGreenTime       : configure switching times
 *   (6) peformCycle        : walk through all states in the correct sequence
 *   (5) stop,alert,go,halt : change state of TrafficLight switching appropriate lamp(s)
 *   (4) xOff               : switch a certain lamp (x) OFF
 *   (3) on, off            : switch a lamp on/off using its fixed color
 *   (2) setColor           : assign a color to an arbitrary lamp, changing all of its LEDs
 *   (1) showFor            : transfer LED settings to the strip and wait  for the appropriate time
 */

// Layer (1) : transfer LED states to the strip and wait for the specified time
void showFor(int msec) {
    strip.show();
    delay(msec);
}

// Layer (2) : set a single lamp to an arbitrary color
void setColor(int tlNr, int lampNr, uint32_t color) {
    strip.fill(color,ledPos[tlNr][lampNr],PIX_PER_LAMP);    
}

// Layer (3) : switch a single lamp ON or OFF using its fixed color
void on(int tlNr, int lampNr) {
    setColor(tlNr,lampNr,colors[lampNr]);    // each lamp has its fixed color
}
void off(int tlNr, int lampNr) {
    setColor(tlNr,lampNr,0);    // color 0 == BLACK == OFF
}

// Layer (4) : switch OFF a certain lamp
void RedOff(int tlNr) {
    off(tlNr,RED_LAMP);
}
void YellowOff(int tlNr) {
    off(tlNr,YELLOW_LAMP);
}
void GreenOff(int tlNr) {
    off(tlNr,GREEN_LAMP);
}

// Layer (5) : change state of TL switching the necessary lamps ON or OFF
void stop(int tlNr) {
    YellowOff(tlNr);
    on(tlNr,RED_LAMP);
    showFor(times[tlNr][STOP]);
}
void alert(int tlNr) {
    on(tlNr,YELLOW_LAMP);
    showFor(times[tlNr][ALERT]);
}
void go(int tlNr) {
    RedOff(tlNr);
    YellowOff(tlNr);
    on(tlNr,GREEN_LAMP);
    showFor(times[tlNr][GO]);
}
void halt(int tlNr) {
    GreenOff(tlNr);
    on(tlNr,YELLOW_LAMP);
    showFor(times[tlNr][HALT]);
}

// Layer (6) : walk through all states in the correct sequence
void performCycle(int tlNr) {
    stop        (tlNr);
    alert       (tlNr);
    go          (tlNr);
    halt        (tlNr);
    stop        (tlNr);   
}

// Layer (7) : configure switching times (adjust duration of GREEN_TIME)
void setGreenTime(int tlNr, int greenTime) {
    for (int d=STOP;d<=HALT;d++) {
        times[tlNr][d] = stdTimes[d];
    }    
    times[tlNr][GO] = greenTime;
}


void setup() {
    Serial.begin(115200);
    delay(100);

    Serial.println("Traffic Lights starting ..");
    strip.fill();
    strip.setBrightness(20);

    setGreenTime(tlA,  5000);
    setGreenTime(tlB, 10000);

    stop(tlA);
    stop(tlB);
}

void loop() {

    performCycle(tlA);
    performCycle(tlB);
}

Ja, so ist es viel besser!

Früher dachte man, dass ein Programm besonders gut ist, wenn es viele Kommentare enthält. Man hat sogar Programme geschrieben, die andere Programme analysieren und den Anteil der Kommentarzeilen ermitteln. Gut bezahlt wurde man mitunter nur, wenn man eine bestimmte „Quote“ erreichte. Heute sehen wir das zum Glück ein wenig differenzierter.

Damals waren die Programmiersprachen noch nicht so weit entwickelt und der Code war sehr technisch. Um ihn zu verstehen, war man auf Kommentare angewiesen, vor allem, wenn man ein Programm ändern sollte, das ein anderere Softwerker fabriziert hatte. Außerdem gab es noch viele Programmierer, die am Anfang Assembler gelernt hatten und bestimmte Gewohnheiten beibehielten, die ihren Programmcode sehr effizient aber auch schwer verständlich machten.

Und heute? Soo viele Kommentare haben wir ja gar nicht verwendet.

Ihr habt das richtige Maß gefunden! Wenn man sprechende Variablennamen benutzt, spricht der Code oft für sich selber. Man kann die Kommentare dann dazu nutzen, abstraktere Konzepte zu erläutern, wie etwa die „Schichtung “ der Funktionen. So soll es sein!

Dann haben wir also das perfekte Programm geschrieben?! Nichts mehr zu meckern?

Oh, vor 30 Jahren hätte man das so gesehen. Seither hat sich jedoch ein Programmierkonzept durchgesetzt, das man als „Objektorientierung“ bezeichnet. Das müßt ihr noch benutzen, damit euer Programm als gut gelten kann.

Um das Prinzip zu verstehen, vergessen wir einmal für einen Moment die Ampeln. Reden wir stattdessen über Hunde!

// This little program demonstrates the idea of OBJECT ORIENTATION.
// version 1

// message logging to the serial port with time stamp
void debug(String message) {
    Serial.println(message);
}

// ============== INTERFACE of class DOG

class Dog {
    // a dog can sleep, eat and play.
    
    public:
        Dog(String name);
        void sleep(); 
        void eat();
        void play();
    private:
        String name;
};

// ============= IMPLEMENTATION of class DOG

Dog::Dog(String name) {
    this->name=name;
}

void Dog::sleep() {
    debug(name+": I am sleeping");
}
void Dog::eat() {
    debug(name+": I am eating");
}
void Dog::play() {
    debug(name+": I am playing");
}


// ================ USING class DOG

void setup() {
    // setup serial port
    Serial.begin(115200);
    delay(1000);

    Dog fluffy = Dog("Fluffy");
    Dog bobby = Dog("Bobby");

    fluffy.sleep();
    fluffy.eat();
    bobby.eat();
    fluffy.play();
}

void loop() {}

Das Programm liest sich ganz natürlich, oder? Im INTERFACE der Klasse Dog wird beschrieben, was ein Hund so tun kann, in der IMPLEMENTATION wird genau erklärt, WIE er das tut und bei der BENUTZUNG der Klasse im setup() werden zwei Hunde (=Objekte vom Typ Hund) erzeugt, denen man dann verschiedene Anweisungen gibt. Ein Hund berichtet bei jedem Aufruf aus der Ich-Perspektive, was er gerade tut. Als Ergebnis sieht man im seriellen Monitor:

Fluffy: I am sleeping
Fluffy: I am eating
Bobby: I am eating
Fluffy: I am playing

Also ja, man versteht es schon irgendwie, aber da ist jetzt schon ziemlich wenig Kommentar…

Ertappt. Ja, das INTERFACE könnte Kommentare vertragen, vor allem, wenn man die Funktionen um Parameter erweitert: WIE LANGE soll der Hund schlafen, WAS soll er fressen, WOMIT spielen? Wenn man ein „echtes“ Interface schreibt, dokumentiert man alles sehr gut, wie etwa zulässige Wertebereiche. Negative Zeitangaben sind beispielsweise definitiv unzulässig. Und natürlich müsste man in der Dokumentation des INTERFACE von Dog::sleep(unsigned int duration); auch die physikalische Einheit der Dauer nennen (Sekunden, Stunden?). Ach ja, es sollte wohl auch ein Höchstdauer geben. Die Funktion kann auf unterschiedliche Weise damit umgehen, wenn der Aufrufer einen zu großen Wert angibt: Sie kann sich wehren und gar nicht „schlafen“ oder sie kann den Wert auf den maximal zulässigen Wert kürzen. Wie sie reagieren wird, muss im Interface dokumentiert werden.


Jetzt kommt aber der eigentliche Clou: Klassen haben die Möglichkeit, ihren ZUSTAND zu speichern. Die Funktionen (man nennt sie auch METHODEN) können diese Zustände PRÜFEN, ihr Verhalten davon abhängig machen und auch die Zustände VERÄNDERN. Die Zustände sind übrigens quasi das Privateigentum der Objekte der Klasse. Ob ein Hund hungrig ist, kann der Aufrufer erst einmal nicht erfahren – es sei denn, der Hund bietet ihm dafür eine Methode an, wie z.B. boolean isHungry(). All das sieht man im folgenden Beispiel:

// This little program demonstrates the idea of OBJECT ORIENTATION.
// version 2

// message logging to the serial port with time stamp
void debug(String message) {
    Serial.println(message);
}

// INTERFACE

class Dog {
    // a dog can sleep, eat and play.
    // It gets tired by playing and will be hungry after sleeping
    
    public:
        Dog(String name);
        void sleep(); 
        void eat();
        void play();
        boolean isHungry();
    private:
        bool hungry;
        bool tired;
        String name;
};

// IMPLEMENTATION

Dog::Dog(String name) {
    this->name=name;
    hungry=false;
    tired=false;
}

void Dog::sleep() {
    if (hungry) {
        debug(name+": cannot sleep. Give me a bone, please!");
    }
    else if (tired) {
        debug(name+": sleeping ..");
        tired=false;
        hungry=true;
    }
    else {
        debug(name+": I am not tired");
    }
}
void Dog::eat() {
    if (hungry) {
        debug(name+": yes, time for eating -- tastes good!");
        hungry=false;
    }
    else {
        debug(name+": I am not hungry");
    }
}
void Dog::play() {
    if (!hungry && !tired)  {
        debug(name+": yes, playing is great fun!");
        tired=true;
        hungry=true;
    }
    else {
        debug(name+": I do not want to play now.");
    }
}

boolean Dog::isHungry() {
    return hungry;
}

// USAGE

void setup() {
    // setup serial port
    Serial.begin(115200);
    delay(1000);

    Dog fluffy = Dog("Fluffy");
    
    fluffy.sleep();
    fluffy.play();
    fluffy.sleep();
    debug("Fluffy might be hungry? : "+String(fluffy.isHungry()));
    fluffy.eat();
    debug("still hungry, Fluffy? "+String(fluffy.isHungry()));
    fluffy.sleep();
    fluffy.play();
    fluffy.sleep();
    fluffy.eat();
}

void loop() {}

Das Prgramm erzeugt folgende Ausgabe:

Fluffy: I am not tired
Fluffy: yes, playing is great fun!
Fluffy: cannot sleep. Give me a bone, please!
Fluffy might be hungry? : 1
Fluffy: yes, time for eating -- tastes good!
still hungry, Fluffy? 0
Fluffy: sleeping ..
Fluffy: I do not want to play now.
Fluffy: cannot sleep. Give me a bone, please!
Fluffy: yes, time for eating -- tastes good!

Damit das Programm nicht zu groß wird, verlagert man das INTERFACE in eine Datei namens Dog.h und die Implementation in eine Datei namens Dog.cpp. Um im setup() des Hauptprogramms auf die Klasse Dog Bezug nehmen zu können, muss man deren Interface über #include "Dog.h" einbinden. Damit die Datei Dog.cpp korrekt übersetzt werden kann, muss sie übrigens auch selbst ihr eigenes Interface per #include einbinden. Die Dateien Dog.cpp und Dog.h müssen im gleichen Verzeichnis gespeichert werden wie die Dog_V2.ino Datei. Außerdem muss man die Option „externen Editor benutzen“ in der Arduino IDE aktivieren. Probiert es aus, denn bei der nächsten Version eurer Verkehrsampel solltet ihr es genau so machen! Lest aber vorher noch einiges im Web über Objektorientierung nach.

Anmerkung: Das Zerlegen eines Programms in mehrere einzelne Quellen, die getrennt übersetzt werden können, ist ein universelles Prinzip, das man in praktisch allen Programmiersprachen findet. Wenn wir irgendeine Bibliothek verwenden (z.B. die Bibliothek für LED-Streifen), dann benutzen wir exakt dasselbe Prinzip: Unser eigenes Programm inkludiert das INTERFACE (also die Header Datei, welche die Autoren der Bibliothek ztur Verfügung gestellt haben), beim Übersetzungsvorgang wird nicht nur unser eigener Quelltext übersetzt, sondern auch der Quelltext der Bibliothek, und beim Bindevorgang („Linking“) werden alle übersetzten Teile („verschiebbarer Objektcode“) hintereinander arrangiert und zu einem Ganzen (dem „Lademodul“) verbunden.

Wenn man Programme zerlegt, werden nicht selten kleinere Ungenauigkeiten sichtbar. In unserem Hunde-Beispiel benutzt beispielsweise die Klasse Dog die Funktion debug(String). Solange der Code der Klasse Dog sich innerhalb der Datei des Hauptprogramms befindet, ist das kein Problem, denn debug wurde gleich zu Beginn des Hauptprogramms definiert. Isoliert man Dog.cpp jedoch als separate Datei, so stellt der Compiler beim Übersetzen dieser Datei fest, dass ihm debug(..) unbekannt ist. Man könnte dies heilen, in dem man eine weitere Datei „Debug.h“ schafft, dort die Funktion deklariert und diese neue Header-Datei sowohl im Haputprogramm inkludiert, als auch in Dog.cpp. Wir haben bei unserem kleinen Umbau das Problem anders gelöst: Dog hat eine eigene Auskunftsfunktion namens void tell(String text); bekommen und benutzt nun diese.


Zurück zu unserer Ampel: Wir wollen eine Klasse namens TrafficLight schaffen und in einen separate Datei TrafficLight.cpp auslagern. Das Interface packen wir in TrafficLight.h und inkludieren es sowohl im Hauptprogramm als auch in der Implementierung der Klasse.

Auf diese Weise ensteht die „obere“ Schicht in unserer Software, nämlich der Code in der *.ino-Datei und die „untere“ Schicht steckt in der Klasse TrafficLight. Wenn wir diese gut strukturieren, dann gibt es auch dort wieder Funktionen wie red(), green() oder off(), die etwas abstrakter sind und andere „niedrige“ Funktionen, die sich darum kümmern, dass die richtigen LEDs angesprochen werden. Bei der Software-Entwicklung ist es ein wichtiges Ziel, technische Details nach unten zu verlagern, damit der Programmcode auf den höheren Ebenen besser verständlich ist.

In der nächsten Version der Software wollen wir auch Funktionen für das Ein- und Ausschalten der Ampelanlage hinzufügen.

Weiter zur Version 3 der Verkehrsampel…

Veröffentlicht am
Kategorisiert in Allgemein