Rothschopf Werner

rowex ganz privat

Angebote

Arduino: LED oder Relais per Tastendruck einschalten und mit Zeitablauf abschalten - der Weg zur objektorientierten Programmierung

In Teil 1 dieser Serie haben wir einen nicht blockierenden Code für das Einschalten und Nachlaufen eines Ausgangs erstellt. Nun wagen wir den nächsten Schritt: eine Einführung in die objektorientierte Programmierung (OOP).

Die objektorientierte Programmierung unter C++ füllt ganze Bücher. Eine umfassende Beschreibung ist hier also gar nicht möglich und soll auch nicht erreicht werden. Die nachfolgenden Sketche sollen nur zeigen, dass der Weg von der prozeduralen Programmierung zur objektorientierten Programmierung am Arduino gar nicht schwer fallen muss.

Von der Struktur zur Klasse

Am Ende von Teil 1 haben wir bereits Strukturen kennengelernt. Solltest du Teil 1 noch nicht durchgearbeitet haben, so rate ich dir, dies zu tun bevor du hier fortsetzt.

Zunächst legen wir statt einer Struktur eine Klasse an. Strukturen und Klassen sind sehr ähnlich, zu den Unterschieden kommen wir später noch.

In den Beispielen aus Teil 1 haben wir bereits gesehen, je ein Button und eine LED bilden eine Gruppe. Es ist naheliegend Button und LED als ein Objekt zu sehen. Beim Durcharbeiten des Beispiels werden wir immer wieder auf bekannte Elemente treffen. Zunächst wieder der ganze Sketch:

/*
  Einstieg in OOP
  by noiasca 
  http://werner.rothschopf.net
*/
class RetriggerOffDelay                      // our class for combination of button and output
{
  private:
    const byte buttonPin;                    
    const byte outputPin;                    
    uint32_t previousMillis = 0;
    byte interval = 3;                        // the offDelay interval in seconds             

  public:
    retriggerOffDelay(byte buttonPin, byte outputPin) :  // the constructor
      buttonPin(buttonPin),
      outputPin(outputPin)
    {}

    void begin() {                            // handles everything what should happen in setup()
      pinMode(buttonPin, INPUT_PULLUP);
      pinMode(outputPin, OUTPUT);
    }

    void tick() {                           
      if (digitalRead(buttonPin) == LOW) {
        digitalWrite(outputPin, HIGH);
        previousMillis = millis();
      }
      if (millis() - previousMillis > interval * 1000UL && digitalRead(outputPin) == HIGH)  
      {
        digitalWrite(outputPin, LOW);
      }
    }
};

RetriggerOffDelay groupA(A0, 2);   // create object (buttonPin, outputPin

void setup() {
  groupA.begin();
}

void loop() {
  groupA.tick();  
}

Wir beginnen mit der Klassendefinition:

class RetriggerOffDelay
{

Unter einer Klasse (auch Objekttyp genannt) versteht man in der objektorientierten Programmierung einen Bauplan für eine Reihe von ähnlichen Objekten. Klingt jetzt komplizierter als es ist. Eine Klasse ist der Oberbegriff, die Objekte sind dann einzelne Instanzen. Oder: Annahme du definierst eine Klasse "Obst" dann wären Objekte z.B. apfel, birne, citrone. Üblicherweise lässt man Klassennamen mit Großbuchstaben beginnen.

Zurück zum Arduino: Jedes unserer Objekte braucht einen buttonPin und outputPin, auch diese sind wieder konstant. Einen Zeitstempel speichern wir in der Variable previousMillis und legen den Intervall fest. Diese Variablen sind "privat" - das heißt sie sind außerhalb der Klasse nicht direkt änderbar:

  private:
    const byte buttonPin;                    
    const byte outputPin;                    
    uint32_t previousMillis = 0;
    byte interval = 3;                        // the offDelay interval in seconds    

Dann kommt der public Teil. Das sind Elemente auf die wir von außen Zugriff haben. Wir benötigen einen Konstruktor damit wir später Objekte anlegen können. Dem Konstruktor übergeben wir eine Initialisierungsliste. Wenn das Objekt angelegt wird, werden auch die Pindefinitionen initialisiert. Den Konstruktor schließen wir mit {} ab.

  public: 
    RetriggerOffDelay(byte buttonPin, byte outputPin) :  // the constructor
      buttonPin(buttonPin),
      outputPin(outputPin)
    {}

interval ist eine "normale" Variable.

Für die Hardware Definitionen die sonst im setup() gemacht werden, legen wir eine eigene Methode .begin() an:

    void begin() {                            // handles everything what should happen in setup()
      pinMode(buttonPin, INPUT_PULLUP);
      pinMode(outputPin, OUTPUT);
    }

begin() schaut fast wie eine Funktion aus. In OOP nennt man Member-Funktionen einer Klasse "Methoden".  In der Methode .tick() erkennen wir unsere ursprüngliche Funktion wieder. Alles was unser Button-LED Objekt im Loop machen muss, ist in dieser Methode.

Das restliche Programm besteht nur mehr aus drei Zeilen:

RetriggerOffDelay groupA(A0, 2);   // create object (buttonPin, outputPin

Zunächst legen wir eine Instanz der Klasse - unser Objekt - groupA an und übergeben dabei den buttonPin und den outputPin.

im setup() rufen wir die .begin() Methode für unser Objekt auf um den pinMode der beiden GPIO festzulegen:

  groupA.begin();

und im loop() rufen wir die .tick() Methode für unser Objekt auf:

  groupA.tick();  

Kompliziert? Nein, ungewohnt vieleicht, das einmal alles selber hinzuschreiben. Auch als Arduino Anfänger verwendet man oft objektorientierte Programmierung, fast jede Library - von LCD bis zu Servo nutzt objektorientierte Programmierung. LED Module oder Schrittmotoren - wirklich fast alles was man so als Arduino Library hinzufügt kommt mit Klassendefinitionen. Das Auslesen von Buttons oder das Ansteuern von LEDs oft sind das perfekte Beispiele für Objekte!

Mehrere Instanzen (Objekte) einer Klasse

Welche Vorteile ergeben sich mit OOP? Auch objektorientiert ist die Ergänzung um weitere Gruppen einfach in drei Schritten zu erledigen:

Weitere Objekte anlegen:

RetriggerOffDelay groupA(A0, 2);   // create object (buttonPin, outputPin
RetriggerOffDelay groupB(A1, 3);
RetriggerOffDelay groupC(A2, 4);  

im setup() die .begin() Methoden aufrufen:

  groupA.begin();
  groupB.begin();
  groupC.begin();

und im loop() jedes objekt antickern: .tick() Methode für unser Objekt auf:

  groupA.tick();
  groupB.tick();
  groupC.tick();

Ganze drei Zeilen mehr für jede Button-Led Gruppe und unser Sketch läuft wieder.

Ein vorletztes Beispiel: Objekte in einem Array

In Teil 1 haben wir den Umgang mit Arrays gelernt. Auch Objekte können in Arrays angelegt werden. Machen wir ein Beispiel mit 6 Nachlauf-Relais. Die eigentliche Klasse bleibt unverändert.

/*
  by noiasca 
  http://werner.rothschopf.net
*/
class RetriggerOffDelay                      // a class for a retriggerable off delay
{
  private:
    const byte buttonPin;                    // GPIO für den Button gegen 5V
    const byte outputPin;                    // GPIO für die LED die nachläuft
    uint32_t previousMillis = 0;             // Zeitstempel, wann die LED das letzte mal eingeschaltet wurde
    byte interval = 3;                       // Nachlaufzeit in Sekunden

  public:
    RetriggerOffDelay(byte buttonPin, byte outputPin) :  // ein Constructor für unsere Klasse mit Initialisierungsliste
      buttonPin(buttonPin),
      outputPin(outputPin)
    {}

    void begin() {                            // die Klasse bekommt eine begin Methode, hier kommt alles rein, was hardware-mäßig zu unserem Objekt gehört und im setup() war
      pinMode(buttonPin, INPUT_PULLUP);
      pinMode(outputPin, OUTPUT);
    }

    void tick() {                             // was die Klasse laufend machen soll, im Prinzip alles was vorher in einer eigenen Funktion durchgeführt wurde
      if (digitalRead(buttonPin) == LOW) {
        digitalWrite(outputPin, HIGH);
        previousMillis = millis();           // Einschaltzeit "merken"
      }
      if (millis() - previousMillis > interval * 1000UL && digitalRead(outputPin) == HIGH)  // wenn Zeit abgelaufen und die LED leuchtet
      {
        digitalWrite(outputPin, LOW);
      }
    }
};

RetriggerOffDelay group[] {
  { A0, 2},   // first objekt,  index 0: buttonPin, outputPin
  { A1, 3},   // second objekt, index 1: buttonPin, outputPin
  { A2, 4},   // third objekt,  index 2: buttonPin, outputPin
  { A3, 5},   // 4th objekt,    index 3: buttonPin, outputPin
  { A4, 6},   // 5th objekt,    index 4: buttonPin, outputPin
  { A5, 7}    // 6th objekt,    index 5: buttonPin, outputPin
};

void setup() {
  for (auto & i : group)  // range based for iterates through array of objects and "i" gets a reference onto the object
    i.begin();
}

void loop() {
  for (auto & i : group)
    i.tick();
}

Der Unterschied ist in der Analge eines Array von Objekten. Dabei weisen wir auch gleich jeder einzelnen Instanz (jeder Button/LED Gruppe) die zwei Pins zu:

TetriggerOffDelay group[] {
  { A0, 2},   // first objekt,  index 0: buttonPin, outputPin
  { A1, 3},   // second objekt, index 1: buttonPin, outputPin
  { A2, 4},   // third objekt,  index 2: buttonPin, outputPin
  { A3, 5},   // 4th objekt,    index 3: buttonPin, outputPin
  { A4, 6},   // 5th objekt,    index 4: buttonPin, outputPin
  { A5, 7}    // 6th objekt,    index 5: buttonPin, outputPin
};

Im setup() nutzen wir auch wieder die "range based for loop".

for (auto & i : group)

Pro Durchgang - für jedes Element des Array group - steht die Referenz (i) zur Verfügung. In der Folge können wir mit

i.begin();

die .begin() Methode für unser Objekt aufrufen.

Analog gehen wir im loop() mit der Methode .tick() vor. Auch .tick() muss für jede Instanz unserer Button-Led Gruppen aufgerufen.

Indidviduelle Intervalle: .set() Methoden

Will man member Variablen verändern, so erstellt man entsprechende .set() Methoden und übergibt den neuen Wert als Parameter. Üblicherweise beginnen diese Methoden mit set gefolgt von dem Variablennamen. Die neue Methode .setInterval() wird im public: Abschnitt angelegt, damit sie von außen aufrufbar ist. Der Aufruf unserer .setInterval() kann anschließend sowohl im setup() wie auch im loop() oder in anderen Funktionen erfolgen.

/*
  OOP setter
  by noiasca
  https://werner.rothschopf.net/
*/

class RetriggerOffDelay                      // a class for a retriggerable off delay
{
  private:
    const byte buttonPin;                    // GPIO für den Button gegen 5V
    const byte outputPin;                    // GPIO für die LED die nachläuft
    uint32_t previousMillis = 0;             // Zeitstempel, wann die LED das letzte mal eingeschaltet wurde
    byte interval = 3;                        // Nachlaufzeit in Sekunden

  public:
    RetriggerOffDelay(byte buttonPin, byte outputPin) :  // ein Constructor für unsere Klasse mit Initialisierungsliste
      buttonPin(buttonPin),
      outputPin(outputPin)
    {}

    void begin() {                            // a method which should be called in setup()
      pinMode(buttonPin, INPUT_PULLUP);
      pinMode(outputPin, OUTPUT);
    }

    void setInterval(byte newInterval)
    {
      interval = newInterval;
    }

    void tick() {                             // a method which should be called permanently in loop()
      if (digitalRead(buttonPin) == LOW) {
        digitalWrite(outputPin, HIGH);
        previousMillis = millis();           // Einschaltzeit "merken"
      }
      if (millis() - previousMillis > interval * 1000UL && digitalRead(outputPin) == HIGH)  // wenn Zeit abgelaufen und die LED leuchtet
      {
        digitalWrite(outputPin, LOW);
      }
    }
};

RetriggerOffDelay group[] {
  { A0, 2},   // first objekt,  index 0: buttonPin, outputPin
  { A1, 3},   // second objekt, index 1: buttonPin, outputPin
  { A2, 4},   // third objekt,  index 2: buttonPin, outputPin
  { A3, 5},   // 4th objekt,    index 3: buttonPin, outputPin
  { A4, 6},   // 5th objekt,    index 4: buttonPin, outputPin
  { A5, 7}    // 6th objekt,    index 5: buttonPin, outputPin
};

void setup() {
  for (auto & i : group)  // range based for iterates through array of objects and "i" gets a reference onto the object
    i.begin();

  group[0].setInterval(4);  // set an individual interval for this button/output group
  group[1].setInterval(5);
}

void loop() {
  for (auto & i : group)
    i.tick();
}

Damit erreichen wir eine Nachlaufzeit von 4000 Millisekunden für den Ausgang GPIO 2 - ausgelöst durch den Button an A0, bzw. sinngemäß einen Nachlaufzeit von 5000 Millisekunden für die zweite Button-LED Gruppe.

Struktur versus Klasse

Der einzige Unterschied zwischen einer Klasse und einer Struktur in C ++ besteht darin, dass Strukturen standardmäßige öffentliche Mitglieder und Variablen haben und Klassen standardmäßige private Mitglieder und Variablen haben. Sowohl Klassen als auch Strukturen können eine Mischung aus öffentlichen und privaten Mitgliedern haben. Coole Sache: kennst du Strukturen - kennst du Klassen.

Damit der Umgang etwas leichter fällt, schreiben wir private: und public: auch explizit in die Klasse.

Ich würde empfehlen, Strukturen als einfache "alte" Datenstrukturen ohne klassenähnliche Merkmale und Klassen als aggregierte Datenstrukturen mit privaten Daten und Elementfunktionen zu verwenden.

Zusammenfassung

  • Wir haben einen ersten eigenen Sketch in objektorientierter Programmierung geschrieben
  • Wir haben gelernt, dass auch Objekte in Arrays gehalten werden können
  • um sie dann noch einfacher einer range based for loop zu behandeln

Links

Werbung: Die mit Sternchen (*) gekennzeichneten Verweise sind sogenannte Provision-Links (Affiliate). Wenn du auf so einen Verweislink klickst und über diesen Link einkaufst, bekomme ich von deinem Einkauf eine Provision. Für dich verändert sich der Preis nicht.


Protokoll

First upload: 2020-04-08 | Version: 2020-04-12