[Projekt] INTERVAL Ersatzstoff

Hier möchte ich euch meinen INTERVAL Ersatzstoff vorstellen.

Wer gar nicht weiß, worum es sich dreht, sollte dieses lesen:
Blink Without Delay
Der Wachmann

Hält man sich an die dort vorgeschlagenen Wege, was auch gut und richtig ist, dann wird der Quellcode mit solchen Statements geflutet:

if(millis() - gemerkteZeit >= wunschLaufzeit)
{
  // tuwas
  gemerkteZeit = millis();
}

Die Wurst ist total korrekt.
Aber dennoch unangenehm, da mit viel Schreibarbeit verbunden.
Es schleichen sich schnell Copy+Paste Fehler ein.
Der Quellcode des Projektes wird recht unübersichtlich. Um so schlimmer, je öfter das Konstrukt darin vorkommt.

Hier die von mir erstellten Abstraktionen des Problems:
Multitasking Macros
Intervall Macro

An dieser Stelle ein herzliches Danke, an die Spender der (in den Threads versteckten) Anregungen.
Im folgenden habe ich versucht sie weitestgehend aufzunehmen.

Zum "WARUM?" bei mir ein Ersatzstoff, für INTERVAL und die TaskMakros, her muss:

Eigentlich funktionieren diese Dinge prächtig!
Sind aber gewissen Einschränkungen unterworfen, was hauptsächlich an der Verwendung der statischen Daten liegt. Diese verhindern eine Mehrfachverwendung der erstellten Funktionen. Ich nenne solche Funktionen "Wegwerf Funktionen". Denn sie sind nur einmal pro Projekt nutzbar. Braucht man z.B. drei unabhängige Wechselblinker, dann muss man die Funktion kopieren, so dass man, im schlimmsten Fall, 3 vollständig identische Kopien in einem Programm hat.
Ebenso verhält es sich wenn man diese Makros in Klassen einbaut, dann ist nur eine Instanz nutzbar. Mehrere Instanzen führen zu Fehlverhalten. Eine "Wegwerf Klasse".

Was im prozeduralen Umfeld gerade noch erträglich erscheint, macht im OOP Umfeld die wichtigsten OO Möglichkeiten kaputt.
Dabei ist doch gerade die Wiederverwendbarkeit eins der vordringlichsten Ziele der OOP.

Ist klar geworden, wo der Hase im Pfeffer liegt?

Hier jetzt der neue Ansatz, um das BlinkWithoutDelay Problem zu erschlagen:
BlinkWithSimpleTimer.ino

#include "CombieTimer.h"

Combie::SimpleTimer timer; // timer Instanz anlegen

void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
    // timer.start();
}

void loop()
{
  if(timer(1000)) // wenn abgelaufen
  { 
    digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
    timer.start();
  }
}

Ich nutze NameSpaces, da es schon eine SimpleTimer Lib gibt, und ich damit nicht kollidieren möchte. Wenn man sich den Code anschaut, dann ist mein SimpleTimer deutlich simpler, als der fremde SimpleTimer.

Daraus leite ich, für mich, das moralische Recht ab meinen SimpleTimer auch so zu nennen.
:smiling_imp: :smiling_imp:

Erklärung:
Der SimpleTimer startet immer im abgelaufenen Zustand. Er wird also bei der ersten Nutzung true liefen. Will man das nicht, sollte man in setup() ein timer.start() notieren, so wie es im Beispiel vorbereitet ist.

Vergleichbares hier nochmal in einer abgewandelten Version:
BlinkWithPulsator.ino

#include "CombieTimer.h"

Combie::Pulsator puls(1000); // liefert alle 1000 ms einmal true sonst false

void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
  puls.start(); // dank des start(), beginnt puls mit Pause
}

void loop()
{
  if(puls) digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
}

Eine Entprelleinrichtung, basierend auf dem SimpleTimer:
TasterEntprellen.ino

#include "CombieTimer.h"
using Combie::EntprellTimer;

const byte taster =  2; // taster gegen GND schaltend

EntprellTimer entprellen(200);   // in der Praxis wird man wohl eher 20ms verwenden

void setup()
{
 pinMode(LED_BUILTIN,OUTPUT);   
 pinMode(taster,INPUT_PULLUP);
} 

void loop()
{
  // die LED zeigt das, vom prellen bereinigte, Signal
  digitalWrite(LED_BUILTIN,entprellen(!digitalRead(taster)));  // invers, wg. pullup
}

Und zum Abschluss noch eine Aufgabe um ein Signal zu formen:
Schaltsequenz.ino

/**
 *    Ablaufdiagramm - Zeitdiagramm
 *    
 *        S1    _----------_____  Schalterstellung
 *        OUT1  _-------------__  Verzoegertes abschalten
 *        OUT2  ____-------_____  Verzoegertes einschalten
 *
*/

#include "CombieTimer.h"
using namespace Combie;

const byte S1   = 2; // Schalter Pin
const byte OUT1 = 3; // Ausgang
const byte OUT2 = 4; // Ausgang

EntprellTimer    entprell(20); // Schalter entprellen
RisingEdgeTimer  ton(500);     // steigende Flanke wird verzoegert
FallingEdgeTimer toff(500);    // abfallende Flanke wird verzoegert
 
void setup(void) 
{
  pinMode(S1,INPUT_PULLUP);
  pinMode(OUT1,OUTPUT);
  pinMode(OUT2,OUTPUT);
}

void loop(void) 
{
  bool schalter = entprell(!digitalRead(S1)); // invers wg. pullup
  digitalWrite(OUT1, toff(schalter));
  digitalWrite(OUT2, ton(schalter));
}

Zu guter Letzt noch die CombieTimer.h in welcher die ganze Magie steckt:

#pragma once

namespace Combie 
{
    
    
    class SimpleTimer
    {
      private:
      unsigned long timeStamp = 0;
      bool abgelaufen = true; // default Status: timer abgelaufen
    
      public:
      void start()
      {
        timeStamp   = millis();
        abgelaufen  = false;
      }
    
      bool operator()(const unsigned long ablaufZeit) 
      {
        if(!abgelaufen) abgelaufen = millis() - timeStamp >= ablaufZeit;
        return abgelaufen;
      }
    };
    
    
    class Pulsator   // liefert alle X ms einen HIGH Impuls
    {
      protected: 
      SimpleTimer timer;
      unsigned long interval;
      
      public:
      Pulsator(unsigned long interval):interval(interval){}
      
      void setInterval(const unsigned long _interval)
      {
         interval =  _interval;
      }

      
      void start()
      {
        timer.start();
      }
      
      operator bool()
      {
          bool result = false;
          if(timer(interval))
          {
            result = true;
            timer.start();
          }
          return result;
      } 
    };
    
    class EdgeTimer     // Die abstakte Mutter der Flankenverzoegerer
    {
      protected:
      SimpleTimer timer;
      unsigned long laufzeit = 0;
    
      public:
      explicit EdgeTimer(const unsigned long laufzeit):laufzeit(laufzeit){}
      
      virtual bool operator()(const bool trigger) = 0;
      
      void setLaufzeit(const unsigned long _laufzeit)
      {
         laufzeit =  _laufzeit;
      }
    };
    
    class FallingEdgeTimer: public EdgeTimer // abfallende Flanke wird verzoegert
    {
      public:
      using EdgeTimer::EdgeTimer;
      
      virtual bool operator()(const bool trigger)
      {
        if(trigger) timer.start();
        return !timer(laufzeit);
      }
    };
    
    class RisingEdgeTimer: public EdgeTimer // steigende Flanke wird verzoegert
    {
      public:
      using EdgeTimer::EdgeTimer;
      
      virtual bool operator()(const bool trigger)
      {
        if(!trigger) timer.start();
        return timer(laufzeit);
      }
    };
    
    class EntprellTimer
    {
      private:
      SimpleTimer timer;
      bool merker = false;  //  zustand
      const unsigned long entprellzeit;
      
      public:
      explicit EntprellTimer(const unsigned long entprellzeit) : entprellzeit(entprellzeit)  {}
      
      bool operator()(const bool trigger)
      {
         if(merker == trigger)   timer.start();
         if(timer(entprellzeit)) merker = !merker;
         return merker; 
      }
    };
    
}

Die CombieTimer.h kann man einfach mit ins Projektverzeichnis werfen und wird dann von Arduino in einem Tab angezeigt.

Vorschläge?
Ansichten?
Kritik?
Würde mich freuen!

Sieht gut aus :slight_smile:

Der bool Operator in EdgeTimer sollte abstrakt sein. Im Kommentator schreibst du auch dass dass so sein soll:

virtual bool operator()(const bool trigger) = 0;

In C++ nennt sich das auch "pure virtual function"

Danke für die Blumen!

Siehste, das "=0" ist mir doch glatt durchgerutscht!
Beabsichtigt war es, da hast du vollkommen recht.
(done+danke)

Das sieht sehr gut aus. Besonders interessant ist, wie vielfältig die Anwendung einer Grundfunktion durch wenig Steuercode gestaltet werden kann.

Gruß Tommy

Hab auch mal ein bisschen mit dem Timer rumgespielt. Ausser, dass man eine Einschaltschaltverzögerung bauen kann, sehe ich noch keine Vorteile gg. INTERVAL.h
Nachteilig ist, dass ja der Befehl timer.start(); noch mit rein muss.
Oder liegen die Vorteile unter der Haube?

Bin INTERVAL.h Fan der ersten Stunde

sehe ich noch keine Vorteile gg. INTERVAL.h

Wenn INTERVAL für dich funktioniert, dann ist es genau das richtige für dich.

Ich bin auch weiterhin ein wenig Stolz auf den INTERVAL Ansatz!
Verwende ihn gerne.
Und froh, dass er dir gefällt!

Die Vorteile des SimpleTimer liegen eher im strukturellen Bereich.
Ins Besondere, wenn es Richtung Wiederverwendung und Vererbung geht.
Das sind die Bereiche, wo einem INTERVAL dann im Weg rum steht.

Bin auch INTERVAL.h Fan und verwende das häufig. Besten Dank für den Code!
Bin aber auch schon sehr neugierig was hier entwickelt wird! :slight_smile:

@Tommy
Auch an dich, meinen Dank, für die Anerkennung.

Es war ein recht weiter Weg bis dahin.
Eigentlich bin ich erst mit C++ angefangen, als ich hier eingeschlagen bin.
C und andere Sprachen waren mir da schon bekannt.

Da stecken also so ca 3 Jahre hobbymäßige Beschäftigung drin.
Sicherlich 1000 Versuche, irgendwelche Timer zu basteln, von denen ihr hier im Forum nur eine kleine Auswahl gesehen habt.
Das ist auch gut so, denn die meisten waren Irrwege.

Ich arbeite gerne mit wechselnden Intervallzeiten bei dem jeweiligen Timern. Normalblink, Schnellblink, asymetrisch, Blitz, etc.

Etwa. z.B. so

#include <CombieTimer.h>
Combie::SimpleTimer timer; // timer Instanz anlegen

uint32_t interval = 1000;

void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop()
{
  if (timer(interval)) // wenn abgelaufen
  {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    interval = digitalRead(LED_BUILTIN) ? 200 : 1000;
    timer.start();
  }
}

Leider muss man dafür immer eine globale Variable (uint32_t interval=1000; in diesem Fall) setzen. Kannst du nix einbauen, was direkt gesetzt (also an beliebiger Stelle im Sketch) werden kann, z.B.:
timer.interval(500)
um dann sowas zu machen:
timer.interval(digitalRead(LED_BUILTIN) ? 200 : 1000);
und dies dann in der
if(timer(interval)) benutzt wird?

In EntprellTimer solltest du noch merker initialisieren

Und geht das nicht einfacher?

if(!(merker^trigger))

@ElEspanol
Für symmetrisches Blinken muss man nur Pulsator um eine Setter-Methode erweitern

timer.interval(digitalRead(LED_BUILTIN) ? 200 : 1000);

So:??

#include "CombieTimer.h"

Combie::Pulsator puls(1000);

void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
}

void loop()
{
  if(puls)
  {
    digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
    puls.setInterval(digitalRead(LED_BUILTIN) ? 200 : 1000);
  }  
}

Die CombieTimer.h im Startbeitrag angepasst.

Serenifly:
In EntprellTimer solltest du noch merker initialisieren

Done!

Wobei mein C++ Buch da recht klar ist:
Statische und globale Variabeln, und Objekteigenschaften werden automatisch initialisiert.
Mit einem int 0 entsprechendem Wert des passenden Typs.
Oder muss ich das Kapitel nochmal lesen?

Serenifly:
Und geht das nicht einfacher?

if(!(merker^trigger))

Tja....
Nachdem ich ein Wenig an dem Problem herumgestolpert bin, habe ich mir schlussendlich eine Wahrheitstabelle aufgemalt.
Und der Ausdruck ist das direkte Ergebnis dieser Bemühungen.

Falls dir was schöneres einfällt, men los, habe ein offenes Ohr dafür.

if(merker == trigger)

Done!

Serenifly:
@ElEspanol
Für symmetrisches Blinken muss man nur Pulsator um eine Setter-Methode erweitern

Done.

Das ist etwas komplizierter. Wenn eine Variable nicht in der Initialisierungs-Liste greift die Default Initialisierung. Und die ist für die eingebauten Standard-Datentypen undefiniert.
Unter bestimmten Umständen (und C++11 spielt da auch eine Rolle!) kann aber eine Initialisierung auf Null erfolgen. Das hängt davon ab wie und wo man das Objekt erstellt. Ob das Objekt selbst global oder lokal ist kann da glaube ich auch eine Rolle spielen.
Daher sollte man im Zweifelsfall alles initialisieren. Siehe auch den Abschnitt "Notes" hier:
http://en.cppreference.com/w/cpp/language/value_initialization

Das ist auch ein Punkt wo Debugger verwirren können. Die sind teilweise so nett und führen da eine Initialisierung durch.

Bei dem Ausdruck geht es doch nur darum abzufragen ob die zwei Ausdrücke gleich oder ungleich sind, oder?

Bei dem Ausdruck geht es doch nur darum abzufragen ob die zwei Ausdrücke gleich oder ungleich sind, oder?

Das ist einer dieser Groschen, welcher manchmal etwas braucht....
Schon geändert.

Ja, das Initialisierungskapitel tue ich mir nochmal an!
Da stehen aber auch viele Sachen drin, in so einem C++ Buch :wink:

Ich gebe auch nicht an auf Anhieb zu wissen wann da genau was statt findet. Am einfachsten ist es anzunehmen dass die Variable nicht initialisiert sein kann :slight_smile:

Combie::Pulsator puls(1000); // liefert alle 1000 ms einmal true sonst false

Kann aber dann nicht stimmen.

Das würde ja puls==!millis()%1000 entsprechen, müsste also genau treffen. Tatsächlich funktioniert es aber wie ich gedacht habe.

#include "CombieTimer.h"

Combie::Pulsator puls(1);

void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
}

void loop()
{
  if(puls)
  {
    digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
    puls.setInterval(digitalRead(LED_BUILTIN) ? 1000 : 500);
  } 

  delay(157);  //soll den Rest des Programmablaufs simulieren. 100 bzw. 500 werden nie genau getroffen
}

und das geht auch (noch?) nicht:

Combie::Pulsator puls[2](1);

void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
  pinMode(12, OUTPUT);
  pinMode(8, OUTPUT);
}

void loop()
{
  if(puls[0])
  {
    digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
    puls[0].setInterval(!digitalRead(LED_BUILTIN) ? 1000 : 100);
  } 

  if(puls[1])
  {
    digitalWrite(12,!digitalRead(12));
    puls[1].setInterval(!digitalRead(12) ? 1000 : 100);
  } 

}

ElEspanol:
Kann aber dann nicht stimmen.

Das würde ja puls==!millis()%1000 entsprechen, müsste also genau treffen. Tatsächlich funktioniert es aber wie ich gedacht habe.

#include "CombieTimer.h"

Combie::Pulsator puls(1);

void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
}

void loop()
{
  if(puls)
  {
    digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN));
    puls.setInterval(digitalRead(LED_BUILTIN) ? 1000 : 500);
  }

delay(157);  //soll den Rest des Programmablaufs simulieren. 100 bzw. 500 werden nie genau getroffen
}

Testprogram:

#include "CombieTimer.h"
using Combie::Pulsator;

Pulsator puls(1000);

void setup(void) 
{
  Serial.begin(9600);
  Serial.println();Serial.println("Start");Serial.println();

}

void loop(void) 
{
  if(puls) Serial.println(millis());
}

Resultat:

Start

0
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
11000
12000
13000
14000
15000
16000
17000

Kommt also exakt auf dem Punkt.

Das mein ich nicht. Muss ja auch so sein, weil die loop ja sonst nix zu tun hat.

wenn es nur bei ==1000 gehen würde, würde ja folgendes nicht funzen:

#include "CombieTimer.h"
using Combie::Pulsator;

Pulsator puls(1000);

void setup(void)
{
  Serial.begin(9600);
  Serial.println();Serial.println("Start");Serial.println();

}

void loop(void)
{
  if(puls) Serial.println(millis());

delay(137);
}

Ausgabe:
Start

0
1095
2192
3288
4384
5480
6577
7672
8769
9866
10961
12058
13154
14251
15347
16443
17540
18636
19732

Ich finde es so gut, wie es ist-

Ist ja entsprechend >=1000, bei millis prüft man ja auch auf <=, nicht auf ==

Die Aussage “Combie::Pulsator puls(1000); // liefert alle 1000 ms einmal true sonst false” müsste eher heissen. liefert einmal true nach frühestens 1000ms, eben wenn es im loop wieder dran ist.
Dummerweise nullt es den Zeitzähler bei der Ausführung, auch wenn die nach den 1000ms gewesen ist, und berücksichtigt nicht die tatsächliche “Fälligkeit”.

Ein Array mit Pulsatoren:

#include "CombieTimer.h"
using Combie::Pulsator;


Combie::Pulsator puls[2] = {1000,1000};

const byte led1 = 3;
const byte led2 = 4;


void setup()
{
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
}

void loop()
{
  if(puls[0])
  {
    digitalWrite(led1,!digitalRead(led1));
    puls[0].setInterval(!digitalRead(led1) ? 1000 : 100);
  } 

  if(puls[1])
  {
    digitalWrite(led2,!digitalRead(led2));
    puls[1].setInterval(!digitalRead(led2) ? 1000 : 100);
  } 

}