Optionaler Aufruf einer Funktion in einer Methode

Ich erstelle eine Library um eine Sieben Segment Anzeige mit Neopixel nachbilden zu können.
Der Anwender braucht in seinem Sketch eigentlich nur ein Mapping aus welchen Pixel ein Segment besteht und dann funktioniert das sehr variabel.

Jetzt gibts aber auch Usecases, wo unregelmäßig zwischen zwei Digits zusätzliche Pixel verwendet werden.
Z.b. bei einer Uhr

12:34

zwei zusätzliche Pixel für den Doppelpunkt.

Da ich in der Libray nicht vorsehen kann (will), wo diese zusätzlichen Digits sind, könnte ich den Anwender eine entsprechende Funktion bauen lassen.

Nur hat das den Nachteil, dass ich im Falle eines Displays ohne zusätzliche Pixel eine Dummy Funktion brauch.

Kann mir jeman eine einfache Möglichkeit zeigen, damit ich im Falle ohne zusätzlicher Pixel auch ohne der Dummy Funktion im User Sketch auskommen könnte? Gehts nur mit einer geänderten Klasse oder vieleicht mit einem optionalen Parameter um eine Referenz auf eine Anwender-Funktion zu übergeben?

Ein User-Sketch mit einer entsprechenden Funktion (bzw. auskommentierter Dummy Funktion)

const byte ledPin = 12;                // Which pin on the Arduino is connected to the NeoPixels?
const byte numDigits = 4;              // How many digits (numbers) are available on your display
const byte pixelPerDigit = 16;         // all pixels, including decimal point pixels if available at each digit
const byte addPixels = 2;              // unregular additional pixels to be added to the strip (e.g. a double point for a clock 12:34)

/*
   Segments are named and orded like this

          SEG_A
   SEG_F         SEG_B
          SEG_G
   SEG_E         SEG_C
          SEG_D

  in the following constants you have to define
  which pixel number belongs to which segment
*/

typedef uint16_t segsize_t;                               // fit variable size to your needed pixels. uint16_t --> max 16 Pixel per digit
const segsize_t SEG_A  = 0b0000000000000011;
const segsize_t SEG_B  = 0b0000000000001100;
const segsize_t SEG_C  = 0b0000000000110000;
const segsize_t SEG_D  = 0b0000000011000000;
const segsize_t SEG_E  = 0b0000001100000000;
const segsize_t SEG_F  = 0b0000110000000000;
const segsize_t SEG_G  = 0b0011000000000000;
const segsize_t SEG_DP = 0b0100000000000000;             // if you don't have a decimal point, just leave it zero

#include <Adafruit_NeoPixel.h>         // install library from Library manager
#include <NoiascaNeopixelDisplay.h>    // include the library after your segment definitions

NeoDisplay display(ledPin, numDigits, pixelPerDigit, addPixels);    // create object

/*
uint16_t addOffset(uint16_t position)                      // keep this function in your sketch, don't remove - see example "20 additional pixels"
{
  (void)position;
  return 0;
}
*/

uint16_t addOffset(uint16_t position)                      // you function to keep track of your additional Pixels
{
  uint16_t offset = 0;
  if (position > 1 ) offset = addPixels;
  Serial.println(offset);
  return offset;
}

void setup()
{
  Serial.begin(115200);
  Serial.println(F("\nNoiascaNeopixelDisplay\n01 hello world"));
  display.begin();                     // call .begin() once in setup
  Serial.println(F("Print 1234 on your display"));
  display.print("12");
  display.print("34");
}

void loop()
{
  // put here other code which needs to run:
}

das Source-File der Lib geht nur als Attachement (9000 Zeichen und so ...)

jedenfalls gibts dort im writeLowLevel den Aufruf zur Ermittlung des Offset.

    void writeLowLevel(uint8_t position, segsize_t bitmask, bool addOnly = false) {        // Ausgabe einer Bitmask an eine bestimmte Stelle
      byte offset = position * pixelPerDigit;                        // pixel offset = first pixel of this digit
      offset = offset + addOffset(position);                         // the user can define his own ruleset for offset calculation due to additional pixels
      Serial.print (offset);                                       // nur debug/dev
      Serial.print (" ");
      Serial.println(bitmask, BIN);
      for (byte i = 0; i < pixelPerDigit * segPerDigit; i++)
      {
        if (bitmask & (1UL << i))
          strip.setPixelColor(i + offset, colorFont);
        else
          if (!addOnly) strip.setPixelColor(i + offset, colorBack);  
      }
    }

NoiascaNeopixelDisplay.h (8.01 KB)

Hat die User-Funktion einen festen Namen, oder soll der Anwender die Funktionsadresse als Paramter übergeben?

Im ersten Fall kannst Du die User-Funktion in der Lib mit attribute ((weak)) deklarieren. Dann ist der Linker auch zufrieden, wenn es die Funktion gar nicht gibt. Die Adresse der Funktion ist in dem Fall NULL, was Du in deiner Lib dann abfragen musst.

Im 2. Fall ähnlich: Wenn die Funktionsadresse nicht mitübergeben wird, setzt Du sie zu NULL.

cool ich glaub das weak reicht ...melde mich

Oder so:
(aus meiner Wühlkiste)

#include <Streaming.h>


class Test
{
  private:
  using CallBack = void (*)(int value);
  CallBack funcPtr;
  
  public:
  Test(): funcPtr(nullptr){}
  void setCallBack(const CallBack funcPtr)
  {
    (*this).funcPtr = funcPtr;
  }

  void tuwas()
  {
    if(funcPtr) funcPtr(4711);
  }
};

Test t;

void meineCallbackFunktion(int value)
{
  Serial << "CallbackFunktion: "<< value << endl;
}


void setup() 
{
  Serial.begin(9600);
  Serial << "Start: "<< __FILE__ << endl;
  Serial << "--- "  << endl;
  
  t.tuwas(); // sagt nix
  Serial << "--- "  << endl;
  
  t.setCallBack(meineCallbackFunktion);
  t.tuwas(); // sagt  "CallbackFunktion: 4711"
  Serial << "--- "  << endl;
  
  t.setCallBack(nullptr);
  t.tuwas(); // sagt  nix
  Serial << "--- "  << endl;

  t.setCallBack([](int wert){(void)wert;Serial << "Och! "  << endl;});
  t.tuwas(); // sagt  "Och!"
  Serial << "--- "  << endl;

}

void loop() 
{

}

Moba: soweit ich das feststellen kann, dürfte das funktionieren:

extern uint16_t addOffset(uint16_t position) __attribute__ ((weak)) ;

und

    void writeLowLevel(uint8_t position, segsize_t bitmask, bool addOnly = false) {        // Ausgabe einer Bitmask an eine bestimmte Stelle
      byte offset = position * pixelPerDigit;                        // pixel offset = first pixel of this digit
      unsigned int (*fun_ptr)(unsigned int) = &addOffset;
       if ((*fun_ptr))
        offset = offset + addOffset(position);                         // the user can define his own ruleset for offset calculation due to additional pixels
      for (byte i = 0; i < pixelPerDigit * segPerDigit; i++)
      {
        if (bitmask & (1UL << i))
          strip.setPixelColor(i + offset, colorFont);
        else
          if (!addOnly) strip.setPixelColor(i + offset, colorBack);  
      }
    }

Korrekt? Sehe ich das richtig, dass wegen dem unsigned int das auch am ESP und Co klappen müsste?

combie, das wird noch etwas dauern.

Du kannst einfach schreiben:

if ( addOffset ) {
...
}

Edit: das funktioniert auf allen Plattformen :wink:

Sehe ich das richtig, dass wegen dem unsigned int das auch am ESP und Co klappen müsste?

Ich sehe da kein "unsigned int" !
Nur ein "unsigned short"

@combie:
Kann man den SetCallback() nicht auch durch zwei verschiedene Konstruktoren - einmal ohne, einmal mit dem Funktionspointer als Paramter - ersetzen?

Okay, dann kannst Du es zur Laufzeit nicht ändern, aber das steht vielleicht auch nicht im Pflichtenheft.

Gruß Walter

Kann man den SetCallback() nicht auch durch zwei verschiedene Konstruktoren - einmal ohne, einmal mit dem Funktionspointer als Paramter - ersetzen?

Es gibt meist mehrere Möglichkeiten.
So auch die von dir genannten.

Okay, dann kannst Du es zur Laufzeit nicht ändern, aber das steht vielleicht auch nicht im Pflichtenheft.

Nicht nur das!

Sondern auch:

Wenn die CallBackFunktion eine anonyme Methode (Lambdafunktion) ist, darf sie auf private und public Eigenschaften der umhüllenden Klasse zugreifen.

Eine eigenständige Funktion oder normale Lambdafunktionen, so wie die beiden in meinem Beispiel, können das nicht.

Zauberwörter: Sichtbarkeit und Geltungsbereich.
Soweit einschränken, wie möglich.

Im noiasca Beispiel dreht sich einmal, um den : in der Uhrzeit und ich dichte jetzt den . beim Datum hinzu.
Schon wird man die Funktion zu Laufzeit austauschen müssen/dürfen, oder man benötigt Wissen aus dem Kontext.

In allen diesen Fällen dreht es sich um das injizieren von Abhängigkeiten.

Direktes manipulieren von Objekteigenschaften ist böse.

Also bleiben hauptsächlich folgende saubere Verfahren:

  • Construktor injection
  • Setter injection
  • Injizieren über Methodenparameter
  • Injizieren über Templateparameter

Wir sind ja nicht alleine, oder gar die ersten, mit solchen Problemen
evtl. lesenswert: "oop dependency injection pattern"
Gründe und Wege finden sich da.

oder man benötigt Wissen aus dem Kontext.
Das ist dann spätestens der KO für die Konstruktor-Idee.

Danke!
/W.

Hi

Warum lässt Du den User nicht stur Seine 'Segmente' definieren?
A-H für jede Position - ob Er nun den Punkt recht unten, oder eher knapp unter der Mitte und bei der folgenden Position links von der Ziffer knapp über der Mitte anbringt - meinen Segen hat Er.
Und trotzdem kann Er dann das Segment H der 2.ten und 3.ten Ziffer ansprechen.
(Das H-Segment kann ja auch beide Punkte vereinen ...)

Segmente, Die keine Pixel zugewiesen bekommen, gibt's nicht - alle Anderen kann man nach Herzenslust ein und ausschalten, Farbe ändern und dimmen.
siseg(0,0,3,17,3,20,3,23,3,26,3,55,3,30,3,29,1); // Stelle Null, A ab Pixel 0 Länge 3, B ab Pixel 17 Länge 3, ... G ab Pixel 30 Länge 3, H ab Pixel 29 Länge 1

MfG

für jede Stelle separat? Das finde schon viel Schreibarbeit. Das wären bei einem 4 Stellen Display 32 Arrays. Da gefallen mir meine 8 Bitmaps besser.

Außerdem will ich nicht vorgeben, dass die Pixel eines Segments aufsteigend sind. Angenommen jemand baut so etwas:
display10.png

Oder etwas wo sich die einzelnen Pixel überschneiden:
display9.png

mit einer Bitmap ist das relativ einfach definierbar finde ich.

const segsize_t SEG_A = _BV(3) |_BV(2) | _BV(1) | _BV(0);      
const segsize_t SEG_B = _BV(6) |_BV(5) | _BV(4) | _BV(3);
const segsize_t SEG_C = _BV(9) |_BV(8) | _BV(7) | _BV(6);
const segsize_t SEG_D = _BV(12) |_BV(11) | _BV(10) | _BV(9);
const segsize_t SEG_E = _BV(15) |_BV(14) | _BV(13) | _BV(12);
const segsize_t SEG_F = _BV(18) |_BV(17) | _BV(16) | _BV(15);
const segsize_t SEG_G = _BV(15) |_BV(20) | _BV(19) | _BV(6);
const segsize_t SEG_DP = 0;

display9.png

display10.png

Hi

Eigentlich ein gar nicht so blöder Weg - dann hätte man nur noch einen Start-Pixel, mit Dem das Segment anfängt.
Man MUSS aber zwingend alle Segmente dann gleich aufbauen, oder Sonderfälle noch abfangen.

MfG

Ja, gleich sein müssen die Digits bei mir.
Das "Startpixel" ergibt sich bei mir auf Grund der Schreibposition. Die tracke ich ohnehin in der write Methode.

Woran ich momentan aber selber zweifle, ob man ein Display für HH : MM überhaupt als ein Display betrachten soll.

display13.png

Meistens wird man eh nur die MM stellen. Und ob ich jetzt immer wieder mit setCursor zu der Position fahre, oder von Haus zwei logische Displays führe, eins für die Stunden, eins für die Minuten, das nimmt sich dann eigentlich auch nichts mehr.

Ach so viele Optionen, so wenig Zeit ...

display13.png

combie:
Oder so:
(aus meiner Wühlkiste)

jetzt habe ich mir das auch angesehen.

Wenn ich einen überladenen Initializer mache um optional die callback Funktion beim Initialiseren mitzugeben, wäre das dann so korrekt?

#include <Streaming.h>

class Test
{
  private:
    using CallBack = byte (*)(int value);
    const byte zusatz;                                                                  // eine zusätzliche const vorbereiten
    CallBack funcPtr;

  public:
    Test(): zusatz(0), funcPtr(nullptr) {}

    Test(byte zusatz, CallBack funcPtr): zusatz(zusatz), funcPtr(funcPtr) {}            // Overload richtig?

    byte setCallBack(const CallBack funcPtr)
    {
      (*this).funcPtr = funcPtr;
      return 0;
    }

    void tuwas()
    {
      if (funcPtr)
      {
        byte result = 0;
        result = funcPtr(4711);
        Serial.print("n");             // nur damit ich seh dass er drinnen ist
        Serial.println(result);        // mein externes callback Ergebnis
      }
    }
};


byte meineCallbackFunktion(int value)
{
  Serial << "CallbackFunktion: " << value << endl;
  return 42;
}

Test t1;
Test t2(24, meineCallbackFunktion);

void setup()
{
  Serial.begin(115200);
  Serial << "Start: " << __FILE__ << endl;
  Serial << "--- "  << endl;

  t1.tuwas();                             // sagt weiterhin nix
  Serial << "1-- "  << endl;

  t2.tuwas();                             // sagt  "CallbackFunktion2: 4711" und "n42" wie erwartet
  Serial << "2-- "  << endl;
}

void loop()
{}

Ausgabe eigentlich wie erwartet:
20:16:23.746 -> Start: C:\Daten\myrepository\Arduino\forum\20200502_callback_initilializer\20200502_callback_initilializer.ino
20:16:23.793 -> ---
20:16:23.793 -> 1--
20:16:23.793 -> CallbackFunktion: 4711
20:16:23.793 -> n42
20:16:23.793 -> 2--

ich denke das Warning kommt nicht von mir, sondern aus dem Streaming:

In file included from C:\Daten\myrepository\Arduino\forum\20200502_callback_initilializer\20200502_callback_initilializer.ino:4:0:

C:\Daten\myrepository\Arduino\libraries\Streaming\src/Streaming.h: In function 'Print& operator<<(Print&, _EndLineCode)':

C:\Daten\myrepository\Arduino\libraries\Streaming\src/Streaming.h:102:52: warning: unused parameter 'arg' [-Wunused-parameter]

 inline Print &operator <<(Print &obj, _EndLineCode arg)

                                                    ^~~

aber könnte ich den Code so lassen (auch bezüglich dem return in meinem Callback) oder ist das humbug?

Sieht richtig aus.
Wenns deine Bedürfnisse befriedigt, ist es auch richtig.

ich denke das Warning kommt nicht von mir, sondern aus dem Streaming:

Kann man leicht reparieren!

inline Print &operator <<(Print &obj, _EndLineCode arg) // falsch
inline Print &operator <<(Print &obj, _EndLineCode) // besser

// alternativ
inline Print &operator <<(Print &obj, _EndLineCode arg)
{
  (void)arg; // hinzufügen
// weiter, wie gehabt

not my business.
:wink:

Denoch karma+

Streaming.h angepasst. Karma++