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

In Teil 1 dieser Serie hast du einen nicht blockierenden Code für das Einschalten und Nachlaufen eines Ausgangs erstellt. Nun folgt der 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 hast du bereits Strukturen kennengelernt. Solltest du Teil 1 noch nicht durchgearbeitet haben, so rate ich dir, dies zu tun bevor du hier fortsetzt.

Zunächst legst du 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 hast du bereits gesehen, dass je ein Button und eine LED eine Gruppe bilden. 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) : 
      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, die Objekte mit Kleinbuchstaben.

Zurück zum Arduino: Jedes unserer Objekte braucht einen buttonPin und einenoutputPin, 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) : 
      buttonPin{buttonPin},
      outputPin{outputPin}
    {}

interval ist eine "normale" Member 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 legst du eine Instanz der Klasse - das Objekt - groupA an und übergibst dabei den buttonPin und den outputPin.

im setup() rufst du die .begin() Methode für das Objekt auf um den pinMode der beiden GPIO festzulegen:

  groupA.begin();

und im loop() rufst du die .tick() Methode für das 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 sind oft sehr gute 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, der letzten LED Einschaltung
    byte interval = 3;                       // Nachlaufzeit in Sekunden

  public:
    RetriggerOffDelay(byte buttonPin, byte outputPin) :  // Konstruktor mit Initialisierungsliste
      buttonPin{buttonPin},
      outputPin{outputPin}
    {}

    void begin() {                            // 
      pinMode(buttonPin, INPUT_PULLUP);
      pinMode(outputPin, OUTPUT);
    }

    void tick() {                             // 
      if (digitalRead(buttonPin) == LOW) {
        digitalWrite(outputPin, HIGH);
        previousMillis = millis();           // Einschaltzeit "merken"
      }
      if (millis() - previousMillis > interval * 1000UL && digitalRead(outputPin) == HIGH)
      {
        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 
    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
    byte interval = 3;                        // Nachlaufzeit in Sekunden

  public:
    RetriggerOffDelay(byte buttonPin, byte outputPin) : 
      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)  
      {
        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
    i.begin();

  group[0].setInterval(4);  // 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

Die mit Sternchen (*) gekennzeichneten Verweise sind sogenannte Affiliate/Provision-Links. Wenn du auf so einen Verweis klickst und über diesen Link einkaufst, bekomme ich von deinem Einkauf eine (kleine) Provision. Für dich verändert sich der Preis dadurch nicht. Ich empfehle nur Produkte die ich selber besitze und wenn ich überzeugt bin, dass sie für andere interesssant sind.

Protokoll

First upload: 2020-04-08 | Version: 2021-09-01