Callback oder platzhalter Funktionen in eigener Arduino Library

Hallo zusammen,
ich habe zwar schon einiges zusammengebastelt, aber das ist mein erster versuch eine eigene library umzusetzen.
Ein teil der library soll es dem programmierer einfach machen, einen button oder potentiometer einzubinden. In der library geht es um soundmodule, deshalb ist es wichtig, dass nicht in jedem durchgang durch pin abfragen verzögert wird und es darf auch kein delay verwendet werden. Da die library sowieso mit zeitmarken arbeitet, würde ich gerne auch die abfragen für die pins dorthin auslagern.

Was ich mir vorstelle ist folgendes:
Im sketch kann man eine eigene funktion bei der library registrieren, zB:

library.registerButtonDown(pin, buttonPressed);

//wird dann von der library aufgerufen:
void buttonPressed(int value){}

in der library werden dann zB mit 60 fps, also alle x millisekunden, alle registrierten buttons abgefragt und je nach statusänderung die "callback" funktion (zB buttonPressed) aufgerufen.

Momentan sieht es in der library so aus, funktioniert auch, nur leider ist es etwas unsauber und nicht dynamisch:

//in header datei:
typedef void (*ButtonDownFunction)(int);

class Lib1
{
    public:
        void registerButtonDown(int pin, ButtonDownFunction f);
    //...
    private:
        ButtonDownFunction buttonDownCallback = {};
}


//in .cpp datei
void Lib1:: registerButtonDown(int pin, ButtonDownFunction f){
    buttonDownCallback = f;
    buttonPin=pin;
}

//in der library an entsprechender stelle dann, wenn der button gedrückt wurde:
buttonDownCallback(pressedButtonPin);

Zusatzlich musste ich eine leere platzhalter-funktion definieren, die der buttonDownCallback variable zugewiesen wird, weil wenn sie sonst aufgerufen wird das programm abstürzt.

Ausserdem stört es mich, so viele variablen und funktionen in der header datei und generell zu definieren, das macht alles unübersichtlich und unverständlich für jemanden, der die library nur benutzen will.

Ich könnte mir auch vorstellen in der sketch datei einen block zu registrieren, also zB:

library.registerButtonDown(pin1, {
//button1 pressed
});

Zusätzlich würde ich gerne in der library nicht nur einen button registrieren können, sondern mehrere. Leider habe ich es nicht geschafft, das ganze mit arrays zu kombinieren. Theoretisch müsste man die pins der registrierten buttons und zugehörig pointer auf die registrierten funktionen speichern, vielleicht als struct in einem array.

Da bin ich mir aber nicht sicher wie und ob das mit arduino funktioniert.
Vielleicht könnt ihr mir helfen, wie ich die letzten Punkte umsetzen kann, generell fällt es mir schwer, die richtige syntax für die "dynamische" variante zu finden.
Danke :slight_smile:

Ich habe keine wirkliche Ahnung, was du meinen könntest....

Aber ich habe ein Callback Beispiel in der Wühlkiste, vielleicht könntest du mir sagen, was dir daran nicht gefällt!

class Test
{
  protected:
   using CallBack = void (*)(int value);

   CallBack callBack = nullptr;
  
  public:
   void onTuWas(CallBack callBack)
   {
     this->callBack = callBack;
   }

   void tuWas(int value)
   {
     if(callBack) callBack(value);
   }
   
};

Test test;


void meinCallBack(int value)
{
   Serial.print("meinCallBack: "); Serial.println(value);
}


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

 test.tuWas(42);
 
 test.onTuWas(meinCallBack);
 test.tuWas(43);

 
 test.onTuWas([](int value){Serial.print("lambdaCallBack: "); Serial.println(value);});
 test.tuWas(44);
 
 test.onTuWas(nullptr);
 test.tuWas(45);
}

void loop() 
{

}

Zur eigentlichen Frage reicht es bei mir nicht, aber was gefällt dir z.B. an der OneButton Lib nicht, dass du etwas gleichartiges in deine Lib noch mal machen willst?

Zusatzlich musste ich eine leere platzhalter-funktion definieren, die der buttonDownCallback variable zugewiesen wird, weil wenn sie sonst aufgerufen wird das programm abstürzt.

Du musst nur abfragen ob der Funktionszeiger NULL ist und dann nichts tun. Das ist nicht anders wie bei allen anderen Zeiger-Zugriffen

Theoretisch müsste man die pins der registrierten buttons und zugehörig pointer auf die registrierten funktionen speichern, vielleicht als struct in einem array.

Dann mach das. Die Größe des Arrays entspricht der maximalen Anzahl an Bottons. Die kann der Anwender bei Bedarf im Header ändern

Danke für die Tipps :slight_smile:

@Serenifly NULL ist mir aus der objektorientierten Programmierung bekannt, bin ich nur nicht drauf gekommen im zusammenhang mit Arduino. Obwohl es eigentlich klar ist, ist mir das hier noch nie begegnet..

@noiasca Die One button lib schaue ich mir mal an, vielleicht kann ich daraus etwas übernehmen. Warum ich bisher nicht nach so etwas gesucht habe: In meiner library gibt es sowieso viele timing events und ich wollte sozusagen den overhead gering halten.

@combie vielen dank, schaue ich mir mal an

Arduino ist ganz normales C/C++. Die Arduino Software ist nur eine C++ API. Und du hast (zumindest auf den AVRs) keine Standard Template Library.

Funktionszeiger auf NULL abfragen bevor man sie aufruft wirst du in allen Libraries sehen die das verwenden. Ansonsten kannst du einen Software Reset machen (und es gibt tatsächlich Leute die das absichtlich tun, obwohl es einen saubereren Weg gibt)

Hi

Serenifly:
... Ansonsten kannst du einen Software Reset machen (und es gibt tatsächlich Leute die das absichtlich tun, obwohl es einen saubereren Weg gibt)

  • meld *
    Zuerst ködern und dann keine INfo's liefern ist aber nicht fein :wink:
    Ein JMP 0 springt auf den RESET-Vector, also die Routiene, Die bei einem RESET aufgerufen wird.
    (Im Normalfall wird dort der Sprung zum Programm ausgeführt, beim Arduino wohl eher zum Bootloader)

Weshalb Das unsauber ist, will mir (noch?) nicht in den Kopf - der µC macht's nicht anders!
Da dort das ganze Programm von Vorne gestartet wird, können Einem bestenfalls Überreste im RAM stören - Die stören dann aber auch im normalem Programm-Ablauf, da Das 'nicht sein darf' - man also nur deklarierte aber nicht initialisierte Variablen benutzt (Die JEDEN Wert haben können) und darauf hin irgend was macht - der Kompiler meckert bei so was aber auch.
(globale Variablen werden vom Arduino aus mit 0 initialisiert, in Funktionen bekommt man 'nur' eine Speicherstelle für die Variable - und Dort kann stehen, was will)

MfG

(Im Normalfall wird dort der Sprung zum Programm ausgeführt, beim Arduino wohl eher zum Bootloader)

Nööö...

Der Bootloader liegt oben im Flash.
Garantiert nicht auf Adresse Null.

Was dann auch ein Beweis dafür ist, dass ein Jump Null kein Reset ist. Denn der Bootloader kommt nicht dran.

Weshalb Das unsauber ist, will mir (noch?) nicht in den Kopf - der µC macht's nicht anders!

Doch!

Alle internen Register werden beim Reset auf ihre Defaultwerte gesetzt.
Das kann ein Jump oder Call nicht leisten.

Und darum:
Der "Richtige" Weg geht über den harten Reset!
Egal welcher!
PowerOn WDT BrownOut Extern, egal...

Also muss ein solcher "Softreset" über den WDT oder einen externen Baustein erfolgen. z.B. ein dedizierter Resetcontroller.


Modernere AVR kennen übrigens ein Reset Kommando!
Die üblichen Arduino AVR kennen das nicht.

Um es klar zu machen: WDT aktivieren und in eine Endlosschleife gehen

Wobei dann auch noch die Frage offen bleibt:
Wozu überhaupt ein von der Software ausgelöster Reset?

Das ist die Kernfrage!
Ohne Beantwortung der Frage ist jegliche Diskussion über das "Wie?" eher ohne jeden Sinn.
Flüssiger als Wasser.
Überflüssig.
:o :o :o

Nur einen einzigen Grund kann ich mir vorstellen!
Der (besondere) Bootloader soll von einer externen Datenquelle das Programm im Flash neu beschreiben.
Also z.B. ein Bootloader, welcher von einem I2C Speicher oder einer SD Karte das neue Programm holt.

Hi

Ok, dann ist die Reihenfolge ein klein Wenig anders :slight_smile:
Zuerst der interne Reset (Register auf Start-Werte) und DANN jmp 0 (dort steht ebenfalls ein jmp zum Programm, sonst würden wir die Vector-Tabelle ausführen, Die ebenfalls nur jmp's enthält).

Ok, der Einwand, daß die Register nicht auf Start-Wert gesetzt sind, ist gut.
Beim WDT-Reset gab's doch Probleme damit, daß Dieser nach dem Reset auf Standard steht :wink: und ein Reset auslöst, bevor der Bootloader die Kontrolle an den Sketch übergibt - damit Boot-Loop und 'aus die Maus' - oder hat Sich Da was geändert oder sind schlicht meine Informationen (gelesen in den Tiefen des WWW ... oder hier, kA) überaltert oder gar falsch?

... ok ... man könnte auch einfach nach einem Beispiel-Sketch zum Thema WatchDog suchen ... zugegeben ...

MfG

postmaster-ino:
Beim WDT-Reset gab's doch Probleme damit, daß Dieser nach dem Reset auf Standard steht :wink: und ein Reset auslöst, bevor der Bootloader die Kontrolle an den Sketch übergibt

Das war glaube ich nur auf dem Mega mit altem Bootloader. Der Bug wurde vor langer Zeit beseitigt, aber die Firmen hatten noch viele alte Boards

Beim WDT-Reset gab’s doch Probleme damit, daß Dieser nach dem Reset auf Standard steht :wink: und ein Reset auslöst, bevor der Bootloader die Kontrolle an den Sketch übergibt - damit Boot-Loop und ‘aus die Maus’ - oder hat Sich Da was geändert

Optiboot kann damit um.
Also der UNO sowieso und auch alle Nano mit neuem Bootloader.

Zuerst der interne Reset (Register auf Start-Werte) und DANN jmp 0 (dort steht ebenfalls ein jmp zum Programm,

Bei Arduinos erfolgt der Sprung ins obere Flash.
(per Fuses so festgelegt)
Erst der Bootloader macht einen Jump Null

combie:
Ich habe keine wirkliche Ahnung, was du meinen könntest....

Aber ich habe ein Callback Beispiel in der Wühlkiste, vielleicht könntest du mir sagen, was dir daran nicht gefällt!

class Test

{
 protected:
  using CallBack = void (*)(int value);

CallBack callBack = nullptr;
 
 public:
  void onTuWas(CallBack callBack)
  {
    this->callBack = callBack;
  }

void tuWas(int value)
  {
    if(callBack) callBack(value);
  }
 
};

Test test;

void meinCallBack(int value)
{
  Serial.print("meinCallBack: "); Serial.println(value);
}

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

test.tuWas(42);

test.onTuWas(meinCallBack);
test.tuWas(43);

test.onTuWas((int value){Serial.print("lambdaCallBack: "); Serial.println(value);});
test.tuWas(44);

test.onTuWas(nullptr);
test.tuWas(45);
}

void loop()
{

}

Also mir gefällt das sehr gut! :grinning: Das ist quasi, wonach ich gesucht habe, nur verstehen tu ich es nicht. Dürfte ich dich, combie (oder gern auch jemand anders, der es kann), bitten, mir deinen Code zu erläutern? Zumindest das, was in der Klasse Test passiert.

Ganz lieben Dank und Grüße!

Zumindest das, was in der Klasse Test passiert.

Mit using wird ein Datentype für einen Funktionszeiger (function pointer) definiert.

onTuWas speichert einen solchen Zeiger in der Instanz
tuWas nutzt den Zeiger, wenn er gesetzt ist.

Super, danke für die schnelle Rückmeldung!

Was mir jetzt eigentlich nur noch fehlt, ist das hier:

   using CallBack = void (*)(int value);

Ich dachte mir in meinem Leichtsinn, ich könnte auch das einfach overloaden, so dass ich, abhängig von verschiedenen Parametertypen (und Parameteranzahlen) dieselbe Grundlage nutzen kann (Fehlanzeige, "redeclaration"). Geht das irgendwie?
Ich habe diesen Code-Teil noch nie gesehen, bin allerdings auch blutiger Anfänger in C++. Könntest du das evtl. noch etwas weiter ausführen oder eine gute Quelle zum Einlesen dieser "Funktionalität" liefern?

KA, was du meinst....

using

Wenn dir typedef geläufiger ist, hat combie da eine neuere Schreibweise für

typedef void (*CallBack )(int);

geschrieben.

Danke für die Blumen!

Allerdings, "meine" Erfindung ist das nicht.
Hier gilt mein Dank den C++11 Entwicklern.

Aber eins stimmt!
Seit es using gibt, ist typedef nahezu ganz bei mir durch.
Using ist mir viel "näher", als das typedef.

Einzig beim basteln der TypeTraits habe ich noch keine allumfassende Alternative gefunden.

Using ist mir viel “näher”, als das typedef.

Nicht alles Neue ist unnötig. Wie Combie und wohl viele andere finde ich die using Syntax einfacher als typedef. Gerade hier beim Beispiel Funktionszeiger ist bei typedef doch eher versteckt, was da genau definiert wird.

Dass Combie das erfunden hätte, hab ich nie behauptet. Wäre dann auch kein Lob :slight_smile: